Skip to content

Commit c2c6fcd

Browse files
committed
feat(fgit): 添加下载 ZIP 功能并优化日志输出
- 新增 download-zip 命令,支持下载仓库的 ZIP 文件 - 优化日志输出,增加详细信息和错误处理 - 更新 README 文档
1 parent 3aa570a commit c2c6fcd

7 files changed

Lines changed: 140 additions & 66 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,6 @@ jobs:
7474
- name: Upload Artifact
7575
uses: actions/upload-artifact@v4.6.2
7676
with:
77-
name: my_script-${{ matrix.os }}
77+
name: fgit-${{ matrix.os }}
7878
path: dist/fgit_*
7979
if-no-files-found: error

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
**完全无感化**,和常规的git命令行相同,支持各种命令行参数,也就是说,平时git命令行怎么用,fgit就怎么用,区别只是git换成了fgit。
3030

3131
- **镜像加速**
32-
自动测试多个Git镜像源延迟,选择最快的源进行克隆/拉取(支持`clone`/`pull`/`push`/`fetch`)。
32+
自动测试多个Git镜像源延迟,选择最快的源进行克隆/拉取(支持`clone`/`pull`/`push`/`fetch`)。
33+
- **下载文件**
34+
支持直接下载仓库压缩包(TODO: 支持下载release文件)。
3335
- **代理支持**
3436
可通过命令行参数或配置文件设置HTTP/HTTPS代理。
3537
- **智能缓存**
@@ -45,7 +47,7 @@
4547

4648
### 直接使用
4749

48-
1. 下载编译后的可执行文件:[releases](https://github.com/NaivG/fgit/releases)
50+
1. 下载编译后的可执行文件:[releases](https://github.com/NaivG/fastgit/releases)(稳定版) / [GitHub Actions](https://github.com/NaivG/fastgit/actions)(最新版)
4951
2. 将可执行文件加入系统路径环境变量,或是移动到PATH目录下,如`C:\Windows\System32`
5052
3. 打开命令行,输入`fgit`命令,即可使用加速版git命令。
5153

@@ -82,6 +84,10 @@ fgit clone <user>/<repo> # fgit的特色方式,会自动转换为https://githu
8284
# 拉取仓库(在失败时自动选择镜像源)
8385
fgit pull
8486

87+
# 下载仓库压缩包(自动选择镜像源)
88+
fgit download-zip <仓库URL>
89+
fgit download-zip <user>/<repo>
90+
8591
# 启用代理
8692
fgit --use-proxy http://127.0.0.1:7890 clone <仓库URL>
8793
fgit --use-proxy http://127.0.0.1:7890 push
@@ -118,4 +124,4 @@ fgit --verbose clone <仓库URL>
118124

119125
Star History:
120126

121-
[![Star History Chart](https://api.star-history.com/svg?repos=NaivG/fgit&type=Date)](https://star-history.com/#NaivG/fgit&Date)
127+
[![Star History Chart](https://api.star-history.com/svg?repos=NaivG/fastgit&type=Date)](https://star-history.com/#NaivG/fastgit&Date)

config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ def save_proxy(self, proxy):
3737
self.config.set('proxy', k, v)
3838
self._save()
3939

40+
def get_downloader_config(self):
41+
if self.config.has_section('downloader'):
42+
return dict(self.config.items('downloader'))
43+
self.config.add_section('downloader')
44+
self.config.set('downloader', 'chunk_size', '1024')
45+
self.config.set('downloader', 'MIN_FILE_SIZE', '1')
46+
self._save()
47+
return dict(self.config.items('downloader'))
48+
4049
def _save(self):
4150
with open(self.config_file, 'w') as f:
4251
self.config.write(f)

downloader.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from tqdm import tqdm
2+
import requests
3+
from loguru import logger
4+
5+
def download_file(url: str, file_path: str, chunk_size: int = 1024, MIN_FILE_SIZE: int = 1) -> bool:
6+
try:
7+
response = requests.get(url, stream=True)
8+
total_size = int(response.headers.get('content-length', 0))
9+
logger.info(f"下载文件: {file_path} ({total_size/1024:.1f}KB)")
10+
if total_size < MIN_FILE_SIZE:
11+
logger.debug(f"镜像源错误: 文件大小小于{MIN_FILE_SIZE}字节")
12+
return False
13+
14+
with open(file_path, 'wb') as f:
15+
with tqdm(total=total_size, unit='B', unit_scale=True, desc="下载文件") as pbar:
16+
for data in response.iter_content(chunk_size=chunk_size):
17+
f.write(data)
18+
pbar.update(len(data))
19+
return True
20+
except Exception as e:
21+
logger.debug(f"下载失败: {str(e)}")
22+
return False

fgit.py

Lines changed: 78 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@
44
import argparse
55
import os
66
import json
7-
import traceback
87
import threading
9-
from urllib.parse import urlparse
8+
from loguru import logger
109
from urllib.request import urlopen, Request
1110
from urllib.error import HTTPError
1211
from colorama import Fore, Style, init
13-
from tempfile import NamedTemporaryFile
14-
import configparser
1512
from config import ConfigHandler
16-
from mirrors import test_latency, select_mirror, MIRRORS, convert_url
13+
from mirrors import select_mirror, convert_url
14+
from downloader import download_file
1715
from proxy import ProxyHandler
1816

1917
init(autoreset=True)
2018

19+
parser = argparse.ArgumentParser(description='Git加速工具,支持镜像源和代理')
20+
parser.add_argument('command', type=str, help='git命令, 或是fgit命令')
21+
parser.add_argument('--use-proxy', type=str, help='设置HTTP代理(格式: http://[user:pass@]host:port)')
22+
parser.add_argument('--verbose', action='store_true', help='显示详细输出')
23+
args, unknown_args = parser.parse_known_args()
24+
25+
logger.remove()
26+
logger.add(sys.stderr, level='DEBUG', colorize=True, format='{time:HH:mm:ss} | {level} | {message}') if args.verbose else logger.add(sys.stderr, level='INFO', colorize=True, format='{time:HH:mm:ss} | {level} | {message}')
27+
2128
GIT_COMMANDS_NEED_MIRROR = {'clone', 'pull', 'push', 'fetch'}
2229

2330
headers = {'User-Agent': 'Mozilla/5.0',
@@ -27,22 +34,21 @@
2734

2835
def main():
2936
config = ConfigHandler()
30-
parser = argparse.ArgumentParser(description='Git加速工具,支持镜像源和代理')
31-
parser.add_argument('command', type=str, help='git命令')
32-
parser.add_argument('--use-proxy', type=str, help='设置HTTP代理(格式: http://[user:pass@]host:port)')
33-
parser.add_argument('--verbose', action='store_true', help='显示详细输出')
34-
args, unknown_args = parser.parse_known_args()
3537

3638
proxy = ProxyHandler(args.use_proxy, config, args.verbose)
3739
env = proxy.setup_proxy_env()
3840

39-
if args.verbose:
40-
if proxy.proxy_url:
41-
print(Fore.CYAN + f"🔧 运行于代理模式" + Style.RESET_ALL)
42-
else:
43-
print(Fore.CYAN + f"🔧 运行于镜像模式" + Style.RESET_ALL)
44-
print(Fore.CYAN + f"命令参数: {' '.join(sys.argv)}" + Style.RESET_ALL)
41+
if proxy.proxy_url:
42+
logger.debug(Fore.CYAN + "🔧 运行于代理模式" + Style.RESET_ALL)
43+
else:
44+
logger.debug(Fore.CYAN + "🔧 运行于镜像模式" + Style.RESET_ALL)
45+
logger.debug(Fore.CYAN + f"命令参数: {' '.join(sys.argv)}" + Style.RESET_ALL)
4546

47+
48+
if args.command == 'download-zip':
49+
handle_download_zip(args, unknown_args, config, env, args.verbose)
50+
return
51+
4652
if args.command not in GIT_COMMANDS_NEED_MIRROR:
4753
subprocess.run(['git'] + sys.argv[1:], env=env)
4854
return
@@ -55,6 +61,41 @@ def main():
5561
finally:
5662
proxy.restore_proxy_settings()
5763

64+
def handle_download_zip(args, unknown_args, config, env, verbose):
65+
downloader_config = config.get_downloader_config()
66+
if not downloader_config:
67+
logger.warning(Fore.YELLOW + "🧐 下载配置不存在, 使用默认配置" + Style.RESET_ALL)
68+
chunk_size = downloader_config.get('chunk_size', 1024)
69+
MIN_FILE_SIZE = downloader_config.get('min_file_size', 1)
70+
71+
original_url = unknown_args[0]
72+
if '://' not in original_url and '/' in original_url:
73+
if '@' in original_url: # SSH格式
74+
original_url = f"https://{original_url.split('@')[1].replace(':', '/', 1)}"
75+
else: # 简写格式
76+
original_url = f"https://github.com/{original_url}"
77+
78+
original_url = original_url.split('.git')[0]
79+
repo_name = original_url.split('/')[-1].split('.git')[0]
80+
if os.path.exists(os.path.join(os.getcwd(), repo_name + '.zip')):
81+
logger.warning(Fore.YELLOW + f"😪 压缩包 {repo_name}.zip 已存在" + Style.RESET_ALL)
82+
return
83+
84+
repo_status = get_repo(original_url)
85+
if repo_status is None:
86+
logger.warning(Fore.YELLOW + "🧐 无法获取到仓库信息, 尝试下载" + Style.RESET_ALL)
87+
elif repo_status is False and not input_with_timeout(Fore.YELLOW + "🧐 仓库可能不存在,5秒内按任意键忽略..." + Style.RESET_ALL, 5):
88+
return
89+
90+
mirror_list = select_mirror(config, args.verbose)
91+
for mirror in mirror_list:
92+
new_url = convert_url(original_url, mirror) + '/archive/refs/heads/main.zip'
93+
logger.info(Fore.GREEN + f"🔄 尝试镜像源 {mirror} [{mirror_list.index(mirror) + 1}/{len(mirror_list)}]: {new_url}" + Style.RESET_ALL)
94+
if download_file(new_url, os.path.join(os.getcwd(), repo_name + '.zip'), chunk_size=chunk_size, MIN_FILE_SIZE=MIN_FILE_SIZE):
95+
return
96+
logger.error(Fore.RED + "❌ 所有镜像源尝试失败" + Style.RESET_ALL)
97+
return
98+
5899
def handle_clone(args, unknown_args, config, env, verbose, proxy):
59100
original_url = unknown_args[0]
60101
if '://' not in original_url and '/' in original_url:
@@ -65,12 +106,12 @@ def handle_clone(args, unknown_args, config, env, verbose, proxy):
65106

66107
repo_name = original_url.split('/')[-1].split('.git')[0]
67108
if os.path.exists(os.path.join(os.getcwd(), repo_name)):
68-
print(Fore.YELLOW + f"😪 仓库 {repo_name} 已存在" + Style.RESET_ALL)
109+
logger.warning(Fore.YELLOW + f"😪 仓库 {repo_name} 已存在" + Style.RESET_ALL)
69110
return
70111

71-
repo_status = get_repo(original_url, verbose)
112+
repo_status = get_repo(original_url)
72113
if repo_status is None:
73-
print(Fore.YELLOW + "🧐 无法获取到仓库信息, 尝试克隆" + Style.RESET_ALL)
114+
logger.warning(Fore.YELLOW + "🧐 无法获取到仓库信息, 尝试克隆" + Style.RESET_ALL)
74115
elif repo_status is False and not input_with_timeout(Fore.YELLOW + "🧐 仓库可能不存在,5秒内按任意键忽略..." + Style.RESET_ALL, 5):
75116
return
76117

@@ -80,29 +121,29 @@ def handle_clone(args, unknown_args, config, env, verbose, proxy):
80121
if result.returncode == 0:
81122
return
82123
else:
83-
print(Fore.RED + "❌ 在代理模式下克隆失败, 尝试使用镜像模式..." + Style.RESET_ALL)
124+
logger.error(Fore.RED + "❌ 在代理模式下克隆失败, 尝试使用镜像模式..." + Style.RESET_ALL)
84125
mirror_list = select_mirror(config, verbose)
85126
for mirror in mirror_list:
86127
new_url = convert_url(original_url, mirror)
87-
print(Fore.GREEN + f"🔄 尝试镜像源 {mirror} [{mirror_list.index(mirror) + 1}/{len(mirror_list)}]: {new_url}" + Style.RESET_ALL)
128+
logger.info(Fore.GREEN + f"🔄 尝试镜像源 {mirror} [{mirror_list.index(mirror) + 1}/{len(mirror_list)}]: {new_url}" + Style.RESET_ALL)
88129
cmd = ['git', 'clone', new_url] + unknown_args[1:]
89130
result = subprocess.run(cmd, env=env, check=False)
90131
if result.returncode == 0:
91132
repo_path = os.path.join(os.getcwd(), repo_name)
92133
subprocess.run(['git', '-C', repo_path, 'remote', 'set-url', 'origin', original_url], check=True) # 还原原始 URL
93134
return
94-
print(Fore.RED + "❌ 所有镜像源尝试失败" + Style.RESET_ALL)
135+
logger.error(Fore.RED + "❌ 所有镜像源尝试失败" + Style.RESET_ALL)
95136

96137
def handle_other_commands(args, unknown_args, config, env, verbose, proxy):
97138
if not os.path.exists(os.path.join(os.getcwd(), '.git')):
98-
print(Fore.YELLOW + "❌ 当前目录不是有效的 Git 仓库" + Style.RESET_ALL)
139+
logger.warning(Fore.YELLOW + "❌ 当前目录不是有效的 Git 仓库" + Style.RESET_ALL)
99140
return
100141
git_args = [args.command] + unknown_args
101142
result = subprocess.run(['git'] + git_args, env=env, check=False)
102143
if result.returncode == 0:
103144
return
104145
elif proxy.proxy_url:
105-
print(Fore.RED + "❌ 在代理模式下运行失败, 尝试使用镜像模式..." + Style.RESET_ALL)
146+
logger.error(Fore.RED + "❌ 在代理模式下运行失败, 尝试使用镜像模式..." + Style.RESET_ALL)
106147

107148
mirror_list = select_mirror(config, verbose)
108149
for mirror in mirror_list:
@@ -113,43 +154,38 @@ def handle_other_commands(args, unknown_args, config, env, verbose, proxy):
113154
return
114155
finally:
115156
restore_git_config()
116-
print(Fore.RED + "❌ 所有镜像源尝试失败" + Style.RESET_ALL)
157+
logger.error(Fore.RED + "❌ 所有镜像源尝试失败" + Style.RESET_ALL)
117158

118-
def get_repo(repo: str, verbose: bool = False) -> bool | None:
159+
def get_repo(repo: str) -> bool | None:
119160
if repo.endswith('.git'):
120161
repo = repo[:-4]
121162
if '://' in repo and '/' in repo:
122163
repo = repo.split('/')[-2] + '/' + repo.split('/')[-1] # 处理 URL 形式的仓库名
123-
if verbose:
124-
print(Fore.CYAN + f"🔍 正在获取仓库: {repo}" + Style.RESET_ALL)
164+
logger.debug(Fore.CYAN + f"🔍 正在获取仓库: {repo}" + Style.RESET_ALL)
125165
url = f"https://api.github.com/repos/{repo}"
126166
req = Request(url, headers=headers)
127167
try:
128168
with urlopen(req) as response:
129169
if response.status == 200:
130170
result = json.loads(response.read().decode())
131171
# return [result['full_name'], result['id']]
132-
print(Fore.GREEN + f"✅ 获取到仓库信息: {result['full_name']}({result['id']})" + Style.RESET_ALL)
172+
logger.info(Fore.GREEN + f"✅ 获取到仓库信息: {result['full_name']}({result['id']})" + Style.RESET_ALL)
133173
return True
134174
elif response.status == 404:
135-
print(Fore.RED + f"❌ 获取仓库信息失败,该仓库可能不存在或未公开" + Style.RESET_ALL)
175+
logger.warning(Fore.RED + "❌ 获取仓库信息失败,该仓库可能不存在或未公开" + Style.RESET_ALL)
136176
return False
137177
else:
138-
if verbose:
139-
print(Fore.RED + f"❌ 获取仓库信息失败: {response.status} {response.reason}" + Style.RESET_ALL)
178+
logger.debug(Fore.RED + f"❌ 获取仓库信息失败: {response.status} {response.reason}" + Style.RESET_ALL)
140179
return None
141180
except HTTPError as e:
142181
if e.code == 404:
143-
print(Fore.RED + f"❌ 获取仓库信息失败,该仓库可能不存在或未公开" + Style.RESET_ALL)
182+
logger.warning(Fore.RED + "❌ 获取仓库信息失败,该仓库可能不存在或未公开" + Style.RESET_ALL)
144183
return False
145184
else:
146-
if verbose:
147-
print(Fore.RED + f"❌ 获取仓库信息失败: {e}" + Style.RESET_ALL)
185+
logger.debug(Fore.RED + f"❌ 获取仓库信息失败: {e}" + Style.RESET_ALL)
148186
return None
149187
except Exception as e:
150-
if verbose:
151-
print(Fore.RED + f"❌ 获取仓库信息失败: {e}" + Style.RESET_ALL)
152-
traceback.print_exc()
188+
logger.debug(Fore.RED + f"❌ 获取仓库信息失败: {e}" + Style.RESET_ALL)
153189
return None
154190

155191
def modify_git_config(mirror):
@@ -159,7 +195,7 @@ def restore_git_config():
159195
subprocess.run(['git', 'config', '--local', '--unset', 'url.https://github.com/.insteadOf'])
160196

161197
def input_with_timeout(prompt, timeout):
162-
print(prompt)
198+
logger.info(prompt)
163199
result = []
164200
thread = threading.Thread(target=lambda: result.append(sys.stdin.read(1)))
165201
thread.daemon = True
@@ -169,13 +205,12 @@ def input_with_timeout(prompt, timeout):
169205

170206
if __name__ == '__main__':
171207
try:
172-
print(Fore.GREEN + f"fastgit🚀 by NaivG" + Style.RESET_ALL)
208+
print(Fore.GREEN + "fastgit🚀 by NaivG" + Style.RESET_ALL)
173209
main()
174210
except KeyboardInterrupt:
175-
print(Fore.YELLOW + "❗ 操作已取消" + Style.RESET_ALL)
211+
logger.warning(Fore.YELLOW + "❗ 操作已取消" + Style.RESET_ALL)
176212
except Exception as e:
177-
print(Fore.RED + f"❌ 错误: {str(e)}" + Style.RESET_ALL)
178-
traceback.print_exc()
213+
logger.exception(Fore.RED + f"❌ 错误: {str(e)}" + Style.RESET_ALL)
179214
sys.exit(1)
180215
finally:
181216
sys.exit(0)

mirrors.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from loguru import logger
12
from ping3 import ping
23
from concurrent.futures import ThreadPoolExecutor
34
from prettytable import PrettyTable
@@ -19,24 +20,27 @@
1920
'gh-deno': 'https://gh-deno.mocn.top/https://github.com'
2021
}
2122

23+
RAWCONTENT_MIRRORS = {
24+
'github': 'https://raw.githubusercontent.com',
25+
'ghproxy.net': 'https://ghproxy.net/https://raw.githubusercontent.com',
26+
}
27+
2228
def test_latency(verbose=False):
23-
if verbose:
24-
print(Fore.CYAN + "🔎 测试镜像源延迟..." + Style.RESET_ALL)
25-
table = PrettyTable()
26-
table.field_names = ['Git 镜像源', 'Latency 延迟']
29+
logger.debug(Fore.CYAN + "🔎 测试镜像源延迟..." + Style.RESET_ALL)
30+
table = PrettyTable()
31+
table.field_names = ['Git 镜像源', 'Latency 延迟']
2732

2833
results = {}
2934
with ThreadPoolExecutor() as executor:
3035
futures = {executor.submit(test_single, name, url): name for name, url in MIRRORS.items()}
3136
for future in futures:
3237
name, latency = future.result()
3338
results[name] = latency
34-
if verbose:
35-
if latency is not None and latency != 0.0:
36-
color = Fore.GREEN if latency < 200 else Fore.YELLOW if latency < 500 else Fore.RED
37-
table.add_row([name, color + f"{latency:.1f}ms" + Style.RESET_ALL])
38-
else:
39-
table.add_row([name, Fore.RED + "超时" + Style.RESET_ALL])
39+
if latency is not None and latency != 0.0:
40+
color = Fore.GREEN if latency < 200 else Fore.YELLOW if latency < 500 else Fore.RED
41+
table.add_row([name, color + f"{latency:.1f}ms" + Style.RESET_ALL])
42+
else:
43+
table.add_row([name, Fore.RED + "超时" + Style.RESET_ALL])
4044

4145
if verbose:
4246
print(table)
@@ -53,13 +57,11 @@ def test_single(name, url):
5357

5458
def select_mirror(config, verbose=False):
5559
if cached := config.get_mirrors():
56-
if verbose:
57-
print(Fore.CYAN + f"✔️ 已选择 {cached}(缓存) 作为 Git 镜像源" + Style.RESET_ALL)
60+
logger.debug(Fore.CYAN + f"✔️ 已选择 {cached}(缓存) 作为 Git 镜像源" + Style.RESET_ALL)
5861
return cached
5962
mirrors = test_latency(verbose)
6063
config.save_mirrors(mirrors)
61-
if verbose:
62-
print(Fore.CYAN + f"✔️ 已选择 {mirrors} 作为 Git 镜像源" + Style.RESET_ALL)
64+
logger.debug(Fore.CYAN + f"✔️ 已选择 {mirrors} 作为 Git 镜像源" + Style.RESET_ALL)
6365
return mirrors
6466

6567
def convert_url(url, mirror):

0 commit comments

Comments
 (0)