atomgit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
atomgit/cli.py ADDED
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import click
6
+ import sys
7
+ from pathlib import Path
8
+ from getpass import getpass
9
+
10
+ # 设置Hugging Face Hub的API端点为AtomGit
11
+ os.environ["HF_ENDPOINT"] = "https://hub.atomgit.com"
12
+ # 设置缓存目录
13
+ cache_dir = os.path.expanduser("~/.cache/atomgit")
14
+ os.makedirs(cache_dir, exist_ok=True)
15
+ os.environ["HF_HOME"] = cache_dir
16
+
17
+ try:
18
+ from .config import config
19
+ from .api import api
20
+ from .utils import (
21
+ print_success, print_error, print_warning, print_info,
22
+ validate_repo_name, validate_repo_type, get_directory_size,
23
+ format_file_size, count_files_in_directory, confirm_action,
24
+ is_valid_path, ensure_directory, setup_git_credentials,
25
+ clear_git_credentials, check_git_available
26
+ )
27
+ except ImportError:
28
+ from config import config
29
+ from api import api
30
+ from utils import (
31
+ print_success, print_error, print_warning, print_info,
32
+ validate_repo_name, validate_repo_type, get_directory_size,
33
+ format_file_size, count_files_in_directory, confirm_action,
34
+ is_valid_path, ensure_directory, setup_git_credentials,
35
+ clear_git_credentials, check_git_available
36
+ )
37
+
38
+
39
+ @click.group()
40
+ @click.version_option(version='1.0.0')
41
+ def cli():
42
+ """AtomGit CLI - 基于Transformers和Hugging Face Hub的AtomGit平台模型文件上传下载工具"""
43
+ pass
44
+
45
+
46
+ @cli.command()
47
+ @click.option('--token', '-t', help='AtomGit访问令牌')
48
+ def login(token):
49
+ """登录到AtomGit平台"""
50
+ if not token:
51
+ token = getpass('请输入访问令牌: ')
52
+
53
+ if not token:
54
+ print_error("令牌不能为空")
55
+ sys.exit(1)
56
+
57
+ print_info("正在验证登录信息...")
58
+
59
+ if api.login(token):
60
+ print_success(f"登录成功!")
61
+
62
+ # 配置Git凭证助手
63
+ if check_git_available():
64
+ if setup_git_credentials(token):
65
+ print_info("✨ Git凭证已配置,现在可以直接使用git命令访问AtomGit仓库")
66
+ else:
67
+ print_warning("Git凭证配置失败,请手动配置或使用完整URL")
68
+ else:
69
+ print_warning("未检测到Git,跳过Git凭证配置")
70
+ else:
71
+ print_error("登录失败,请检查令牌是否正确")
72
+ sys.exit(1)
73
+
74
+
75
+ @cli.command()
76
+ def logout():
77
+ """退出登录"""
78
+ if config.is_logged_in():
79
+ config.clear_credentials()
80
+
81
+ # 清除Git凭证配置
82
+ if check_git_available():
83
+ clear_git_credentials()
84
+
85
+ print_success("已退出登录")
86
+ else:
87
+ print_info("当前未登录")
88
+
89
+
90
+ @cli.command()
91
+ def whoami():
92
+ if not config.is_logged_in():
93
+ print_warning("请先登录:atomgit login")
94
+ sys.exit(1)
95
+ # 调用API获取用户信息
96
+ user_info = api.get_login_user()
97
+ if user_info:
98
+ print_success(f"当前登录用户: {user_info['login']}")
99
+ else:
100
+ print_error("获取用户信息失败")
101
+ sys.exit(1)
102
+
103
+
104
+ @cli.group()
105
+ def repo():
106
+ """仓库管理命令"""
107
+ pass
108
+
109
+
110
+ @repo.command()
111
+ @click.argument('repo_name')
112
+ @click.option('--type', 'repo_type', type=click.Choice(['model', 'dataset']),
113
+ required=True, help='仓库类型 (model/dataset)')
114
+ @click.option('--private', is_flag=True, help='创建私有仓库')
115
+ def create(repo_name, repo_type, private):
116
+ """创建新仓库"""
117
+ if not config.is_logged_in():
118
+ print_error("请先登录:atomgit login")
119
+ sys.exit(1)
120
+
121
+ if not validate_repo_name(repo_name):
122
+ print_error("仓库名称格式不正确,应为: username/repo-name")
123
+ sys.exit(1)
124
+
125
+ print_info(f"正在创建{repo_type}仓库: {repo_name}")
126
+ if api.create_repo(repo_name, repo_type, private):
127
+ print_success(f"仓库 {repo_name} 创建成功")
128
+ else:
129
+ print_error(f"仓库 {repo_name} 创建失败")
130
+ sys.exit(1)
131
+
132
+
133
+ # @repo.command()
134
+ # @click.argument('repo_id')
135
+ # def info(repo_id):
136
+ # """显示仓库信息"""
137
+ # if not config.is_logged_in():
138
+ # print_error("请先登录:atomgit login")
139
+ # sys.exit(1)
140
+
141
+ # if not validate_repo_name(repo_id):
142
+ # print_error("仓库ID格式不正确,应为: username/repo-name")
143
+ # sys.exit(1)
144
+
145
+ # repo_info = api.get_repo_info(repo_id)
146
+ # if repo_info:
147
+ # print_info(f"仓库名称: {repo_info.get('name', '未知')}")
148
+ # print_info(f"仓库类型: {repo_info.get('type', '未知')}")
149
+ # print_info(f"描述: {repo_info.get('description', '无')}")
150
+ # print_info(f"是否私有: {'是' if repo_info.get('private', False) else '否'}")
151
+ # print_info(f"创建时间: {repo_info.get('created_at', '未知')}")
152
+ # else:
153
+ # print_error(f"无法获取仓库 {repo_id} 的信息")
154
+ # sys.exit(1)
155
+
156
+
157
+ @cli.command()
158
+ @click.argument('path', type=click.Path(exists=True))
159
+ @click.option('--repo-id', required=True, help='目标仓库ID (username/repo-name)')
160
+ @click.option('--message', '-m', default='', help='上传说明')
161
+ def upload(path, repo_id, message):
162
+ """上传文件或目录到仓库"""
163
+ if not config.is_logged_in():
164
+ print_error("请先登录:atomgit login")
165
+ sys.exit(1)
166
+
167
+ if not validate_repo_name(repo_id):
168
+ print_error("仓库ID格式不正确,应为: username/repo-name")
169
+ sys.exit(1)
170
+
171
+ path = Path(path)
172
+ if not path.exists():
173
+ print_error(f"路径不存在: {path}")
174
+ sys.exit(1)
175
+
176
+ if path.is_file():
177
+ print_info(f"正在上传文件: {path}")
178
+ file_size = format_file_size(path.stat().st_size)
179
+ print_info(f"文件大小: {file_size}")
180
+
181
+ if api.upload_folder(path, repo_id, message=message):
182
+ print_success(f"文件上传成功: {path.name}")
183
+ else:
184
+ print_error(f"文件上传失败: {path.name}")
185
+ sys.exit(1)
186
+
187
+ elif path.is_dir():
188
+ file_count = count_files_in_directory(path)
189
+ dir_size = format_file_size(get_directory_size(path))
190
+
191
+ print_info(f"正在上传目录: {path}")
192
+ print_info(f"文件数量: {file_count}")
193
+ print_info(f"目录大小: {dir_size}")
194
+
195
+ if api.upload_directory(path, repo_id, message=message):
196
+ print_success(f"目录上传成功: {path}")
197
+ else:
198
+ print_error(f"目录上传失败: {path}")
199
+ sys.exit(1)
200
+
201
+ else:
202
+ print_error(f"不支持的路径类型: {path}")
203
+ sys.exit(1)
204
+
205
+
206
+ @cli.command()
207
+ @click.argument('repo_id')
208
+ @click.option('--directory', '-d', type=click.Path(),
209
+ help='下载到指定目录')
210
+ @click.option('--force', is_flag=True, help='强制覆盖已存在的文件')
211
+ def download(repo_id, directory, force):
212
+ """下载仓库到本地(公开仓库无需登录)"""
213
+ if not validate_repo_name(repo_id):
214
+ print_error("仓库ID格式不正确,应为: username/repo-name")
215
+ sys.exit(1)
216
+
217
+ # 确定下载目录
218
+ if directory:
219
+ local_path = Path(directory)
220
+ if not is_valid_path(directory):
221
+ print_error(f"无效的目录路径: {directory}")
222
+ sys.exit(1)
223
+ else:
224
+ local_path = Path.cwd() / repo_id.split('/')[-1]
225
+
226
+ # 检查目录是否存在
227
+ if local_path.exists():
228
+ if local_path.is_file():
229
+ print_error(f"目标路径是文件,不是目录: {local_path}")
230
+ sys.exit(1)
231
+ elif local_path.is_dir() and any(local_path.iterdir()):
232
+ if force:
233
+ print_info("强制覆盖模式,将重新下载所有文件")
234
+ else:
235
+ print_info("目录已存在,启用断点续传模式")
236
+
237
+ # 确保目录存在
238
+ if not ensure_directory(local_path):
239
+ print_error(f"无法创建目录: {local_path}")
240
+ sys.exit(1)
241
+
242
+ print_info(f"正在下载仓库: {repo_id}")
243
+ print_info(f"下载到: {local_path}")
244
+
245
+ # 如果未登录,提示用户这是公开仓库下载模式
246
+ if not config.is_logged_in():
247
+ print_info("当前未登录,尝试下载公开仓库...")
248
+
249
+ if api.download_repo(repo_id, local_path, force_download=force):
250
+ print_success(f"仓库下载成功: {local_path}")
251
+ else:
252
+ print_error(f"仓库下载失败: {repo_id}")
253
+ if not config.is_logged_in():
254
+ print_info("提示:如果这是私有仓库,请先使用 'atomgit login' 登录")
255
+ sys.exit(1)
256
+
257
+
258
+ @cli.command()
259
+ def config_show():
260
+ """显示配置信息"""
261
+ if config.is_logged_in():
262
+ credentials = config.get_credentials()
263
+ print_info(f"登录状态: 已登录")
264
+ print_info(f"配置文件: {config.config_file}")
265
+
266
+ # 简单检查Git集成状态
267
+ if check_git_available():
268
+ try:
269
+ import subprocess
270
+ # 检查任一域名的凭证助手配置
271
+ result1 = subprocess.run(['git', 'config', '--global', '--get', 'credential.https://atomgit.com.helper'],
272
+ capture_output=True, text=True)
273
+ result2 = subprocess.run(['git', 'config', '--global', '--get', 'credential.https://hub.atomgit.com.helper'],
274
+ capture_output=True, text=True)
275
+
276
+ if ((result1.returncode == 0 and 'git-credential-atomgit' in result1.stdout) or
277
+ (result2.returncode == 0 and 'git-credential-atomgit' in result2.stdout)):
278
+ print_info("Git集成: 已启用")
279
+ else:
280
+ print_info("Git集成: 未启用")
281
+ except Exception:
282
+ print_info("Git集成: 检查失败")
283
+ else:
284
+ print_warning("当前未登录")
285
+ print_info(f"配置文件: {config.config_file}")
286
+
287
+
288
+
289
+ if __name__ == '__main__':
290
+ cli()
atomgit/config.py ADDED
@@ -0,0 +1,67 @@
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Optional, Dict, Any
5
+
6
+
7
+ class Config:
8
+ """配置管理类,用于管理用户认证信息和设置"""
9
+
10
+ def __init__(self):
11
+ self.config_dir = Path.home() / '.atomgit'
12
+ self.config_file = self.config_dir / 'config.json'
13
+ self.config_dir.mkdir(exist_ok=True)
14
+ self._config = self._load_config()
15
+
16
+ def _load_config(self) -> Dict[str, Any]:
17
+ """加载配置文件"""
18
+ if self.config_file.exists():
19
+ try:
20
+ with open(self.config_file, 'r', encoding='utf-8') as f:
21
+ return json.load(f)
22
+ except (json.JSONDecodeError, IOError):
23
+ return {}
24
+ return {}
25
+
26
+ def _save_config(self) -> None:
27
+ """保存配置文件"""
28
+ try:
29
+ with open(self.config_file, 'w', encoding='utf-8') as f:
30
+ json.dump(self._config, f, ensure_ascii=False, indent=2)
31
+ except IOError as e:
32
+ raise Exception(f"无法保存配置文件: {e}")
33
+
34
+ def set_credentials(self, token: str) -> None:
35
+ """设置用户认证信息"""
36
+ self._config['token'] = token
37
+ self._save_config()
38
+
39
+ def get_credentials(self) -> Optional[Dict[str, str]]:
40
+ """获取用户认证信息"""
41
+ token = self._config.get('token')
42
+ if token:
43
+ return {'token': token}
44
+ return None
45
+
46
+ def clear_credentials(self) -> None:
47
+ """清除用户认证信息"""
48
+ self._config.pop('username', None) # 保留以兼容旧配置
49
+ self._config.pop('token', None)
50
+ self._save_config()
51
+
52
+ def is_logged_in(self) -> bool:
53
+ """检查是否已登录"""
54
+ return self.get_credentials() is not None
55
+
56
+ def set_value(self, key: str, value: Any) -> None:
57
+ """设置配置值"""
58
+ self._config[key] = value
59
+ self._save_config()
60
+
61
+ def get_value(self, key: str, default: Any = None) -> Any:
62
+ """获取配置值"""
63
+ return self._config.get(key, default)
64
+
65
+
66
+ # 全局配置实例
67
+ config = Config()
atomgit/utils.py ADDED
@@ -0,0 +1,344 @@
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from colorama import Fore, Style, init
7
+ import urllib.parse
8
+
9
+ # 初始化colorama
10
+ init(autoreset=True)
11
+
12
+
13
+ def print_success(message: str) -> None:
14
+ """打印成功信息"""
15
+ print(f"{Fore.GREEN}✓ {message}{Style.RESET_ALL}")
16
+
17
+
18
+ def print_error(message: str) -> None:
19
+ """打印错误信息"""
20
+ print(f"{Fore.RED}✗ {message}{Style.RESET_ALL}")
21
+
22
+
23
+ def print_warning(message: str) -> None:
24
+ """打印警告信息"""
25
+ print(f"{Fore.YELLOW}⚠ {message}{Style.RESET_ALL}")
26
+
27
+
28
+ def print_info(message: str) -> None:
29
+ """打印信息"""
30
+ print(f"{Fore.CYAN}ℹ {message}{Style.RESET_ALL}")
31
+
32
+
33
+ def validate_repo_name(repo_name: str) -> bool:
34
+ """验证仓库名称格式"""
35
+ if not repo_name:
36
+ return False
37
+
38
+ # 先解码URL编码
39
+ decoded_name = urllib.parse.unquote(repo_name)
40
+
41
+ # 检查是否包含至少一个斜杠(在解码后的名称中)
42
+ if '/' not in decoded_name:
43
+ return False
44
+
45
+ parts = decoded_name.split('/')
46
+ # 支持多层次仓库名称,至少需要2个部分,但可以有更多
47
+ # 允许hf_mirrors/Qwen/Qwen3-Reranker-0.6B这样的格式
48
+ if len(parts) < 2:
49
+ return False
50
+
51
+ # 检查每个部分都不为空
52
+ for part in parts:
53
+ if not part:
54
+ return False
55
+
56
+ # 检查字符是否合法(字母、数字、下划线、短横线、点号、斜杠)
57
+ # 对于原始名称,也允许%字符用于URL编码
58
+ allowed_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.%/')
59
+
60
+ # 验证原始名称的字符
61
+ for char in repo_name:
62
+ if char not in allowed_chars:
63
+ return False
64
+
65
+ return True
66
+
67
+
68
+ def validate_repo_type(repo_type: str) -> bool:
69
+ """验证仓库类型"""
70
+ return repo_type in ['model', 'dataset']
71
+
72
+
73
+ def get_file_size(file_path: Path) -> int:
74
+ """获取文件大小"""
75
+ try:
76
+ return file_path.stat().st_size
77
+ except OSError:
78
+ return 0
79
+
80
+
81
+ def format_file_size(size_bytes: int) -> str:
82
+ """格式化文件大小"""
83
+ if size_bytes == 0:
84
+ return "0 B"
85
+
86
+ size_names = ["B", "KB", "MB", "GB", "TB"]
87
+ i = 0
88
+ while size_bytes >= 1024 and i < len(size_names) - 1:
89
+ size_bytes /= 1024.0
90
+ i += 1
91
+
92
+ return f"{size_bytes:.1f} {size_names[i]}"
93
+
94
+
95
+ def get_directory_size(dir_path: Path) -> int:
96
+ """获取目录大小"""
97
+ total_size = 0
98
+ try:
99
+ for file_path in dir_path.rglob('*'):
100
+ if file_path.is_file():
101
+ total_size += get_file_size(file_path)
102
+ except OSError:
103
+ pass
104
+ return total_size
105
+
106
+
107
+ def count_files_in_directory(dir_path: Path) -> int:
108
+ """统计目录中的文件数量"""
109
+ count = 0
110
+ try:
111
+ for file_path in dir_path.rglob('*'):
112
+ if file_path.is_file():
113
+ count += 1
114
+ except OSError:
115
+ pass
116
+ return count
117
+
118
+
119
+ def confirm_action(message: str, default: bool = False) -> bool:
120
+ """确认操作"""
121
+ suffix = " [Y/n]" if default else " [y/N]"
122
+ while True:
123
+ response = input(f"{message}{suffix}: ").strip().lower()
124
+ if response == '':
125
+ return default
126
+ elif response in ['y', 'yes']:
127
+ return True
128
+ elif response in ['n', 'no']:
129
+ return False
130
+ else:
131
+ print("请输入 y/yes 或 n/no")
132
+
133
+
134
+ def is_valid_path(path_str: str) -> bool:
135
+ """检查路径是否有效"""
136
+ try:
137
+ Path(path_str)
138
+ return True
139
+ except (ValueError, OSError):
140
+ return False
141
+
142
+
143
+ def ensure_directory(dir_path: Path) -> bool:
144
+ """确保目录存在"""
145
+ try:
146
+ dir_path.mkdir(parents=True, exist_ok=True)
147
+ return True
148
+ except OSError:
149
+ return False
150
+
151
+
152
+ def get_relative_path(file_path: Path, base_path: Path) -> str:
153
+ """获取相对路径"""
154
+ try:
155
+ return str(file_path.relative_to(base_path))
156
+ except ValueError:
157
+ return str(file_path)
158
+
159
+
160
+ def setup_git_credentials(token: str) -> bool:
161
+ """配置Git凭证助手,使用保存的token"""
162
+ try:
163
+ # 获取配置目录
164
+ config_dir = Path.home() / '.atomgit'
165
+ config_dir.mkdir(exist_ok=True)
166
+
167
+ # 创建凭证助手脚本
168
+ credential_helper_path = config_dir / 'git-credential-atomgit'
169
+
170
+ # 创建凭证助手脚本内容
171
+ helper_script = '''#!/usr/bin/env python3
172
+ # -*- coding: utf-8 -*-
173
+ """
174
+ AtomGit Git Credential Helper
175
+ 自动提供保存的AtomGit token用于Git认证,并从API获取真实用户名
176
+ """
177
+
178
+ import sys
179
+ import json
180
+ import urllib.request
181
+ import urllib.error
182
+ from pathlib import Path
183
+
184
+ def get_atomgit_username(token):
185
+ """通过AtomGit API获取用户名"""
186
+ try:
187
+ # 调用AtomGit API获取用户信息
188
+ api_url = 'https://atomgit.com/api/v5/user'
189
+ req = urllib.request.Request(
190
+ api_url,
191
+ headers={
192
+ 'Authorization': f'token {token}',
193
+ 'User-Agent': 'atomgit-cli',
194
+ 'Accept': 'application/json'
195
+ }
196
+ )
197
+
198
+ with urllib.request.urlopen(req, timeout=10) as response:
199
+ if response.status == 200:
200
+ data = json.loads(response.read().decode('utf-8'))
201
+ login = data.get('login')
202
+ if login:
203
+ return login
204
+ except Exception:
205
+ pass
206
+
207
+ # API调用失败时返回默认用户名
208
+ return 'atomgit-user'
209
+
210
+ def main():
211
+ operation = sys.argv[1] if len(sys.argv) > 1 else 'get'
212
+
213
+ if operation == 'get':
214
+ # 读取Git传递的信息
215
+ input_data = {}
216
+ for line in sys.stdin:
217
+ line = line.strip()
218
+ if not line:
219
+ break
220
+ key, value = line.split('=', 1)
221
+ input_data[key] = value
222
+
223
+ # 检查是否为AtomGit主机(支持多个域名)
224
+ host = input_data.get('host', '')
225
+ if host in ['atomgit.com', 'hub.atomgit.com']:
226
+ # 读取保存的token
227
+ config_file = Path.home() / '.atomgit' / 'config.json'
228
+ if config_file.exists():
229
+ try:
230
+ with open(config_file, 'r', encoding='utf-8') as f:
231
+ config = json.load(f)
232
+
233
+ token = config.get('token')
234
+ if token:
235
+ # 获取真实的AtomGit用户名
236
+ username = get_atomgit_username(token)
237
+
238
+ # 输出认证信息
239
+ print(f'username={username}')
240
+ print(f'password={token}')
241
+ return
242
+ except Exception:
243
+ pass
244
+
245
+ # 对于store和erase操作,什么都不做
246
+ elif operation in ['store', 'erase']:
247
+ # 读取并忽略输入
248
+ for line in sys.stdin:
249
+ if not line.strip():
250
+ break
251
+
252
+ if __name__ == '__main__':
253
+ main()
254
+ '''
255
+
256
+ # 写入脚本文件
257
+ with open(credential_helper_path, 'w', encoding='utf-8') as f:
258
+ f.write(helper_script)
259
+
260
+ # 设置脚本为可执行
261
+ credential_helper_path.chmod(0o755)
262
+
263
+ # 配置Git使用我们的凭证助手(为两个域名都配置)
264
+ git_hosts = ['atomgit.com', 'hub.atomgit.com']
265
+ commands = []
266
+
267
+ for host in git_hosts:
268
+ commands.append(['git', 'config', '--global', f'credential.https://{host}.helper', f'!{credential_helper_path}'])
269
+
270
+ for cmd in commands:
271
+ result = subprocess.run(cmd, capture_output=True, text=True)
272
+ if result.returncode != 0:
273
+ print_warning(f"配置Git命令失败: {' '.join(cmd)}")
274
+ print_warning(f"错误信息: {result.stderr}")
275
+ return False
276
+
277
+ print_success(f"Git凭证助手配置成功,现在可以直接使用git命令访问AtomGit仓库")
278
+ return True
279
+
280
+ except Exception as e:
281
+ print_error(f"配置Git凭证助手失败: {e}")
282
+ return False
283
+
284
+
285
+ def clear_git_credentials() -> bool:
286
+ """清除Git凭证配置"""
287
+ try:
288
+ # 移除Git配置(清除两个域名的配置)
289
+ git_hosts = ['atomgit.com', 'hub.atomgit.com']
290
+ commands = []
291
+
292
+ for host in git_hosts:
293
+ commands.append(['git', 'config', '--global', '--unset', f'credential.https://{host}.helper'])
294
+
295
+ for cmd in commands:
296
+ result = subprocess.run(cmd, capture_output=True, text=True)
297
+ # 忽略不存在的配置项错误
298
+ if result.returncode != 0 and "not found" not in result.stderr:
299
+ print_warning(f"清除Git配置命令失败: {' '.join(cmd)}")
300
+ print_warning(f"错误信息: {result.stderr}")
301
+
302
+ # 删除凭证助手脚本
303
+ config_dir = Path.home() / '.atomgit'
304
+ credential_helper_path = config_dir / 'git-credential-atomgit'
305
+ if credential_helper_path.exists():
306
+ credential_helper_path.unlink()
307
+
308
+ print_success(f"Git凭证配置已清除")
309
+ return True
310
+
311
+ except Exception as e:
312
+ print_error(f"清除Git凭证配置失败: {e}")
313
+ return False
314
+
315
+
316
+ def check_git_available() -> bool:
317
+ """检查Git是否可用"""
318
+ try:
319
+ result = subprocess.run(['git', '--version'], capture_output=True, text=True)
320
+ return result.returncode == 0
321
+ except FileNotFoundError:
322
+ return False
323
+
324
+
325
+ def get_git_user_info() -> dict:
326
+ """获取Git用户信息"""
327
+ info = {}
328
+ try:
329
+ # 获取用户名
330
+ result = subprocess.run(['git', 'config', '--global', 'user.name'],
331
+ capture_output=True, text=True)
332
+ if result.returncode == 0:
333
+ info['name'] = result.stdout.strip()
334
+
335
+ # 获取邮箱
336
+ result = subprocess.run(['git', 'config', '--global', 'user.email'],
337
+ capture_output=True, text=True)
338
+ if result.returncode == 0:
339
+ info['email'] = result.stdout.strip()
340
+
341
+ except FileNotFoundError:
342
+ pass
343
+
344
+ return info