devlake-mcp 0.4.1__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.
- devlake_mcp/__init__.py +7 -0
- devlake_mcp/__main__.py +10 -0
- devlake_mcp/cli.py +794 -0
- devlake_mcp/client.py +474 -0
- devlake_mcp/compat.py +165 -0
- devlake_mcp/config.py +204 -0
- devlake_mcp/constants.py +161 -0
- devlake_mcp/enums.py +58 -0
- devlake_mcp/generation_manager.py +296 -0
- devlake_mcp/git_utils.py +489 -0
- devlake_mcp/hooks/__init__.py +49 -0
- devlake_mcp/hooks/hook_utils.py +246 -0
- devlake_mcp/hooks/post_tool_use.py +325 -0
- devlake_mcp/hooks/pre_tool_use.py +110 -0
- devlake_mcp/hooks/record_session.py +183 -0
- devlake_mcp/hooks/session_start.py +81 -0
- devlake_mcp/hooks/stop.py +275 -0
- devlake_mcp/hooks/transcript_utils.py +547 -0
- devlake_mcp/hooks/user_prompt_submit.py +204 -0
- devlake_mcp/logging_config.py +202 -0
- devlake_mcp/retry_queue.py +556 -0
- devlake_mcp/server.py +664 -0
- devlake_mcp/session_manager.py +444 -0
- devlake_mcp/utils.py +225 -0
- devlake_mcp/version_utils.py +174 -0
- devlake_mcp-0.4.1.dist-info/METADATA +541 -0
- devlake_mcp-0.4.1.dist-info/RECORD +31 -0
- devlake_mcp-0.4.1.dist-info/WHEEL +5 -0
- devlake_mcp-0.4.1.dist-info/entry_points.txt +3 -0
- devlake_mcp-0.4.1.dist-info/licenses/LICENSE +21 -0
- devlake_mcp-0.4.1.dist-info/top_level.txt +1 -0
devlake_mcp/git_utils.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Git 信息获取工具模块
|
|
5
|
+
|
|
6
|
+
提供 Git 仓库信息的获取功能:
|
|
7
|
+
- 当前分支
|
|
8
|
+
- 最新 commit hash
|
|
9
|
+
- Git 配置的用户名和邮箱
|
|
10
|
+
- 项目ID提取(namespace/name)
|
|
11
|
+
|
|
12
|
+
改进:
|
|
13
|
+
- 完整的类型注解
|
|
14
|
+
- 使用常量配置
|
|
15
|
+
- 更好的错误处理
|
|
16
|
+
- 完善的日志记录
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import subprocess
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import logging
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional, Dict
|
|
25
|
+
|
|
26
|
+
from .constants import GIT_COMMAND_TIMEOUT
|
|
27
|
+
|
|
28
|
+
# 配置日志
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_git_info(
|
|
33
|
+
cwd: str,
|
|
34
|
+
timeout: int = GIT_COMMAND_TIMEOUT,
|
|
35
|
+
include_user_info: bool = True
|
|
36
|
+
) -> Dict[str, str]:
|
|
37
|
+
"""
|
|
38
|
+
获取当前项目的 Git 信息
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
cwd: 项目根目录路径
|
|
42
|
+
timeout: Git 命令超时时间(秒),默认 1 秒
|
|
43
|
+
include_user_info: 是否获取用户信息(git_author/git_email),默认 True
|
|
44
|
+
如果环境变量已缓存,可以设为 False 提升性能
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Git 信息字典:
|
|
48
|
+
{
|
|
49
|
+
"git_branch": "feature/ai-coding", # 当前分支
|
|
50
|
+
"git_commit": "abc123def456...", # 最新 commit hash(完整)
|
|
51
|
+
"git_author": "wangzhong", # Git 配置的用户名(可选)
|
|
52
|
+
"git_email": "wangzhong@example.com" # Git 配置的邮箱(可选)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
如果不是 Git 仓库或获取失败,返回 "unknown"
|
|
56
|
+
|
|
57
|
+
示例:
|
|
58
|
+
>>> git_info = get_git_info('/path/to/project')
|
|
59
|
+
>>> print(git_info['git_branch'])
|
|
60
|
+
feature/ai-coding
|
|
61
|
+
|
|
62
|
+
>>> # 如果用户信息已缓存,可以跳过获取
|
|
63
|
+
>>> git_info = get_git_info('/path/to/project', include_user_info=False)
|
|
64
|
+
"""
|
|
65
|
+
git_info = {
|
|
66
|
+
"git_branch": "unknown",
|
|
67
|
+
"git_commit": "unknown",
|
|
68
|
+
"git_author": "unknown",
|
|
69
|
+
"git_email": "unknown"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# 1. 检查是否是 Git 仓库
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
['git', 'rev-parse', '--is-inside-work-tree'],
|
|
76
|
+
cwd=cwd,
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
timeout=timeout
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
is_git_repo = (result.returncode == 0)
|
|
83
|
+
|
|
84
|
+
if not is_git_repo:
|
|
85
|
+
# 不是 Git 仓库,只获取全局配置
|
|
86
|
+
logger.debug(f"目录不是 Git 仓库: {cwd},尝试获取全局 Git 配置")
|
|
87
|
+
|
|
88
|
+
if include_user_info:
|
|
89
|
+
# 获取全局用户名
|
|
90
|
+
result_global = subprocess.run(
|
|
91
|
+
['git', 'config', '--global', 'user.name'],
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True,
|
|
94
|
+
timeout=timeout
|
|
95
|
+
)
|
|
96
|
+
if result_global.returncode == 0:
|
|
97
|
+
git_info['git_author'] = result_global.stdout.strip()
|
|
98
|
+
logger.debug(f"Git author (global): {git_info['git_author']}")
|
|
99
|
+
|
|
100
|
+
# 获取全局邮箱
|
|
101
|
+
result_global = subprocess.run(
|
|
102
|
+
['git', 'config', '--global', 'user.email'],
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
timeout=timeout
|
|
106
|
+
)
|
|
107
|
+
if result_global.returncode == 0:
|
|
108
|
+
git_info['git_email'] = result_global.stdout.strip()
|
|
109
|
+
logger.debug(f"Git email (global): {git_info['git_email']}")
|
|
110
|
+
|
|
111
|
+
return git_info
|
|
112
|
+
|
|
113
|
+
# 2. 获取当前分支
|
|
114
|
+
result = subprocess.run(
|
|
115
|
+
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
|
116
|
+
cwd=cwd,
|
|
117
|
+
capture_output=True,
|
|
118
|
+
text=True,
|
|
119
|
+
timeout=timeout
|
|
120
|
+
)
|
|
121
|
+
if result.returncode == 0:
|
|
122
|
+
git_info['git_branch'] = result.stdout.strip()
|
|
123
|
+
logger.debug(f"Git 分支: {git_info['git_branch']}")
|
|
124
|
+
else:
|
|
125
|
+
logger.warning(f"无法获取 Git 分支: {result.stderr.strip()}")
|
|
126
|
+
|
|
127
|
+
# 3. 获取最新 commit hash(完整 40 位)
|
|
128
|
+
result = subprocess.run(
|
|
129
|
+
['git', 'rev-parse', 'HEAD'],
|
|
130
|
+
cwd=cwd,
|
|
131
|
+
capture_output=True,
|
|
132
|
+
text=True,
|
|
133
|
+
timeout=timeout
|
|
134
|
+
)
|
|
135
|
+
if result.returncode == 0:
|
|
136
|
+
git_info['git_commit'] = result.stdout.strip()
|
|
137
|
+
logger.debug(f"Git commit: {git_info['git_commit'][:8]}")
|
|
138
|
+
else:
|
|
139
|
+
logger.warning(f"无法获取 Git commit: {result.stderr.strip()}")
|
|
140
|
+
|
|
141
|
+
# 4. 获取 Git 配置的用户名和邮箱(可选)
|
|
142
|
+
# 直接使用全局配置,简化逻辑并确保一致性
|
|
143
|
+
if include_user_info:
|
|
144
|
+
# 获取全局用户名
|
|
145
|
+
result = subprocess.run(
|
|
146
|
+
['git', 'config', '--global', 'user.name'],
|
|
147
|
+
capture_output=True,
|
|
148
|
+
text=True,
|
|
149
|
+
timeout=timeout
|
|
150
|
+
)
|
|
151
|
+
if result.returncode == 0:
|
|
152
|
+
git_info['git_author'] = result.stdout.strip()
|
|
153
|
+
logger.debug(f"Git author (global): {git_info['git_author']}")
|
|
154
|
+
else:
|
|
155
|
+
logger.warning("未配置 git user.name (--global)")
|
|
156
|
+
|
|
157
|
+
# 获取全局邮箱
|
|
158
|
+
result = subprocess.run(
|
|
159
|
+
['git', 'config', '--global', 'user.email'],
|
|
160
|
+
capture_output=True,
|
|
161
|
+
text=True,
|
|
162
|
+
timeout=timeout
|
|
163
|
+
)
|
|
164
|
+
if result.returncode == 0:
|
|
165
|
+
git_info['git_email'] = result.stdout.strip()
|
|
166
|
+
logger.debug(f"Git email (global): {git_info['git_email']}")
|
|
167
|
+
else:
|
|
168
|
+
logger.warning("未配置 git user.email (--global)")
|
|
169
|
+
|
|
170
|
+
except subprocess.TimeoutExpired as e:
|
|
171
|
+
# 超时,返回默认值
|
|
172
|
+
logger.warning(f"Git 命令超时 ({timeout}秒): {e.cmd}")
|
|
173
|
+
except FileNotFoundError:
|
|
174
|
+
# git 命令未找到
|
|
175
|
+
logger.error("Git 命令未找到,请确保已安装 Git")
|
|
176
|
+
except Exception as e:
|
|
177
|
+
# 其他异常
|
|
178
|
+
logger.error(f"获取 Git 信息失败: {e}", exc_info=True)
|
|
179
|
+
|
|
180
|
+
return git_info
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_current_branch(cwd: str, timeout: int = GIT_COMMAND_TIMEOUT) -> str:
|
|
184
|
+
"""
|
|
185
|
+
快速获取当前 Git 分支(简化版)
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
cwd: 项目根目录
|
|
189
|
+
timeout: 超时时间(秒)
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
分支名称,失败返回 'unknown'
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
result = subprocess.run(
|
|
196
|
+
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
|
197
|
+
cwd=cwd,
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
timeout=timeout
|
|
201
|
+
)
|
|
202
|
+
if result.returncode == 0:
|
|
203
|
+
return result.stdout.strip()
|
|
204
|
+
else:
|
|
205
|
+
logger.debug(f"获取分支失败: {result.stderr.strip()}")
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.warning(f"获取当前分支失败: {e}")
|
|
208
|
+
|
|
209
|
+
return 'unknown'
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_git_remote_url(cwd: str, timeout: int = GIT_COMMAND_TIMEOUT) -> Optional[str]:
|
|
213
|
+
"""
|
|
214
|
+
获取 Git remote URL
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
cwd: 项目路径
|
|
218
|
+
timeout: 超时时间(秒)
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Git remote URL,失败返回 None
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
result = subprocess.run(
|
|
225
|
+
['git', 'config', '--get', 'remote.origin.url'],
|
|
226
|
+
cwd=cwd,
|
|
227
|
+
capture_output=True,
|
|
228
|
+
text=True,
|
|
229
|
+
timeout=timeout
|
|
230
|
+
)
|
|
231
|
+
if result.returncode == 0:
|
|
232
|
+
return result.stdout.strip()
|
|
233
|
+
except Exception:
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def extract_git_repo_path(git_remote_url: Optional[str], cwd: str) -> str:
|
|
240
|
+
"""
|
|
241
|
+
从 Git remote URL 提取 git_repo_path (namespace/name)
|
|
242
|
+
|
|
243
|
+
支持的格式:
|
|
244
|
+
- https://github.com/yourorg/devlake.git -> yourorg/devlake
|
|
245
|
+
- git@github.com:yourorg/devlake.git -> yourorg/devlake
|
|
246
|
+
- https://gitlab.com/team/subteam/project.git -> team/subteam/project
|
|
247
|
+
- git@gitlab.com:team/project.git -> team/project
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
git_remote_url: Git 远程仓库 URL
|
|
251
|
+
cwd: 项目路径(作为 fallback)
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
git_repo_path (namespace/name)
|
|
255
|
+
如果无法提取,返回 'local/{directory_name}'
|
|
256
|
+
"""
|
|
257
|
+
if not git_remote_url:
|
|
258
|
+
# 没有 Git 仓库,使用 local/{dirname}
|
|
259
|
+
return f"local/{Path(cwd).name}"
|
|
260
|
+
|
|
261
|
+
# 去掉 .git 后缀(修复:使用 removesuffix 避免误删末尾字符)
|
|
262
|
+
url = git_remote_url.removesuffix('.git')
|
|
263
|
+
|
|
264
|
+
# 提取 namespace/name (支持多级 namespace)
|
|
265
|
+
# 匹配格式:
|
|
266
|
+
# - https://github.com/yourorg/devlake -> yourorg/devlake
|
|
267
|
+
# - git@gitlab.com:team/project -> team/project
|
|
268
|
+
# - https://gitlab.com/team/subteam/project -> team/subteam/project
|
|
269
|
+
|
|
270
|
+
patterns = [
|
|
271
|
+
# HTTPS 格式:https://domain.com/namespace/name
|
|
272
|
+
r'https?://[^/]+/(.+)',
|
|
273
|
+
# SSH 格式:git@domain.com:namespace/name
|
|
274
|
+
r'git@[^:]+:(.+)',
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
for pattern in patterns:
|
|
278
|
+
match = re.search(pattern, url)
|
|
279
|
+
if match:
|
|
280
|
+
return match.group(1)
|
|
281
|
+
|
|
282
|
+
# 解析失败,降级到 local/{dirname}
|
|
283
|
+
return f"local/{Path(cwd).name}"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def get_git_repo_path(cwd: str) -> str:
|
|
287
|
+
"""
|
|
288
|
+
获取Git仓库路径 (namespace/name)
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
cwd: 项目路径
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
git_repo_path,如 'yourorg/devlake'
|
|
295
|
+
"""
|
|
296
|
+
git_remote_url = get_git_remote_url(cwd)
|
|
297
|
+
return extract_git_repo_path(git_remote_url, cwd)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def get_git_root(cwd: str, timeout: int = GIT_COMMAND_TIMEOUT) -> Optional[str]:
|
|
301
|
+
"""
|
|
302
|
+
获取 Git 仓库根目录
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
cwd: 当前工作目录
|
|
306
|
+
timeout: 超时时间(秒)
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Git 仓库根目录的绝对路径,失败返回 None
|
|
310
|
+
"""
|
|
311
|
+
try:
|
|
312
|
+
result = subprocess.run(
|
|
313
|
+
['git', 'rev-parse', '--show-toplevel'],
|
|
314
|
+
cwd=cwd,
|
|
315
|
+
capture_output=True,
|
|
316
|
+
text=True,
|
|
317
|
+
timeout=timeout
|
|
318
|
+
)
|
|
319
|
+
if result.returncode == 0:
|
|
320
|
+
return result.stdout.strip()
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def get_full_git_context(cwd: str, use_env_cache: bool = True) -> Dict[str, str]:
|
|
328
|
+
"""
|
|
329
|
+
获取完整的 Git 上下文信息(统一接口)
|
|
330
|
+
|
|
331
|
+
这个函数整合了静态和动态 Git 信息的获取逻辑,避免重复代码。
|
|
332
|
+
|
|
333
|
+
策略:
|
|
334
|
+
- 静态信息(author, email, repo_path):
|
|
335
|
+
* 如果 use_env_cache=True,优先从环境变量读取(避免重复执行 git config)
|
|
336
|
+
* 如果环境变量不存在,则执行 git 命令获取
|
|
337
|
+
- 动态信息(branch, commit):始终执行 git 命令获取最新值
|
|
338
|
+
- 衍生信息(project_name, git_root):自动计算
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
cwd: 当前工作目录
|
|
342
|
+
use_env_cache: 是否使用环境变量缓存的静态信息(默认 True)
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Dict[str, str]: 完整的 Git 上下文,包含:
|
|
346
|
+
- git_branch: 当前分支
|
|
347
|
+
- git_commit: 当前 commit hash(完整40位)
|
|
348
|
+
- git_author: Git 用户名
|
|
349
|
+
- git_email: Git 用户邮箱
|
|
350
|
+
- git_repo_path: 仓库路径 (namespace/name)
|
|
351
|
+
- project_name: 项目名称(从 repo_path 提取)
|
|
352
|
+
- git_root: Git 仓库根目录(绝对路径)
|
|
353
|
+
|
|
354
|
+
示例:
|
|
355
|
+
>>> context = get_full_git_context('/path/to/project')
|
|
356
|
+
>>> print(context['git_branch'])
|
|
357
|
+
main
|
|
358
|
+
>>> print(context['git_repo_path'])
|
|
359
|
+
yourorg/devlake
|
|
360
|
+
>>> print(context['project_name'])
|
|
361
|
+
devlake
|
|
362
|
+
|
|
363
|
+
>>> # 不使用缓存,强制重新获取
|
|
364
|
+
>>> context = get_full_git_context('/path/to/project', use_env_cache=False)
|
|
365
|
+
"""
|
|
366
|
+
context = {}
|
|
367
|
+
|
|
368
|
+
# 1. 静态信息:优先从环境变量读取(避免重复执行 git config)
|
|
369
|
+
if use_env_cache:
|
|
370
|
+
context['git_author'] = os.getenv('GIT_AUTHOR', 'unknown')
|
|
371
|
+
context['git_email'] = os.getenv('GIT_EMAIL', 'unknown')
|
|
372
|
+
context['git_repo_path'] = os.getenv('GIT_REPO_PATH', 'unknown')
|
|
373
|
+
|
|
374
|
+
# 如果环境变量不存在,则执行 git 命令获取
|
|
375
|
+
if context['git_author'] == 'unknown' or context['git_email'] == 'unknown':
|
|
376
|
+
git_info = get_git_info(cwd, include_user_info=True)
|
|
377
|
+
if context['git_author'] == 'unknown':
|
|
378
|
+
context['git_author'] = git_info.get('git_author', 'unknown')
|
|
379
|
+
if context['git_email'] == 'unknown':
|
|
380
|
+
context['git_email'] = git_info.get('git_email', 'unknown')
|
|
381
|
+
|
|
382
|
+
if context['git_repo_path'] == 'unknown':
|
|
383
|
+
context['git_repo_path'] = get_git_repo_path(cwd)
|
|
384
|
+
else:
|
|
385
|
+
# 不使用缓存,直接获取
|
|
386
|
+
git_info = get_git_info(cwd, include_user_info=True)
|
|
387
|
+
context['git_author'] = git_info.get('git_author', 'unknown')
|
|
388
|
+
context['git_email'] = git_info.get('git_email', 'unknown')
|
|
389
|
+
context['git_repo_path'] = get_git_repo_path(cwd)
|
|
390
|
+
|
|
391
|
+
# 2. 动态信息:每次获取最新值(确保 branch/commit 正确)
|
|
392
|
+
git_info = get_git_info(cwd, include_user_info=False)
|
|
393
|
+
context['git_branch'] = git_info.get('git_branch', 'unknown')
|
|
394
|
+
context['git_commit'] = git_info.get('git_commit', 'unknown')
|
|
395
|
+
|
|
396
|
+
# 3. 衍生信息:自动计算
|
|
397
|
+
# 提取 project_name(从 repo_path 最后一段)
|
|
398
|
+
repo_path = context['git_repo_path']
|
|
399
|
+
context['project_name'] = repo_path.split('/')[-1] if '/' in repo_path else repo_path
|
|
400
|
+
|
|
401
|
+
# 获取 git_root
|
|
402
|
+
context['git_root'] = get_git_root(cwd) or ''
|
|
403
|
+
|
|
404
|
+
logger.debug(
|
|
405
|
+
f"Git 上下文: {context['project_name']} "
|
|
406
|
+
f"({context['git_branch']}@{context['git_commit'][:8]})"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
return context
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def get_git_context_from_file(file_path: str, use_env_cache: bool = True) -> Dict[str, str]:
|
|
413
|
+
"""
|
|
414
|
+
从文件路径获取 Git 上下文(支持 workspace 多项目环境)
|
|
415
|
+
|
|
416
|
+
这个函数专门用于处理文件变更场景,能够正确识别 workspace 中不同子项目的 Git 仓库。
|
|
417
|
+
|
|
418
|
+
工作原理:
|
|
419
|
+
1. 从文件所在目录向上查找 .git 目录(获取 git_root)
|
|
420
|
+
2. 基于 git_root 获取完整的 Git 上下文
|
|
421
|
+
3. 如果找不到 git_root,降级到文件所在目录
|
|
422
|
+
|
|
423
|
+
使用场景:
|
|
424
|
+
- 文件编辑/创建操作(Edit/Write tool)
|
|
425
|
+
- Shell 命令修改文件
|
|
426
|
+
- 任何有具体文件路径的操作
|
|
427
|
+
|
|
428
|
+
Workspace 支持:
|
|
429
|
+
在 workspace 环境下,不同的文件可能属于不同的 git 仓库:
|
|
430
|
+
```
|
|
431
|
+
workspace/
|
|
432
|
+
├── project-a/ (git: yourorg/project-a)
|
|
433
|
+
│ └── main.py
|
|
434
|
+
└── project-b/ (git: yourorg/project-b)
|
|
435
|
+
└── app.py
|
|
436
|
+
|
|
437
|
+
get_git_context_from_file('workspace/project-a/main.py')
|
|
438
|
+
→ git_repo_path = 'yourorg/project-a' ✓
|
|
439
|
+
|
|
440
|
+
get_git_context_from_file('workspace/project-b/app.py')
|
|
441
|
+
→ git_repo_path = 'yourorg/project-b' ✓
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
file_path: 文件路径(可以是相对路径或绝对路径)
|
|
446
|
+
use_env_cache: 是否使用环境变量缓存的静态信息(默认 True)
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Dict[str, str]: 完整的 Git 上下文,包含:
|
|
450
|
+
- git_branch: 当前分支
|
|
451
|
+
- git_commit: 当前 commit hash
|
|
452
|
+
- git_author: Git 用户名
|
|
453
|
+
- git_email: Git 用户邮箱
|
|
454
|
+
- git_repo_path: 仓库路径 (namespace/name)
|
|
455
|
+
- project_name: 项目名称
|
|
456
|
+
- git_root: Git 仓库根目录
|
|
457
|
+
|
|
458
|
+
示例:
|
|
459
|
+
>>> # Workspace 环境
|
|
460
|
+
>>> context = get_git_context_from_file('/workspace/project-a/src/main.py')
|
|
461
|
+
>>> print(context['git_repo_path'])
|
|
462
|
+
yourorg/project-a
|
|
463
|
+
|
|
464
|
+
>>> # 单项目环境(向后兼容)
|
|
465
|
+
>>> context = get_git_context_from_file('/project/src/utils.py')
|
|
466
|
+
>>> print(context['git_repo_path'])
|
|
467
|
+
yourorg/project
|
|
468
|
+
"""
|
|
469
|
+
# 1. 转换为绝对路径
|
|
470
|
+
abs_file_path = os.path.abspath(file_path)
|
|
471
|
+
|
|
472
|
+
# 2. 获取文件所在目录
|
|
473
|
+
file_dir = os.path.dirname(abs_file_path)
|
|
474
|
+
|
|
475
|
+
# 3. 从文件所在目录向上查找 Git 仓库根目录
|
|
476
|
+
git_root = get_git_root(file_dir)
|
|
477
|
+
|
|
478
|
+
# 4. 基于 git_root 获取完整的 Git 上下文
|
|
479
|
+
if git_root:
|
|
480
|
+
# 找到了 git root,使用它作为工作目录
|
|
481
|
+
logger.debug(f'从文件 {abs_file_path} 找到 Git root: {git_root}')
|
|
482
|
+
return get_full_git_context(git_root, use_env_cache=use_env_cache)
|
|
483
|
+
else:
|
|
484
|
+
# 降级方案:使用文件所在目录
|
|
485
|
+
logger.warning(
|
|
486
|
+
f'文件 {abs_file_path} 不在 Git 仓库中,'
|
|
487
|
+
f'使用文件所在目录 {file_dir} 作为 fallback'
|
|
488
|
+
)
|
|
489
|
+
return get_full_git_context(file_dir, use_env_cache=use_env_cache)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
DevLake MCP - Claude Code Hooks 模块
|
|
5
|
+
|
|
6
|
+
提供完整的 Claude Code hooks 功能,用于收集 AI 编码数据并上报到 DevLake。
|
|
7
|
+
|
|
8
|
+
使用方法:
|
|
9
|
+
from devlake_mcp.hooks import session_start
|
|
10
|
+
session_start.main()
|
|
11
|
+
|
|
12
|
+
所有可用的 hooks:
|
|
13
|
+
- session_start: 会话启动时触发
|
|
14
|
+
- pre_tool_use: 工具执行前触发
|
|
15
|
+
- post_tool_use: 工具执行后触发
|
|
16
|
+
- stop: Claude 完成回复时触发
|
|
17
|
+
- record_session: 会话结束时触发
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# 配置将在实际使用时加载,而不是在导入时
|
|
21
|
+
# 这样可以避免在测试环境中导致问题
|
|
22
|
+
from devlake_mcp.config import DevLakeConfig
|
|
23
|
+
|
|
24
|
+
def _initialize_hooks_config():
|
|
25
|
+
"""初始化 Hooks 配置(按需调用)"""
|
|
26
|
+
return DevLakeConfig.from_env(include_git=True)
|
|
27
|
+
|
|
28
|
+
# 导入所有 hook 模块
|
|
29
|
+
from devlake_mcp.hooks import (
|
|
30
|
+
hook_utils,
|
|
31
|
+
session_start,
|
|
32
|
+
pre_tool_use,
|
|
33
|
+
post_tool_use,
|
|
34
|
+
stop,
|
|
35
|
+
record_session,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# 工具模块
|
|
40
|
+
"hook_utils",
|
|
41
|
+
# Hook 模块
|
|
42
|
+
"session_start",
|
|
43
|
+
"pre_tool_use",
|
|
44
|
+
"post_tool_use",
|
|
45
|
+
"stop",
|
|
46
|
+
"record_session",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
__version__ = "0.1.0"
|