99from urllib .request import urlopen , Request
1010from urllib .error import HTTPError
1111from 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
1717init (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+
1927parser = argparse .ArgumentParser (description = 'Git加速工具,支持镜像源和代理' )
2028parser .add_argument ('command' , type = str , help = 'git命令, 或是fgit命令' )
2129parser .add_argument ('--use-proxy' , type = str , help = '设置HTTP代理(格式: http://[user:pass@]host:port)' )
2230parser .add_argument ('--branch' , type = str , help = '分支名(仅在download命令时有效)' , default = 'main' )
23-
2431parser .add_argument ('--verbose' , action = 'store_true' , help = '显示详细输出' )
32+
2533args , unknown_args = parser .parse_known_args ()
2634
35+ # 配置日志
2736logger .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
3743def 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+
6684def 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
101120def 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+
139178def 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+
193264def modify_git_config (mirror ):
265+ """修改本地Git配置以使用镜像源"""
194266 subprocess .run (['git' , 'config' , '--local' , 'url.https://github.com/.insteadOf' , f'https://{ mirror } ' ])
195267
268+
196269def restore_git_config ():
270+ """恢复本地Git配置"""
197271 subprocess .run (['git' , 'config' , '--local' , '--unset' , 'url.https://github.com/.insteadOf' ])
198272
273+
199274def 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+
208294if __name__ == '__main__' :
209295 try :
210296 print (Fore .GREEN + "fastgit🚀 by NaivG" + Style .RESET_ALL )
0 commit comments