maque 0.2.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.
Files changed (143) hide show
  1. maque/__init__.py +30 -0
  2. maque/__main__.py +926 -0
  3. maque/ai_platform/__init__.py +0 -0
  4. maque/ai_platform/crawl.py +45 -0
  5. maque/ai_platform/metrics.py +258 -0
  6. maque/ai_platform/nlp_preprocess.py +67 -0
  7. maque/ai_platform/webpage_screen_shot.py +195 -0
  8. maque/algorithms/__init__.py +78 -0
  9. maque/algorithms/bezier.py +15 -0
  10. maque/algorithms/bktree.py +117 -0
  11. maque/algorithms/core.py +104 -0
  12. maque/algorithms/hilbert.py +16 -0
  13. maque/algorithms/rate_function.py +92 -0
  14. maque/algorithms/transform.py +27 -0
  15. maque/algorithms/trie.py +272 -0
  16. maque/algorithms/utils.py +63 -0
  17. maque/algorithms/video.py +587 -0
  18. maque/api/__init__.py +1 -0
  19. maque/api/common.py +110 -0
  20. maque/api/fetch.py +26 -0
  21. maque/api/static/icon.png +0 -0
  22. maque/api/static/redoc.standalone.js +1782 -0
  23. maque/api/static/swagger-ui-bundle.js +3 -0
  24. maque/api/static/swagger-ui.css +3 -0
  25. maque/cli/__init__.py +1 -0
  26. maque/cli/clean_invisible_chars.py +324 -0
  27. maque/cli/core.py +34 -0
  28. maque/cli/groups/__init__.py +26 -0
  29. maque/cli/groups/config.py +205 -0
  30. maque/cli/groups/data.py +615 -0
  31. maque/cli/groups/doctor.py +259 -0
  32. maque/cli/groups/embedding.py +222 -0
  33. maque/cli/groups/git.py +29 -0
  34. maque/cli/groups/help.py +410 -0
  35. maque/cli/groups/llm.py +223 -0
  36. maque/cli/groups/mcp.py +241 -0
  37. maque/cli/groups/mllm.py +1795 -0
  38. maque/cli/groups/mllm_simple.py +60 -0
  39. maque/cli/groups/quant.py +210 -0
  40. maque/cli/groups/service.py +490 -0
  41. maque/cli/groups/system.py +570 -0
  42. maque/cli/mllm_run.py +1451 -0
  43. maque/cli/script.py +52 -0
  44. maque/cli/tree.py +49 -0
  45. maque/clustering/__init__.py +52 -0
  46. maque/clustering/analyzer.py +347 -0
  47. maque/clustering/clusterers.py +464 -0
  48. maque/clustering/sampler.py +134 -0
  49. maque/clustering/visualizer.py +205 -0
  50. maque/constant.py +13 -0
  51. maque/core.py +133 -0
  52. maque/cv/__init__.py +1 -0
  53. maque/cv/image.py +219 -0
  54. maque/cv/utils.py +68 -0
  55. maque/cv/video/__init__.py +3 -0
  56. maque/cv/video/keyframe_extractor.py +368 -0
  57. maque/embedding/__init__.py +43 -0
  58. maque/embedding/base.py +56 -0
  59. maque/embedding/multimodal.py +308 -0
  60. maque/embedding/server.py +523 -0
  61. maque/embedding/text.py +311 -0
  62. maque/git/__init__.py +24 -0
  63. maque/git/pure_git.py +912 -0
  64. maque/io/__init__.py +29 -0
  65. maque/io/core.py +38 -0
  66. maque/io/ops.py +194 -0
  67. maque/llm/__init__.py +111 -0
  68. maque/llm/backend.py +416 -0
  69. maque/llm/base.py +411 -0
  70. maque/llm/server.py +366 -0
  71. maque/mcp_server.py +1096 -0
  72. maque/mllm_data_processor_pipeline/__init__.py +17 -0
  73. maque/mllm_data_processor_pipeline/core.py +341 -0
  74. maque/mllm_data_processor_pipeline/example.py +291 -0
  75. maque/mllm_data_processor_pipeline/steps/__init__.py +56 -0
  76. maque/mllm_data_processor_pipeline/steps/data_alignment.py +267 -0
  77. maque/mllm_data_processor_pipeline/steps/data_loader.py +172 -0
  78. maque/mllm_data_processor_pipeline/steps/data_validation.py +304 -0
  79. maque/mllm_data_processor_pipeline/steps/format_conversion.py +411 -0
  80. maque/mllm_data_processor_pipeline/steps/mllm_annotation.py +331 -0
  81. maque/mllm_data_processor_pipeline/steps/mllm_refinement.py +446 -0
  82. maque/mllm_data_processor_pipeline/steps/result_validation.py +501 -0
  83. maque/mllm_data_processor_pipeline/web_app.py +317 -0
  84. maque/nlp/__init__.py +14 -0
  85. maque/nlp/ngram.py +9 -0
  86. maque/nlp/parser.py +63 -0
  87. maque/nlp/risk_matcher.py +543 -0
  88. maque/nlp/sentence_splitter.py +202 -0
  89. maque/nlp/simple_tradition_cvt.py +31 -0
  90. maque/performance/__init__.py +21 -0
  91. maque/performance/_measure_time.py +70 -0
  92. maque/performance/_profiler.py +367 -0
  93. maque/performance/_stat_memory.py +51 -0
  94. maque/pipelines/__init__.py +15 -0
  95. maque/pipelines/clustering.py +252 -0
  96. maque/quantization/__init__.py +42 -0
  97. maque/quantization/auto_round.py +120 -0
  98. maque/quantization/base.py +145 -0
  99. maque/quantization/bitsandbytes.py +127 -0
  100. maque/quantization/llm_compressor.py +102 -0
  101. maque/retriever/__init__.py +35 -0
  102. maque/retriever/chroma.py +654 -0
  103. maque/retriever/document.py +140 -0
  104. maque/retriever/milvus.py +1140 -0
  105. maque/table_ops/__init__.py +1 -0
  106. maque/table_ops/core.py +133 -0
  107. maque/table_viewer/__init__.py +4 -0
  108. maque/table_viewer/download_assets.py +57 -0
  109. maque/table_viewer/server.py +698 -0
  110. maque/table_viewer/static/element-plus-icons.js +5791 -0
  111. maque/table_viewer/static/element-plus.css +1 -0
  112. maque/table_viewer/static/element-plus.js +65236 -0
  113. maque/table_viewer/static/main.css +268 -0
  114. maque/table_viewer/static/main.js +669 -0
  115. maque/table_viewer/static/vue.global.js +18227 -0
  116. maque/table_viewer/templates/index.html +401 -0
  117. maque/utils/__init__.py +56 -0
  118. maque/utils/color.py +68 -0
  119. maque/utils/color_string.py +45 -0
  120. maque/utils/compress.py +66 -0
  121. maque/utils/constant.py +183 -0
  122. maque/utils/core.py +261 -0
  123. maque/utils/cursor.py +143 -0
  124. maque/utils/distance.py +58 -0
  125. maque/utils/docker.py +96 -0
  126. maque/utils/downloads.py +51 -0
  127. maque/utils/excel_helper.py +542 -0
  128. maque/utils/helper_metrics.py +121 -0
  129. maque/utils/helper_parser.py +168 -0
  130. maque/utils/net.py +64 -0
  131. maque/utils/nvidia_stat.py +140 -0
  132. maque/utils/ops.py +53 -0
  133. maque/utils/packages.py +31 -0
  134. maque/utils/path.py +57 -0
  135. maque/utils/tar.py +260 -0
  136. maque/utils/untar.py +129 -0
  137. maque/web/__init__.py +0 -0
  138. maque/web/image_downloader.py +1410 -0
  139. maque-0.2.1.dist-info/METADATA +450 -0
  140. maque-0.2.1.dist-info/RECORD +143 -0
  141. maque-0.2.1.dist-info/WHEEL +4 -0
  142. maque-0.2.1.dist-info/entry_points.txt +3 -0
  143. maque-0.2.1.dist-info/licenses/LICENSE +21 -0
maque/git/pure_git.py ADDED
@@ -0,0 +1,912 @@
1
+ """
2
+ Pure Git - 纯 Python Git 操作模块
3
+
4
+ 基于 Dulwich 实现,不依赖 git 客户端。
5
+ 提供面向对象的 API 进行 Git 仓库操作。
6
+
7
+ Usage:
8
+ from maque.git import PureGitRepo
9
+
10
+ # 初始化仓库
11
+ repo = PureGitRepo.init('/path/to/repo')
12
+
13
+ # 打开现有仓库
14
+ repo = PureGitRepo.open('/path/to/repo')
15
+
16
+ # 基本操作
17
+ repo.add('.')
18
+ repo.commit('Initial commit')
19
+ print(repo.status())
20
+ print(repo.log())
21
+
22
+ # Rebase
23
+ repo.rebase('main') # rebase 当前分支到 main
24
+ repo.rebase('main', interactive=True) # 交互式 rebase
25
+
26
+ # Stash
27
+ repo.stash_push('WIP: my changes')
28
+ repo.stash_pop()
29
+
30
+ # Cherry-pick / Revert
31
+ repo.cherry_pick('abc1234')
32
+ repo.revert('abc1234')
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import os
37
+ from pathlib import Path
38
+ from typing import Dict, List, Optional, Union, Callable
39
+ from dataclasses import dataclass
40
+
41
+ try:
42
+ from dulwich import porcelain
43
+ from dulwich.repo import Repo
44
+ from dulwich.objects import Commit, Tree
45
+ from dulwich.diff_tree import tree_changes
46
+ DULWICH_AVAILABLE = True
47
+ except ImportError:
48
+ DULWICH_AVAILABLE = False
49
+
50
+
51
+ @dataclass
52
+ class GitStatus:
53
+ """Git 状态信息"""
54
+ staged: List[str] # 已暂存的文件
55
+ unstaged: List[str] # 已修改但未暂存的文件
56
+ untracked: List[str] # 未跟踪的文件
57
+
58
+ def __str__(self):
59
+ lines = []
60
+ if self.staged:
61
+ lines.append("Changes to be committed:")
62
+ for f in self.staged:
63
+ lines.append(f" {f}")
64
+ if self.unstaged:
65
+ lines.append("Changes not staged for commit:")
66
+ for f in self.unstaged:
67
+ lines.append(f" {f}")
68
+ if self.untracked:
69
+ lines.append("Untracked files:")
70
+ for f in self.untracked:
71
+ lines.append(f" {f}")
72
+ if not lines:
73
+ lines.append("Nothing to commit, working tree clean")
74
+ return "\n".join(lines)
75
+
76
+
77
+ @dataclass
78
+ class GitCommitInfo:
79
+ """Git 提交信息"""
80
+ sha: str
81
+ message: str
82
+ author: str
83
+ date: str
84
+
85
+ def __str__(self):
86
+ return f"{self.sha[:7]} {self.message} ({self.author}, {self.date})"
87
+
88
+
89
+ @dataclass
90
+ class GitStashEntry:
91
+ """Git stash 条目"""
92
+ index: int
93
+ message: str
94
+ commit_sha: str
95
+
96
+ def __str__(self):
97
+ return f"stash@{{{self.index}}}: {self.message}"
98
+
99
+
100
+ @dataclass
101
+ class GitBlameEntry:
102
+ """Git blame 条目"""
103
+ commit_sha: str
104
+ author: str
105
+ line_number: int
106
+ content: str
107
+
108
+ def __str__(self):
109
+ return f"{self.commit_sha[:7]} ({self.author}) {self.line_number}: {self.content}"
110
+
111
+
112
+ class PureGitRepo:
113
+ """纯 Python Git 仓库操作类
114
+
115
+ 基于 Dulwich 实现,不依赖 git 客户端。
116
+ """
117
+
118
+ def __init__(self, path: str):
119
+ """初始化仓库对象
120
+
121
+ Args:
122
+ path: 仓库路径
123
+ """
124
+ if not DULWICH_AVAILABLE:
125
+ raise ImportError("dulwich 未安装,请运行: pip install dulwich")
126
+
127
+ self.path = Path(path).resolve()
128
+ self._repo: Optional[Repo] = None
129
+ self._author_name: Optional[str] = None
130
+ self._author_email: Optional[str] = None
131
+
132
+ @property
133
+ def repo(self) -> Repo:
134
+ """获取 Dulwich Repo 对象"""
135
+ if self._repo is None:
136
+ git_dir = self.path / '.git'
137
+ if git_dir.exists():
138
+ self._repo = Repo(str(self.path))
139
+ else:
140
+ raise ValueError(f"不是有效的 Git 仓库: {self.path}")
141
+ return self._repo
142
+
143
+ def set_author(self, name: str, email: str) -> 'PureGitRepo':
144
+ """设置提交作者信息
145
+
146
+ Args:
147
+ name: 作者名称
148
+ email: 作者邮箱
149
+
150
+ Returns:
151
+ self,支持链式调用
152
+ """
153
+ self._author_name = name
154
+ self._author_email = email
155
+ return self
156
+
157
+ @property
158
+ def _author(self) -> Optional[bytes]:
159
+ """获取作者字符串"""
160
+ if self._author_name and self._author_email:
161
+ return f"{self._author_name} <{self._author_email}>".encode('utf-8')
162
+ return None
163
+
164
+ # =========================================================================
165
+ # 仓库操作
166
+ # =========================================================================
167
+
168
+ @classmethod
169
+ def init(cls, path: str) -> 'PureGitRepo':
170
+ """初始化新仓库
171
+
172
+ Args:
173
+ path: 仓库路径
174
+
175
+ Returns:
176
+ PureGitRepo 实例
177
+ """
178
+ if not DULWICH_AVAILABLE:
179
+ raise ImportError("dulwich 未安装,请运行: pip install dulwich")
180
+
181
+ path = Path(path).resolve()
182
+ path.mkdir(parents=True, exist_ok=True)
183
+ porcelain.init(str(path))
184
+ return cls(str(path))
185
+
186
+ @classmethod
187
+ def clone(cls, url: str, path: str,
188
+ username: str = None, password: str = None) -> 'PureGitRepo':
189
+ """克隆远程仓库
190
+
191
+ Args:
192
+ url: 远程仓库 URL
193
+ path: 本地路径
194
+ username: 用户名(可选)
195
+ password: 密码/Token(可选)
196
+
197
+ Returns:
198
+ PureGitRepo 实例
199
+ """
200
+ if not DULWICH_AVAILABLE:
201
+ raise ImportError("dulwich 未安装,请运行: pip install dulwich")
202
+
203
+ porcelain.clone(url, path, username=username, password=password)
204
+ return cls(path)
205
+
206
+ @classmethod
207
+ def open(cls, path: str = '.') -> 'PureGitRepo':
208
+ """打开现有仓库
209
+
210
+ Args:
211
+ path: 仓库路径,默认为当前目录
212
+
213
+ Returns:
214
+ PureGitRepo 实例
215
+ """
216
+ repo = cls(path)
217
+ # 验证是否是有效仓库
218
+ _ = repo.repo
219
+ return repo
220
+
221
+ # =========================================================================
222
+ # 基础操作
223
+ # =========================================================================
224
+
225
+ def add(self, paths: Union[str, List[str]] = '.') -> 'PureGitRepo':
226
+ """添加文件到暂存区
227
+
228
+ Args:
229
+ paths: 文件路径或路径列表,'.' 表示所有文件
230
+
231
+ Returns:
232
+ self,支持链式调用
233
+ """
234
+ if isinstance(paths, str):
235
+ if paths == '.':
236
+ # 添加所有文件
237
+ porcelain.add(str(self.path))
238
+ else:
239
+ porcelain.add(str(self.path), [paths])
240
+ else:
241
+ porcelain.add(str(self.path), paths)
242
+ return self
243
+
244
+ def commit(self, message: str, author: str = None) -> str:
245
+ """提交更改
246
+
247
+ Args:
248
+ message: 提交信息
249
+ author: 作者(格式: "Name <email>"),可选
250
+
251
+ Returns:
252
+ 提交的 SHA
253
+ """
254
+ if author:
255
+ author_bytes = author.encode('utf-8')
256
+ else:
257
+ author_bytes = self._author
258
+
259
+ sha = porcelain.commit(
260
+ str(self.path),
261
+ message.encode('utf-8'),
262
+ author=author_bytes,
263
+ committer=author_bytes
264
+ )
265
+ return sha.decode('utf-8') if isinstance(sha, bytes) else str(sha)
266
+
267
+ def status(self) -> GitStatus:
268
+ """获取仓库状态
269
+
270
+ Returns:
271
+ GitStatus 对象
272
+ """
273
+ result = porcelain.status(str(self.path))
274
+
275
+ staged = []
276
+ unstaged = []
277
+ untracked = []
278
+
279
+ # result 是一个 GitStatus namedtuple
280
+ if hasattr(result, 'staged'):
281
+ # staged 是一个 dict: {'add': [...], 'modify': [...], 'delete': [...]}
282
+ for action, files in result.staged.items():
283
+ for f in files:
284
+ if isinstance(f, bytes):
285
+ f = f.decode('utf-8')
286
+ staged.append(f"{action}: {f}")
287
+
288
+ if hasattr(result, 'unstaged'):
289
+ for f in result.unstaged:
290
+ if isinstance(f, bytes):
291
+ f = f.decode('utf-8')
292
+ unstaged.append(f)
293
+
294
+ if hasattr(result, 'untracked'):
295
+ for f in result.untracked:
296
+ if isinstance(f, bytes):
297
+ f = f.decode('utf-8')
298
+ untracked.append(f)
299
+
300
+ return GitStatus(staged=staged, unstaged=unstaged, untracked=untracked)
301
+
302
+ def log(self, max_entries: int = 10) -> List[GitCommitInfo]:
303
+ """获取提交日志
304
+
305
+ Args:
306
+ max_entries: 最大条目数
307
+
308
+ Returns:
309
+ GitCommitInfo 列表
310
+ """
311
+ from datetime import datetime
312
+ from dulwich.walk import Walker
313
+
314
+ commits = []
315
+ try:
316
+ # 使用底层 Walker API 获取提交历史
317
+ walker = Walker(self.repo.object_store, [self.repo.head()])
318
+ count = 0
319
+ for entry in walker:
320
+ if count >= max_entries:
321
+ break
322
+ commit = entry.commit
323
+ # commit.id 是 hex 字符串的 bytes,直接 decode
324
+ sha = commit.id.decode('utf-8') if isinstance(commit.id, bytes) else str(commit.id)
325
+ message = commit.message.decode('utf-8').strip().split('\n')[0]
326
+ author = commit.author.decode('utf-8')
327
+ date = datetime.fromtimestamp(commit.author_time).strftime('%Y-%m-%d %H:%M')
328
+ commits.append(GitCommitInfo(sha=sha, message=message, author=author, date=date))
329
+ count += 1
330
+ except Exception:
331
+ # 回退到 porcelain.log(会输出到 stdout)
332
+ for entry in porcelain.log(str(self.path), max_entries=max_entries):
333
+ if isinstance(entry, Commit):
334
+ sha = entry.id.decode('utf-8') if isinstance(entry.id, bytes) else str(entry.id)
335
+ message = entry.message.decode('utf-8').strip().split('\n')[0]
336
+ author = entry.author.decode('utf-8')
337
+ date = datetime.fromtimestamp(entry.author_time).strftime('%Y-%m-%d %H:%M')
338
+ commits.append(GitCommitInfo(sha=sha, message=message, author=author, date=date))
339
+ return commits
340
+
341
+ # =========================================================================
342
+ # 分支操作
343
+ # =========================================================================
344
+
345
+ @property
346
+ def branches(self) -> List[str]:
347
+ """获取所有分支列表"""
348
+ refs = porcelain.branch_list(str(self.path))
349
+ return [ref.decode('utf-8') if isinstance(ref, bytes) else ref for ref in refs]
350
+
351
+ @property
352
+ def current_branch(self) -> str:
353
+ """获取当前分支名"""
354
+ try:
355
+ head_ref = self.repo.refs.read_ref(b'HEAD')
356
+ if head_ref and head_ref.startswith(b'ref: refs/heads/'):
357
+ return head_ref[16:].decode('utf-8')
358
+ # 如果是 detached HEAD
359
+ with open(self.path / '.git' / 'HEAD', 'r') as f:
360
+ content = f.read().strip()
361
+ if content.startswith('ref: refs/heads/'):
362
+ return content[16:]
363
+ return content[:7] # detached HEAD, 返回短 SHA
364
+ except Exception:
365
+ return 'HEAD'
366
+
367
+ def create_branch(self, name: str, ref: str = 'HEAD') -> 'PureGitRepo':
368
+ """创建新分支
369
+
370
+ Args:
371
+ name: 分支名
372
+ ref: 起始引用,默认为 HEAD
373
+
374
+ Returns:
375
+ self,支持链式调用
376
+ """
377
+ porcelain.branch_create(str(self.path), name)
378
+ return self
379
+
380
+ def checkout(self, branch: str) -> 'PureGitRepo':
381
+ """切换分支
382
+
383
+ Args:
384
+ branch: 分支名
385
+
386
+ Returns:
387
+ self,支持链式调用
388
+ """
389
+ porcelain.checkout_branch(str(self.path), branch)
390
+ return self
391
+
392
+ def merge(self, branch: str) -> 'PureGitRepo':
393
+ """合并分支
394
+
395
+ Args:
396
+ branch: 要合并的分支名
397
+
398
+ Returns:
399
+ self,支持链式调用
400
+ """
401
+ # Dulwich 的 merge 需要分支引用
402
+ if not branch.startswith('refs/'):
403
+ branch = f'refs/heads/{branch}'
404
+ porcelain.merge(str(self.path), branch.encode('utf-8'))
405
+ return self
406
+
407
+ # =========================================================================
408
+ # 远程操作
409
+ # =========================================================================
410
+
411
+ def fetch(self, remote: str = 'origin',
412
+ username: str = None, password: str = None) -> 'PureGitRepo':
413
+ """拉取远程更新(不合并)
414
+
415
+ Args:
416
+ remote: 远程仓库名
417
+ username: 用户名
418
+ password: 密码/Token
419
+
420
+ Returns:
421
+ self,支持链式调用
422
+ """
423
+ porcelain.fetch(str(self.path), remote, username=username, password=password)
424
+ return self
425
+
426
+ def pull(self, remote: str = 'origin',
427
+ username: str = None, password: str = None) -> 'PureGitRepo':
428
+ """拉取并合并远程更新
429
+
430
+ Args:
431
+ remote: 远程仓库名
432
+ username: 用户名
433
+ password: 密码/Token
434
+
435
+ Returns:
436
+ self,支持链式调用
437
+ """
438
+ porcelain.pull(str(self.path), remote, username=username, password=password)
439
+ return self
440
+
441
+ def push(self, remote: str = 'origin', branch: str = None,
442
+ username: str = None, password: str = None) -> 'PureGitRepo':
443
+ """推送到远程仓库
444
+
445
+ Args:
446
+ remote: 远程仓库名
447
+ branch: 分支名,默认为当前分支
448
+ username: 用户名
449
+ password: 密码/Token
450
+
451
+ Returns:
452
+ self,支持链式调用
453
+ """
454
+ if branch is None:
455
+ branch = self.current_branch
456
+ ref = f'refs/heads/{branch}'
457
+ porcelain.push(str(self.path), remote, ref, username=username, password=password)
458
+ return self
459
+
460
+ def remote_add(self, name: str, url: str) -> 'PureGitRepo':
461
+ """添加远程仓库
462
+
463
+ Args:
464
+ name: 远程仓库名
465
+ url: 远程仓库 URL
466
+
467
+ Returns:
468
+ self,支持链式调用
469
+ """
470
+ porcelain.remote_add(str(self.path), name, url)
471
+ return self
472
+
473
+ @property
474
+ def remotes(self) -> Dict[str, str]:
475
+ """获取远程仓库列表"""
476
+ config = self.repo.get_config()
477
+ remotes = {}
478
+ for section in config.sections():
479
+ if section[0] == b'remote':
480
+ name = section[1].decode('utf-8')
481
+ url = config.get(section, b'url')
482
+ if url:
483
+ remotes[name] = url.decode('utf-8')
484
+ return remotes
485
+
486
+ # =========================================================================
487
+ # 高级操作
488
+ # =========================================================================
489
+
490
+ def diff(self, ref1: str = None, ref2: str = None) -> str:
491
+ """比较差异
492
+
493
+ Args:
494
+ ref1: 第一个引用(默认为 HEAD)
495
+ ref2: 第二个引用(默认为工作区)
496
+
497
+ Returns:
498
+ diff 输出字符串
499
+ """
500
+ import io
501
+ output = io.BytesIO()
502
+
503
+ if ref1 is None and ref2 is None:
504
+ # 比较 HEAD 和工作区
505
+ porcelain.diff_tree(str(self.path), outstream=output)
506
+ else:
507
+ # 比较两个引用
508
+ porcelain.diff_tree(str(self.path), ref1, ref2, outstream=output)
509
+
510
+ return output.getvalue().decode('utf-8', errors='replace')
511
+
512
+ def reset(self, ref: str = 'HEAD', mode: str = 'mixed') -> 'PureGitRepo':
513
+ """重置到指定引用
514
+
515
+ Args:
516
+ ref: 目标引用
517
+ mode: 重置模式 ('soft', 'mixed', 'hard')
518
+
519
+ Returns:
520
+ self,支持链式调用
521
+ """
522
+ if mode == 'hard':
523
+ porcelain.reset(str(self.path), 'hard', ref)
524
+ elif mode == 'soft':
525
+ porcelain.reset(str(self.path), 'soft', ref)
526
+ else: # mixed
527
+ porcelain.reset(str(self.path), 'mixed', ref)
528
+ return self
529
+
530
+ def tag(self, name: str, message: str = None) -> 'PureGitRepo':
531
+ """创建标签
532
+
533
+ Args:
534
+ name: 标签名
535
+ message: 标签消息(可选,创建 annotated tag)
536
+
537
+ Returns:
538
+ self,支持链式调用
539
+ """
540
+ if message:
541
+ porcelain.tag_create(str(self.path), name, message=message.encode('utf-8'))
542
+ else:
543
+ porcelain.tag_create(str(self.path), name)
544
+ return self
545
+
546
+ @property
547
+ def tags(self) -> List[str]:
548
+ """获取所有标签列表"""
549
+ tags = []
550
+ for ref in self.repo.refs.keys():
551
+ if isinstance(ref, bytes):
552
+ ref = ref.decode('utf-8')
553
+ if ref.startswith('refs/tags/'):
554
+ tags.append(ref[10:])
555
+ return tags
556
+
557
+ def delete_tag(self, name: str) -> 'PureGitRepo':
558
+ """删除标签
559
+
560
+ Args:
561
+ name: 标签名
562
+
563
+ Returns:
564
+ self,支持链式调用
565
+ """
566
+ porcelain.tag_delete(str(self.path), name)
567
+ return self
568
+
569
+ def delete_branch(self, name: str, force: bool = False) -> 'PureGitRepo':
570
+ """删除分支
571
+
572
+ Args:
573
+ name: 分支名
574
+ force: 强制删除(即使未合并)
575
+
576
+ Returns:
577
+ self,支持链式调用
578
+ """
579
+ porcelain.branch_delete(str(self.path), name, force=force)
580
+ return self
581
+
582
+ # =========================================================================
583
+ # Rebase 操作
584
+ # =========================================================================
585
+
586
+ def rebase(
587
+ self,
588
+ upstream: str,
589
+ onto: str = None,
590
+ branch: str = None,
591
+ interactive: bool = False,
592
+ editor_callback: Callable[[bytes], bytes] = None,
593
+ ) -> List[str]:
594
+ """Rebase 当前分支到指定上游
595
+
596
+ Args:
597
+ upstream: 上游分支/提交
598
+ onto: 目标提交(默认与 upstream 相同)
599
+ branch: 要 rebase 的分支(默认当前分支)
600
+ interactive: 是否交互式 rebase
601
+ editor_callback: 交互式 rebase 时的编辑器回调
602
+
603
+ Returns:
604
+ 新创建的提交 SHA 列表
605
+
606
+ Raises:
607
+ Exception: rebase 失败或发生冲突
608
+ """
609
+ upstream_bytes = upstream.encode('utf-8')
610
+ onto_bytes = onto.encode('utf-8') if onto else None
611
+ branch_bytes = branch.encode('utf-8') if branch else None
612
+
613
+ result = porcelain.rebase(
614
+ str(self.path),
615
+ upstream_bytes,
616
+ onto=onto_bytes,
617
+ branch=branch_bytes,
618
+ interactive=interactive,
619
+ )
620
+ return [sha.decode('utf-8') if isinstance(sha, bytes) else str(sha) for sha in result]
621
+
622
+ def rebase_continue(self, interactive: bool = False) -> List[str]:
623
+ """继续 rebase
624
+
625
+ Args:
626
+ interactive: 是否交互式 rebase
627
+
628
+ Returns:
629
+ 新创建的提交 SHA 列表
630
+ """
631
+ result = porcelain.rebase(
632
+ str(self.path),
633
+ b'', # upstream 不需要
634
+ continue_rebase=True,
635
+ interactive=interactive,
636
+ )
637
+ return [sha.decode('utf-8') if isinstance(sha, bytes) else str(sha) for sha in result]
638
+
639
+ def rebase_abort(self) -> 'PureGitRepo':
640
+ """中止 rebase
641
+
642
+ Returns:
643
+ self,支持链式调用
644
+ """
645
+ porcelain.rebase(str(self.path), b'', abort=True)
646
+ return self
647
+
648
+ def rebase_skip(self) -> List[str]:
649
+ """跳过当前提交并继续 rebase
650
+
651
+ Returns:
652
+ 新创建的提交 SHA 列表
653
+ """
654
+ result = porcelain.rebase(str(self.path), b'', skip=True)
655
+ return [sha.decode('utf-8') if isinstance(sha, bytes) else str(sha) for sha in result]
656
+
657
+ def is_rebasing(self) -> bool:
658
+ """检查是否正在进行 rebase
659
+
660
+ Returns:
661
+ True 如果正在 rebase
662
+ """
663
+ rebase_merge = self.path / '.git' / 'rebase-merge'
664
+ rebase_apply = self.path / '.git' / 'rebase-apply'
665
+ return rebase_merge.exists() or rebase_apply.exists()
666
+
667
+ # =========================================================================
668
+ # Stash 操作
669
+ # =========================================================================
670
+
671
+ def stash_push(self, message: str = None, include_untracked: bool = False) -> str:
672
+ """保存当前工作区到 stash
673
+
674
+ Args:
675
+ message: stash 消息
676
+ include_untracked: 是否包含未跟踪的文件
677
+
678
+ Returns:
679
+ stash 提交的 SHA
680
+ """
681
+ result = porcelain.stash_push(
682
+ str(self.path),
683
+ message=message.encode('utf-8') if message else None,
684
+ include_untracked=include_untracked,
685
+ )
686
+ return result.decode('utf-8') if isinstance(result, bytes) else str(result)
687
+
688
+ def stash_pop(self, index: int = 0) -> 'PureGitRepo':
689
+ """恢复 stash 并删除
690
+
691
+ Args:
692
+ index: stash 索引(默认 0,即最新)
693
+
694
+ Returns:
695
+ self,支持链式调用
696
+ """
697
+ porcelain.stash_pop(str(self.path), index)
698
+ return self
699
+
700
+ def stash_list(self) -> List[GitStashEntry]:
701
+ """列出所有 stash
702
+
703
+ Returns:
704
+ GitStashEntry 列表
705
+ """
706
+ entries = []
707
+ result = porcelain.stash_list(str(self.path))
708
+ for idx, (sha, msg) in enumerate(result):
709
+ sha_str = sha.decode('utf-8') if isinstance(sha, bytes) else str(sha)
710
+ msg_str = msg.decode('utf-8') if isinstance(msg, bytes) else str(msg)
711
+ entries.append(GitStashEntry(index=idx, message=msg_str, commit_sha=sha_str))
712
+ return entries
713
+
714
+ def stash_drop(self, index: int = 0) -> 'PureGitRepo':
715
+ """删除 stash(不恢复)
716
+
717
+ Args:
718
+ index: stash 索引
719
+
720
+ Returns:
721
+ self,支持链式调用
722
+ """
723
+ porcelain.stash_drop(str(self.path), index)
724
+ return self
725
+
726
+ # =========================================================================
727
+ # Cherry-pick / Revert
728
+ # =========================================================================
729
+
730
+ def cherry_pick(self, commit: str) -> str:
731
+ """Cherry-pick 指定提交
732
+
733
+ Args:
734
+ commit: 提交 SHA 或引用
735
+
736
+ Returns:
737
+ 新提交的 SHA
738
+ """
739
+ commit_bytes = commit.encode('utf-8')
740
+ result = porcelain.cherry_pick(str(self.path), commit_bytes)
741
+ return result.decode('utf-8') if isinstance(result, bytes) else str(result)
742
+
743
+ def revert(self, commit: str) -> str:
744
+ """撤销指定提交(创建新提交)
745
+
746
+ Args:
747
+ commit: 提交 SHA 或引用
748
+
749
+ Returns:
750
+ 新提交的 SHA
751
+ """
752
+ commit_bytes = commit.encode('utf-8')
753
+ result = porcelain.revert(str(self.path), commit_bytes)
754
+ return result.decode('utf-8') if isinstance(result, bytes) else str(result)
755
+
756
+ # =========================================================================
757
+ # 文件操作
758
+ # =========================================================================
759
+
760
+ def clean(self, dry_run: bool = False) -> List[str]:
761
+ """清理未跟踪的文件
762
+
763
+ Args:
764
+ dry_run: 仅显示将要删除的文件,不实际删除
765
+
766
+ Returns:
767
+ 被删除(或将要删除)的文件列表
768
+ """
769
+ result = porcelain.clean(str(self.path), dry_run=dry_run)
770
+ return [f.decode('utf-8') if isinstance(f, bytes) else str(f) for f in result]
771
+
772
+ def rm(self, paths: Union[str, List[str]], cached: bool = False) -> 'PureGitRepo':
773
+ """从仓库中移除文件
774
+
775
+ Args:
776
+ paths: 文件路径或路径列表
777
+ cached: 仅从索引中移除,保留工作区文件
778
+
779
+ Returns:
780
+ self,支持链式调用
781
+ """
782
+ if isinstance(paths, str):
783
+ paths = [paths]
784
+ porcelain.rm(str(self.path), paths, cached=cached)
785
+ return self
786
+
787
+ def mv(self, src: str, dst: str) -> 'PureGitRepo':
788
+ """移动/重命名文件
789
+
790
+ Args:
791
+ src: 源路径
792
+ dst: 目标路径
793
+
794
+ Returns:
795
+ self,支持链式调用
796
+ """
797
+ porcelain.mv(str(self.path), [src], dst)
798
+ return self
799
+
800
+ # =========================================================================
801
+ # 查询操作
802
+ # =========================================================================
803
+
804
+ def blame(self, path: str) -> List[GitBlameEntry]:
805
+ """获取文件的 blame 信息
806
+
807
+ Args:
808
+ path: 文件路径
809
+
810
+ Returns:
811
+ GitBlameEntry 列表
812
+ """
813
+ entries = []
814
+ result = porcelain.blame(str(self.path), path)
815
+ for line_num, (commit, line_content) in enumerate(result, 1):
816
+ if commit:
817
+ sha = commit.id.decode('utf-8') if isinstance(commit.id, bytes) else str(commit.id)
818
+ author = commit.author.decode('utf-8') if isinstance(commit.author, bytes) else str(commit.author)
819
+ # 提取作者名(去掉 email)
820
+ if '<' in author:
821
+ author = author.split('<')[0].strip()
822
+ else:
823
+ sha = '00000000'
824
+ author = 'Not Committed'
825
+ content = line_content.decode('utf-8') if isinstance(line_content, bytes) else str(line_content)
826
+ entries.append(GitBlameEntry(
827
+ commit_sha=sha,
828
+ author=author,
829
+ line_number=line_num,
830
+ content=content.rstrip('\n'),
831
+ ))
832
+ return entries
833
+
834
+ def show(self, ref: str = 'HEAD') -> str:
835
+ """显示提交或对象的内容
836
+
837
+ Args:
838
+ ref: 引用(默认 HEAD)
839
+
840
+ Returns:
841
+ 对象内容字符串
842
+ """
843
+ import io
844
+ output = io.BytesIO()
845
+ porcelain.show(str(self.path), ref, outstream=output)
846
+ return output.getvalue().decode('utf-8', errors='replace')
847
+
848
+ def ls_files(self, stage: bool = False) -> List[str]:
849
+ """列出索引中的文件
850
+
851
+ Args:
852
+ stage: 是否显示暂存状态
853
+
854
+ Returns:
855
+ 文件列表
856
+ """
857
+ result = porcelain.ls_files(str(self.path))
858
+ return [f.decode('utf-8') if isinstance(f, bytes) else str(f) for f in result]
859
+
860
+ def is_ancestor(self, ancestor: str, descendant: str) -> bool:
861
+ """检查一个提交是否是另一个的祖先
862
+
863
+ Args:
864
+ ancestor: 祖先提交
865
+ descendant: 后代提交
866
+
867
+ Returns:
868
+ True 如果 ancestor 是 descendant 的祖先
869
+ """
870
+ return porcelain.is_ancestor(
871
+ str(self.path),
872
+ ancestor.encode('utf-8'),
873
+ descendant.encode('utf-8'),
874
+ )
875
+
876
+ def merge_base(self, commit1: str, commit2: str) -> str:
877
+ """查找两个提交的公共祖先
878
+
879
+ Args:
880
+ commit1: 第一个提交
881
+ commit2: 第二个提交
882
+
883
+ Returns:
884
+ 公共祖先的 SHA
885
+ """
886
+ result = porcelain.merge_base(
887
+ str(self.path),
888
+ [commit1.encode('utf-8'), commit2.encode('utf-8')],
889
+ )
890
+ return result.decode('utf-8') if isinstance(result, bytes) else str(result)
891
+
892
+ def describe(self, ref: str = 'HEAD') -> str:
893
+ """描述提交(使用最近的标签)
894
+
895
+ Args:
896
+ ref: 引用
897
+
898
+ Returns:
899
+ 描述字符串(如 v1.0.0-5-gabc1234)
900
+ """
901
+ result = porcelain.describe(str(self.path), ref)
902
+ return result.decode('utf-8') if isinstance(result, bytes) else str(result)
903
+
904
+ # =========================================================================
905
+ # 辅助方法
906
+ # =========================================================================
907
+
908
+ def __repr__(self):
909
+ return f"PureGitRepo('{self.path}')"
910
+
911
+ def __str__(self):
912
+ return f"Git repo at {self.path} (branch: {self.current_branch})"