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.
- maque/__init__.py +30 -0
- maque/__main__.py +926 -0
- maque/ai_platform/__init__.py +0 -0
- maque/ai_platform/crawl.py +45 -0
- maque/ai_platform/metrics.py +258 -0
- maque/ai_platform/nlp_preprocess.py +67 -0
- maque/ai_platform/webpage_screen_shot.py +195 -0
- maque/algorithms/__init__.py +78 -0
- maque/algorithms/bezier.py +15 -0
- maque/algorithms/bktree.py +117 -0
- maque/algorithms/core.py +104 -0
- maque/algorithms/hilbert.py +16 -0
- maque/algorithms/rate_function.py +92 -0
- maque/algorithms/transform.py +27 -0
- maque/algorithms/trie.py +272 -0
- maque/algorithms/utils.py +63 -0
- maque/algorithms/video.py +587 -0
- maque/api/__init__.py +1 -0
- maque/api/common.py +110 -0
- maque/api/fetch.py +26 -0
- maque/api/static/icon.png +0 -0
- maque/api/static/redoc.standalone.js +1782 -0
- maque/api/static/swagger-ui-bundle.js +3 -0
- maque/api/static/swagger-ui.css +3 -0
- maque/cli/__init__.py +1 -0
- maque/cli/clean_invisible_chars.py +324 -0
- maque/cli/core.py +34 -0
- maque/cli/groups/__init__.py +26 -0
- maque/cli/groups/config.py +205 -0
- maque/cli/groups/data.py +615 -0
- maque/cli/groups/doctor.py +259 -0
- maque/cli/groups/embedding.py +222 -0
- maque/cli/groups/git.py +29 -0
- maque/cli/groups/help.py +410 -0
- maque/cli/groups/llm.py +223 -0
- maque/cli/groups/mcp.py +241 -0
- maque/cli/groups/mllm.py +1795 -0
- maque/cli/groups/mllm_simple.py +60 -0
- maque/cli/groups/quant.py +210 -0
- maque/cli/groups/service.py +490 -0
- maque/cli/groups/system.py +570 -0
- maque/cli/mllm_run.py +1451 -0
- maque/cli/script.py +52 -0
- maque/cli/tree.py +49 -0
- maque/clustering/__init__.py +52 -0
- maque/clustering/analyzer.py +347 -0
- maque/clustering/clusterers.py +464 -0
- maque/clustering/sampler.py +134 -0
- maque/clustering/visualizer.py +205 -0
- maque/constant.py +13 -0
- maque/core.py +133 -0
- maque/cv/__init__.py +1 -0
- maque/cv/image.py +219 -0
- maque/cv/utils.py +68 -0
- maque/cv/video/__init__.py +3 -0
- maque/cv/video/keyframe_extractor.py +368 -0
- maque/embedding/__init__.py +43 -0
- maque/embedding/base.py +56 -0
- maque/embedding/multimodal.py +308 -0
- maque/embedding/server.py +523 -0
- maque/embedding/text.py +311 -0
- maque/git/__init__.py +24 -0
- maque/git/pure_git.py +912 -0
- maque/io/__init__.py +29 -0
- maque/io/core.py +38 -0
- maque/io/ops.py +194 -0
- maque/llm/__init__.py +111 -0
- maque/llm/backend.py +416 -0
- maque/llm/base.py +411 -0
- maque/llm/server.py +366 -0
- maque/mcp_server.py +1096 -0
- maque/mllm_data_processor_pipeline/__init__.py +17 -0
- maque/mllm_data_processor_pipeline/core.py +341 -0
- maque/mllm_data_processor_pipeline/example.py +291 -0
- maque/mllm_data_processor_pipeline/steps/__init__.py +56 -0
- maque/mllm_data_processor_pipeline/steps/data_alignment.py +267 -0
- maque/mllm_data_processor_pipeline/steps/data_loader.py +172 -0
- maque/mllm_data_processor_pipeline/steps/data_validation.py +304 -0
- maque/mllm_data_processor_pipeline/steps/format_conversion.py +411 -0
- maque/mllm_data_processor_pipeline/steps/mllm_annotation.py +331 -0
- maque/mllm_data_processor_pipeline/steps/mllm_refinement.py +446 -0
- maque/mllm_data_processor_pipeline/steps/result_validation.py +501 -0
- maque/mllm_data_processor_pipeline/web_app.py +317 -0
- maque/nlp/__init__.py +14 -0
- maque/nlp/ngram.py +9 -0
- maque/nlp/parser.py +63 -0
- maque/nlp/risk_matcher.py +543 -0
- maque/nlp/sentence_splitter.py +202 -0
- maque/nlp/simple_tradition_cvt.py +31 -0
- maque/performance/__init__.py +21 -0
- maque/performance/_measure_time.py +70 -0
- maque/performance/_profiler.py +367 -0
- maque/performance/_stat_memory.py +51 -0
- maque/pipelines/__init__.py +15 -0
- maque/pipelines/clustering.py +252 -0
- maque/quantization/__init__.py +42 -0
- maque/quantization/auto_round.py +120 -0
- maque/quantization/base.py +145 -0
- maque/quantization/bitsandbytes.py +127 -0
- maque/quantization/llm_compressor.py +102 -0
- maque/retriever/__init__.py +35 -0
- maque/retriever/chroma.py +654 -0
- maque/retriever/document.py +140 -0
- maque/retriever/milvus.py +1140 -0
- maque/table_ops/__init__.py +1 -0
- maque/table_ops/core.py +133 -0
- maque/table_viewer/__init__.py +4 -0
- maque/table_viewer/download_assets.py +57 -0
- maque/table_viewer/server.py +698 -0
- maque/table_viewer/static/element-plus-icons.js +5791 -0
- maque/table_viewer/static/element-plus.css +1 -0
- maque/table_viewer/static/element-plus.js +65236 -0
- maque/table_viewer/static/main.css +268 -0
- maque/table_viewer/static/main.js +669 -0
- maque/table_viewer/static/vue.global.js +18227 -0
- maque/table_viewer/templates/index.html +401 -0
- maque/utils/__init__.py +56 -0
- maque/utils/color.py +68 -0
- maque/utils/color_string.py +45 -0
- maque/utils/compress.py +66 -0
- maque/utils/constant.py +183 -0
- maque/utils/core.py +261 -0
- maque/utils/cursor.py +143 -0
- maque/utils/distance.py +58 -0
- maque/utils/docker.py +96 -0
- maque/utils/downloads.py +51 -0
- maque/utils/excel_helper.py +542 -0
- maque/utils/helper_metrics.py +121 -0
- maque/utils/helper_parser.py +168 -0
- maque/utils/net.py +64 -0
- maque/utils/nvidia_stat.py +140 -0
- maque/utils/ops.py +53 -0
- maque/utils/packages.py +31 -0
- maque/utils/path.py +57 -0
- maque/utils/tar.py +260 -0
- maque/utils/untar.py +129 -0
- maque/web/__init__.py +0 -0
- maque/web/image_downloader.py +1410 -0
- maque-0.2.1.dist-info/METADATA +450 -0
- maque-0.2.1.dist-info/RECORD +143 -0
- maque-0.2.1.dist-info/WHEEL +4 -0
- maque-0.2.1.dist-info/entry_points.txt +3 -0
- 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})"
|