Skip to content

Commit caad32f

Browse files
committed
refactor(utils): 重构工具模块结构和功能
- 标准化 URL 处理函数 - 优化命令行参数解析和日志配置 - 克隆命令支持自定义 remote 名称和自定义路径
1 parent 19c1507 commit caad32f

8 files changed

Lines changed: 274 additions & 58 deletions

File tree

.gitignore

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,13 @@ fgit.build/
178178
fgit.dist/
179179
fgit.onefile-build/
180180
.exe
181-
.bak
181+
.bak
182+
183+
# IDE
184+
.project/
185+
.classpath/
186+
.settings/
187+
.factorypath/
188+
.vscode/
189+
.idea/
190+
.gradle/

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@
5959
pip install -r requirements.txt
6060
```
6161

62-
1. 克隆项目
62+
2. 克隆项目
6363

6464
```bash
6565
git clone https://github.com/NaivG/fgit.git
6666
```
6767

68-
1. 进入项目目录,使用python库编译可执行文件
68+
3. 进入项目目录,使用python库编译可执行文件
6969

7070
**本项目选择nuitka编译,若需复现则编译前请先安装Visual C++ Build Tools**
7171

fgit.py

Lines changed: 130 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,52 +9,69 @@
99
from urllib.request import urlopen, Request
1010
from urllib.error import HTTPError
1111
from colorama import Fore, Style, init
12-
from config import ConfigHandler
13-
from mirrors import select_mirror, convert_url
14-
from downloader import download_file
15-
from proxy import ProxyHandler
12+
from utils.config import ConfigHandler
13+
from utils.mirrors import select_mirror, convert_url
14+
from utils.downloader import download_file
15+
from utils.proxy import ProxyHandler
1616

1717
init(autoreset=True)
1818

19+
# 定义常量
20+
GIT_COMMANDS_NEED_MIRROR = {'clone', 'pull', 'push', 'fetch'}
21+
HEADERS = {
22+
'User-Agent': 'Mozilla/5.0',
23+
'Content-Type': 'application/json',
24+
'Accept': 'application/json'
25+
}
26+
1927
parser = argparse.ArgumentParser(description='Git加速工具,支持镜像源和代理')
2028
parser.add_argument('command', type=str, help='git命令, 或是fgit命令')
2129
parser.add_argument('--use-proxy', type=str, help='设置HTTP代理(格式: http://[user:pass@]host:port)')
2230
parser.add_argument('--branch', type=str, help='分支名(仅在download命令时有效)', default='main')
23-
2431
parser.add_argument('--verbose', action='store_true', help='显示详细输出')
32+
2533
args, unknown_args = parser.parse_known_args()
2634

35+
# 配置日志
2736
logger.remove()
28-
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}')
37+
if args.verbose:
38+
logger.add(sys.stderr, level='DEBUG', colorize=True, format='{time:HH:mm:ss} | {level} | {message}')
39+
else:
40+
logger.add(sys.stderr, level='INFO', colorize=True, format='{time:HH:mm:ss} | {level} | {message}')
2941

30-
GIT_COMMANDS_NEED_MIRROR = {'clone', 'pull', 'push', 'fetch'}
31-
32-
headers = {'User-Agent': 'Mozilla/5.0',
33-
'Content-Type': 'application/json',
34-
'Accept': 'application/json'
35-
}
3642

3743
def main():
44+
"""主函数"""
3845
config = ConfigHandler()
39-
4046
proxy = ProxyHandler(args.use_proxy, config, args.verbose)
4147
env = proxy.setup_proxy_env()
4248

49+
# 显示运行模式
4350
if proxy.proxy_url:
4451
logger.debug(Fore.CYAN + "🔧 运行于代理模式" + Style.RESET_ALL)
4552
else:
4653
logger.debug(Fore.CYAN + "🔧 运行于镜像模式" + Style.RESET_ALL)
54+
4755
logger.debug(Fore.CYAN + f"命令参数: {' '.join(sys.argv)}" + Style.RESET_ALL)
4856

57+
if not args.command or len(sys.argv) <= 2:
58+
logger.error(Fore.RED + "❌ 缺少必要参数" + Style.RESET_ALL)
59+
logger.error(' '.join(sys.argv))
60+
logger.error(" ^^")
61+
logger.info(Fore.CYAN + "📖 使用帮助: fgit -h" + Style.RESET_ALL)
62+
return
4963

64+
# 处理 download 命令
5065
if args.command == 'download':
5166
handle_download_zip(args, unknown_args, config, env, args.verbose)
5267
return
5368

69+
# 处理不需要镜像的 Git 命令
5470
if args.command not in GIT_COMMANDS_NEED_MIRROR:
5571
subprocess.run(['git'] + sys.argv[1:], env=env)
5672
return
5773

74+
# 处理需要镜像的 Git 命令
5875
try:
5976
if args.command == 'clone':
6077
handle_clone(args, unknown_args, config, env, args.verbose, proxy)
@@ -63,24 +80,25 @@ def main():
6380
finally:
6481
proxy.restore_proxy_settings()
6582

83+
6684
def handle_download_zip(args, unknown_args, config, env, verbose):
85+
"""处理下载zip文件命令"""
6786
downloader_config = config.get_downloader_config()
6887
if not downloader_config:
6988
logger.warning(Fore.YELLOW + "🧐 下载配置不存在, 使用默认配置" + Style.RESET_ALL)
89+
7090
chunk_size = downloader_config.get('chunk_size', 1024)
71-
MIN_FILE_SIZE = downloader_config.get('min_file_size', 100)
91+
min_file_size = downloader_config.get('min_file_size', 100)
7292

7393
original_url = unknown_args[0]
74-
if '://' not in original_url and '/' in original_url:
75-
if '@' in original_url: # SSH格式
76-
original_url = f"https://{original_url.split('@')[1].replace(':', '/', 1)}"
77-
else: # 简写格式
78-
original_url = f"https://github.com/{original_url}"
94+
original_url = normalize_repo_url(original_url)
7995

80-
original_url = original_url.split('.git')[0]
8196
repo_name = original_url.split('/')[-1].split('.git')[0]
82-
if os.path.exists(os.path.join(os.getcwd(), repo_name + '.zip')):
83-
logger.warning(Fore.YELLOW + f"😪 压缩包 {repo_name}.zip 已存在" + Style.RESET_ALL)
97+
zip_filename = f"{repo_name}-{args.branch}.zip"
98+
zip_filepath = os.path.join(os.getcwd(), zip_filename)
99+
100+
if os.path.exists(zip_filepath):
101+
logger.warning(Fore.YELLOW + f"😪 压缩包 {zip_filename} 已存在" + Style.RESET_ALL)
84102
return
85103

86104
repo_status = get_repo(original_url)
@@ -89,25 +107,41 @@ def handle_download_zip(args, unknown_args, config, env, verbose):
89107
elif repo_status is False and not input_with_timeout(Fore.YELLOW + "🧐 仓库可能不存在,5秒内按任意键忽略..." + Style.RESET_ALL, 5):
90108
return
91109

92-
mirror_list = select_mirror(config, args.verbose)
110+
mirror_list = select_mirror(config, verbose)
93111
for mirror in mirror_list:
94112
new_url = convert_url(original_url, mirror) + f'/archive/refs/heads/{args.branch}.zip'
95113
logger.info(Fore.GREEN + f"🔄 尝试镜像源 {mirror} [{mirror_list.index(mirror) + 1}/{len(mirror_list)}]: {new_url}" + Style.RESET_ALL)
96-
if download_file(new_url, os.path.join(os.getcwd(), f'{repo_name}-{args.branch}.zip'), chunk_size=chunk_size, MIN_FILE_SIZE=MIN_FILE_SIZE):
114+
if download_file(new_url, zip_filepath, chunk_size=chunk_size, MIN_FILE_SIZE=min_file_size):
97115
return
116+
98117
logger.error(Fore.RED + "❌ 所有镜像源尝试失败" + Style.RESET_ALL)
99-
return
118+
100119

101120
def handle_clone(args, unknown_args, config, env, verbose, proxy):
121+
"""处理克隆命令"""
102122
original_url = unknown_args[0]
103-
if '://' not in original_url and '/' in original_url:
104-
if '@' in original_url: # SSH格式
105-
original_url = f"https://{original_url.split('@')[1].replace(':', '/', 1)}"
106-
else: # 简写格式
107-
original_url = f"https://github.com/{original_url}"
123+
original_url = normalize_repo_url(original_url)
108124

109-
repo_name = original_url.split('/')[-1].split('.git')[0]
110-
if os.path.exists(os.path.join(os.getcwd(), repo_name)):
125+
# 查找是否指定了自定义 remote 名称
126+
remote_name = 'origin' # 默认 remote 名称
127+
custom_remote_index = None
128+
for i, arg in enumerate(unknown_args):
129+
if arg in ['-o', '--origin'] and i + 1 < len(unknown_args):
130+
remote_name = unknown_args[i + 1]
131+
custom_remote_index = i
132+
break
133+
134+
# 获取仓库路径
135+
if len(unknown_args) >= 2 and not unknown_args[-2].startswith('-') and not unknown_args[-1].startswith('-'):
136+
# 用户指定了目标目录
137+
repo_path = os.path.join(os.getcwd(), unknown_args[-1])
138+
repo_name = unknown_args[-1]
139+
else:
140+
# 使用默认仓库名
141+
repo_name = original_url.split('/')[-1].split('.git')[0]
142+
repo_path = os.path.join(os.getcwd(), repo_name)
143+
144+
if os.path.exists(repo_path):
111145
logger.warning(Fore.YELLOW + f"😪 仓库 {repo_name} 已存在" + Style.RESET_ALL)
112146
return
113147

@@ -117,36 +151,47 @@ def handle_clone(args, unknown_args, config, env, verbose, proxy):
117151
elif repo_status is False and not input_with_timeout(Fore.YELLOW + "🧐 仓库可能不存在,5秒内按任意键忽略..." + Style.RESET_ALL, 5):
118152
return
119153

120-
if proxy.proxy_url: # 代理模式
154+
# 如果设置了代理,则优先使用代理模式
155+
if proxy.proxy_url:
121156
cmd = ['git', 'clone', original_url] + unknown_args[1:]
122157
result = subprocess.run(cmd, env=env, check=False)
123158
if result.returncode == 0:
124159
return
125160
else:
126161
logger.error(Fore.RED + "❌ 在代理模式下克隆失败, 尝试使用镜像模式..." + Style.RESET_ALL)
162+
163+
# 使用镜像源尝试克隆
127164
mirror_list = select_mirror(config, verbose)
128165
for mirror in mirror_list:
129166
new_url = convert_url(original_url, mirror)
130167
logger.info(Fore.GREEN + f"🔄 尝试镜像源 {mirror} [{mirror_list.index(mirror) + 1}/{len(mirror_list)}]: {new_url}" + Style.RESET_ALL)
131168
cmd = ['git', 'clone', new_url] + unknown_args[1:]
132169
result = subprocess.run(cmd, env=env, check=False)
133170
if result.returncode == 0:
134-
repo_path = os.path.join(os.getcwd(), repo_name)
135-
subprocess.run(['git', '-C', repo_path, 'remote', 'set-url', 'origin', original_url], check=True) # 还原原始 URL
171+
# 克隆成功后,将远程仓库地址还原为原始地址
172+
subprocess.run(['git', '-C', repo_path, 'remote', 'set-url', remote_name, original_url], check=True)
136173
return
174+
137175
logger.error(Fore.RED + "❌ 所有镜像源尝试失败" + Style.RESET_ALL)
138176

177+
139178
def handle_other_commands(args, unknown_args, config, env, verbose, proxy):
179+
"""处理其他Git命令 (pull, push, fetch等)"""
140180
if not os.path.exists(os.path.join(os.getcwd(), '.git')):
141181
logger.warning(Fore.YELLOW + "❌ 当前目录不是有效的 Git 仓库" + Style.RESET_ALL)
142182
return
183+
143184
git_args = [args.command] + unknown_args
144185
result = subprocess.run(['git'] + git_args, env=env, check=False)
186+
187+
# 如果命令执行成功,直接返回
145188
if result.returncode == 0:
146189
return
190+
# 如果使用代理但执行失败,则尝试镜像模式
147191
elif proxy.proxy_url:
148192
logger.error(Fore.RED + "❌ 在代理模式下运行失败, 尝试使用镜像模式..." + Style.RESET_ALL)
149193

194+
# 使用镜像源尝试执行命令
150195
mirror_list = select_mirror(config, verbose)
151196
for mirror in mirror_list:
152197
modify_git_config(mirror)
@@ -156,21 +201,44 @@ def handle_other_commands(args, unknown_args, config, env, verbose, proxy):
156201
return
157202
finally:
158203
restore_git_config()
204+
159205
logger.error(Fore.RED + "❌ 所有镜像源尝试失败" + Style.RESET_ALL)
160206

161-
def get_repo(repo: str) -> bool | None:
162-
if repo.endswith('.git'):
163-
repo = repo[:-4]
164-
if '://' in repo and '/' in repo:
165-
repo = repo.split('/')[-2] + '/' + repo.split('/')[-1] # 处理 URL 形式的仓库名
166-
logger.debug(Fore.CYAN + f"🔍 正在获取仓库: {repo}" + Style.RESET_ALL)
167-
url = f"https://api.github.com/repos/{repo}"
168-
req = Request(url, headers=headers)
207+
208+
def normalize_repo_url(url):
209+
"""标准化仓库URL格式"""
210+
if '://' not in url and '/' in url:
211+
if '@' in url: # SSH格式
212+
url = f"https://{url.split('@')[1].replace(':', '/', 1)}"
213+
else: # 简写格式 (如 user/repo)
214+
url = f"https://github.com/{url}"
215+
return url.split('.git')[0]
216+
217+
218+
def get_repo(repo_url):
219+
"""
220+
获取仓库信息
221+
222+
Args:
223+
repo_url (str): 仓库URL
224+
225+
Returns:
226+
bool or None: True表示仓库存在,False表示仓库不存在,None表示获取信息失败
227+
"""
228+
clean_url = repo_url.split('.git')[0] if repo_url.endswith('.git') else repo_url
229+
230+
# 处理 URL 形式的仓库名
231+
if '://' in clean_url and '/' in clean_url:
232+
clean_url = clean_url.split('/')[-2] + '/' + clean_url.split('/')[-1]
233+
234+
logger.debug(Fore.CYAN + f"🔍 正在获取仓库: {clean_url}" + Style.RESET_ALL)
235+
api_url = f"https://api.github.com/repos/{clean_url}"
236+
req = Request(api_url, headers=HEADERS)
237+
169238
try:
170239
with urlopen(req) as response:
171240
if response.status == 200:
172241
result = json.loads(response.read().decode())
173-
# return [result['full_name'], result['id']]
174242
logger.info(Fore.GREEN + f"✅ 获取到仓库信息: {result['full_name']}({result['id']})" + Style.RESET_ALL)
175243
return True
176244
elif response.status == 404:
@@ -179,24 +247,41 @@ def get_repo(repo: str) -> bool | None:
179247
else:
180248
logger.debug(Fore.RED + f"❌ 获取仓库信息失败: {response.status} {response.reason}" + Style.RESET_ALL)
181249
return None
250+
182251
except HTTPError as e:
183252
if e.code == 404:
184253
logger.warning(Fore.RED + "❌ 获取仓库信息失败,该仓库可能不存在或未公开" + Style.RESET_ALL)
185254
return False
186255
else:
187256
logger.debug(Fore.RED + f"❌ 获取仓库信息失败: {e}" + Style.RESET_ALL)
188257
return None
258+
189259
except Exception as e:
190260
logger.debug(Fore.RED + f"❌ 获取仓库信息失败: {e}" + Style.RESET_ALL)
191261
return None
192262

263+
193264
def modify_git_config(mirror):
265+
"""修改本地Git配置以使用镜像源"""
194266
subprocess.run(['git', 'config', '--local', 'url.https://github.com/.insteadOf', f'https://{mirror}'])
195267

268+
196269
def restore_git_config():
270+
"""恢复本地Git配置"""
197271
subprocess.run(['git', 'config', '--local', '--unset', 'url.https://github.com/.insteadOf'])
198272

273+
199274
def input_with_timeout(prompt, timeout):
275+
"""
276+
带超时的输入函数
277+
278+
Args:
279+
prompt (str): 提示信息
280+
timeout (int): 超时时间(秒)
281+
282+
Returns:
283+
bool: 用户是否在超时前输入了内容
284+
"""
200285
logger.info(prompt)
201286
result = []
202287
thread = threading.Thread(target=lambda: result.append(sys.stdin.read(1)))
@@ -205,6 +290,7 @@ def input_with_timeout(prompt, timeout):
205290
thread.join(timeout)
206291
return bool(result)
207292

293+
208294
if __name__ == '__main__':
209295
try:
210296
print(Fore.GREEN + "fastgit🚀 by NaivG" + Style.RESET_ALL)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ colorama
33
prettytable
44
loguru
55
requests
6-
tqdm
6+
tqdm

0 commit comments

Comments
 (0)