GitPython08-源码解读
GitPython08-源码解读
1-核心知识
- 1)gitPython核心代码很多都是对git命令返回的结果进行解析,在此补充git命令的返回内容
- 2)git ls-tree -> 查看某个提交或分支所对应的目录树
- 3)源码中Tree对应的业务逻辑 -> 获取git ls-tree返回到contents中返回(比较绕的一点就是懒加载,借助烘焙标签)
- 4)git rev-list ->按拓扑顺序(或时间顺序)列出提交对象的 SHA-1 哈希值,用来展示用户的commit信息
- 5)源码中Commit对应的业务逻辑 -> 从git rev-list中获取提交的信息【提交人等信息】+【Diff文件变动】
- 6)git diff -> 获取git文件变动内容
- 7)源码中Diff对应的业务逻辑 -> Commit使用git diff命令结果转化为Diff对象
- 8) LazyMixin是为了属性的懒读取,只要同一个对象已经读取过了就直接返回,没有才读取
- 9)LazyMixin有两个实现类:Tree和Commit,Diff不是LazyMixin的实现类
2-参考网址
- Git的使用教程
3-上手实操
1-git ls-tree命令
git ls-tree
是一个底层(plumbing)命令,用来查看某个 Git 对象(通常是树对象 tree object)的内容。它最常见的用途是查看某个提交或分支所对应的目录树(即文件和子目录的列表及其对应的 blob/tree 对象哈希)。
📌 基本语法
git ls-tree [-d] [-r] [-t] [-l] [--name-only] <tree-ish> [path...]
<tree-ish>
:可以是分支名、tag、commit SHA、HEAD 等。[path...]
:可选,限制输出的路径。
🔍 常用选项
选项 | 说明 |
---|---|
-r | 递归列出所有子目录的内容 |
-d | 只显示目录(tree 对象) |
-l | 显示对象大小 |
--name-only | 只显示文件名(不显示模式、类型、哈希) |
✅ 实际案例
假设你有一个 Git 仓库,结构如下:
my-project/
├── README.md
├── src/
│ └── main.py
└── docs/└── guide.txt
1. 查看当前分支根目录的内容(非递归)
git ls-tree HEAD
输出示例:
100644 blob 3b18e51... README.md
040000 tree 9f3e2a1... docs
040000 tree 1a2b3c4... src
说明:
100644
是普通文件模式;040000
是目录;blob
是文件对象,tree
是目录对象;- 后面是对象哈希和文件名。
2. 递归查看整个目录树(包括子目录)
git ls-tree -r HEAD
输出示例:
100644 blob 3b18e51... README.md
100644 blob 5f6a7b8... docs/guide.txt
100644 blob 2c3d4e5... src/main.py
3. 只查看目录(不显示文件)
git ls-tree -d HEAD
输出示例:
040000 tree 9f3e2a1... docs
040000 tree 1a2b3c4... src
4. 查看某个子目录的内容
git ls-tree HEAD src
输出示例:
100644 blob 2c3d4e5... src/main.py
5. 只显示文件名(常用于脚本处理)
git ls-tree --name-only HEAD
输出示例:
README.md
docs
src
6. 查看某个特定提交(非 HEAD)的目录结构
git ls-tree -r a1b2c3d
其中 a1b2c3d
是某个 commit 的哈希。
🧠 补充说明
git ls-tree
不会显示.gitignore
中忽略的文件,因为它只显示已提交的对象。- 如果你想查看某个路径的历史变化,应该使用
git log -- path
而不是ls-tree
。
📌 小结速查表
命令 | 说明 |
---|---|
git ls-tree HEAD | 查看当前分支根目录的内容(非递归) |
git ls-tree -r HEAD | 递归查看整个目录树(包括子目录) |
git ls-tree -d HEAD | 只查看目录(不显示文件) |
git ls-tree HEAD src | 查看某个子目录的内容 |
git ls-tree --name-only HEAD | 只显示文件名(常用于脚本处理) |
git ls-tree -r a1b2c3d | g查看某个特定提交(非 HEAD)的目录结构 |
如需进一步处理这些文件(比如导出或打包),可以结合 git archive
或 git cat-file
使用。
2-源码中Tree对应的业务逻辑
100644 blob 6a9d72ad2b4e8b7288973a8bb1ead8a529f84190 .gitignore
040000 tree 27f560f217fe016818401537ed55a5c7430e9588 test
100644 blob acf4a637c96293ceea1f7e78fd1dafbfb517cc2c tree.py
100644 blob a10a7b41e4314ea4d87557af65aa303337c225d3 utils.py
160000 commit d35b34c6e931b9da8f6941007a92c9c9a9b0141a bar
目的是:直接new Tree(repo)先不烘焙,调用构造方法才烘焙(烘焙就是把属性set进去)
def construct_initialize(self, repo, id, text):self.repo = repo # 保存仓库self.id = id # 保存 SHA-1self.contents = [] # 子对象列表self.__baked__ = False # 标记还未烘焙(延迟加载用)# 逐行解析(烘焙:就是遍历text把文件的名称全部设置到self.contents中)for line in text.splitlines():self.contents.append(self.content_from_string(self.repo, line))# 过滤掉解析失败的 Noneself.contents = [c for c in self.contents if c is not None]# 标记为已烘焙(防止重复解析)self.__bake_it__()return self
3-git rev-list命令
git rev-list
是一个非常底层且强大的 Git 命令,用于按拓扑顺序(或时间顺序)列出提交对象的 SHA-1 哈希值。它是许多高级命令(如 git log
, git bisect
, git rebase
)的底层实现基础。
📌 基本语法
git rev-list [options] <commit-ish>... [--] [<path>...]
<commit-ish>
:可以是分支名、tag、commit SHA、HEAD 等。[-- <path>...]
:可选,限制只列出影响指定路径的提交。
✅ 常见用途与案例
1️⃣ 查看某个分支的所有提交(按时间倒序)
git rev-list main
输出示例:
a1b2c3d4e5f6...
7a8b9c0d1e2f...
...
2️⃣ 查看某个分支的提交数量
git rev-list --count main
输出示例:
42
3️⃣ 查看两个分支之间的差异提交(main 有但 dev 没有的)
git rev-list main ^dev
或等价:
git rev-list dev..main
输出示例:
a1b2c3d4...
7a8b9c0d...
4️⃣ 查看从某个时间点以来的提交(比如某个 tag 之后)
git rev-list v1.0..HEAD
5️⃣ 查看某个文件的所有历史提交(影响该文件的)
git rev-list HEAD -- src/main.py
6️⃣ 查看某个作者的提交
git rev-list --author="Alice" main
7️⃣ 查看某段时间内的提交
git rev-list --since="2024-01-01" --until="2024-06-01" main
8️⃣ 查看合并提交(merge commits)
git rev-list --merges main
9️⃣ 查看非合并提交(普通提交)
git rev-list --no-merges main
🔟 限制输出数量(比如只看最近的 5 个)
git rev-list -n 5 main
🧠 进阶案例:找出某个 bug 的引入提交(结合 bisect)
git rev-list --bisect main
这会输出一个中间提交,用于二分查找 bug。
✅ 实际应用:配合 xargs
批量处理提交
git rev-list main | head -5 | xargs -I {} git show --oneline {}
输出示例:
a1b2c3d Fix typo in README
7a8b9c0 Add new feature
...
📌 小结速查表
命令 | 说明 |
---|---|
git rev-list main | 列出 main 分支的所有提交 |
git rev-list --count main | 统计提交数量 |
git rev-list dev..main | main 有但 dev 没有的提交 |
git rev-list --author="Alice" | 某作者的所有提交 |
git rev-list --since="2024-01-01" | 某时间之后的提交 |
git rev-list --merges | 只列出合并提交 |
git rev-list HEAD -- file.txt | 列出影响 file.txt 的提交 |
如需进一步分析这些提交(如查看 diff、统计变更行数),可以结合 git log
, git show
, git diff
使用。
4-源码中Commit用法
Commit的作用就是从git rev-list中获取提交的信息【提交人等信息】+【Diff文件变动】
commit 4c8124ffcf4039d292442eeccabdeca5af5c5017
tree 672eca9b7f9e09c22dcb128c283e8c3c8d7697a4
parent 634396b2f541a9f2d58b00be1a07f0c358b999b3
author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700
committer Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700implement Grit#headscommit 634396b2f541a9f2d58b00be1a07f0c358b999b3
tree b35b4bf642d667fdd613eebcfe4e17efd420fb8a
author Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700
committer Tom Preston-Werner <tom@mojombo.com> 1191997100 -0700initial grit setupcommit ab25fd8483882c3bda8a458ad2965d2248654335
tree c20b5ec543bde1e43a931449b196052c06ed8acc
parent 6e64c55896aabb9a7d8e9f8f296f426d21a78c2c
parent 7f874954efb9ba35210445be456c74e037ba6af2
author Tom Preston-Werner <tom@mojombo.com> 1182645538 -0700
committer Tom Preston-Werner <tom@mojombo.com> 1182645538 -0700Merge branch 'site'Some other stuff
- Commiti对象属性
id: 提交的 SHA1 ID
parents: 父提交的 ID 列表(将转换为 Commit 实例)
tree: 对应的树对象 ID(将转换为 Tree 实例)
author: 作者信息字符串
authored_date: 作者提交时间
committer: 提交者信息字符串
committed_date: 提交时间
message: 提交消息的第一行
def __init__(self, repo, **kwargs):LazyMixin.__init__(self)self.repo = repo # 所属的 Git 仓库对象self.id = None # 提交 IDself.tree = None # 树对象self.author = None # 作者self.authored_date = None # 作者提交时间self.committer = None # 提交者self.committed_date = None # 提交时间self.message = None # 提交消息self.parents = None # 父提交列表# 动态设置传入的参数for k, v in kwargs.items():setattr(self, k, v)# 如果提供了 ID,则进一步解析 parents 和 treeif self.id:if 'parents' in kwargs:self.parents = map(lambda p: Commit(repo, **{'id': p}), kwargs['parents'])if 'tree' in kwargs:self.tree = tree.Tree(repo, **{'id': kwargs['tree']})
5-git diff命令
git diff
是日常使用频率最高的 Git 命令之一,用来查看工作区、暂存区、分支之间的差异。它本质上是调用 git diff-files
、git diff-index
、git diff-tree
等底层命令的封装。
📌 基本语法
git diff [<options>] [<commit>] [--] [<path>...]
<commit>
:可以是 commit SHA、分支名、tag 等。[-- <path>...]
:可选,限制只查看某些文件或目录的差异。
✅ 常见场景与案例
1️⃣ 查看工作区与暂存区的差异(默认行为)
git diff
输出示例(简写):
diff --git a/README.md b/README.md
index 3b18e51..7a8b9c0 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@# My Project
+This line is added in working directory
2️⃣ 查看暂存区与最新提交的差异(即准备提交的改动)
git diff --cached # 或 --staged
输出示例:
diff --git a/src/main.py b/src/main.py
index 2c3d4e5..5f6a7b8 100644
--- a/src/main.py
+++ b/src/main.py
@@ -10,6 +10,7 @@ def main():print("Hello")
+ print("World")
3️⃣ 查看两个分支之间的差异
git diff main..dev
或等价:
git diff main dev
输出示例:
diff --git a/src/utils.py b/src/utils.py
new file mode 100644
index 0000000..9f3e2a1
--- /dev/null
+++ b/src/utils.py
@@ -0,0 +1,3 @@
+def helper():
+ pass
4️⃣ 查看某个文件在两个提交之间的差异
git diff HEAD~2 HEAD -- src/main.py
5️⃣ 查看某个文件在工作区与最新提交之间的差异
git diff HEAD -- src/main.py
6️⃣ 查看两个提交之间的所有差异(压缩为一个 diff)
git diff a1b2c3d..7a8b9c0
7️⃣ 查看某次提交引入了哪些改动(与父提交对比)
git diff 7a8b9c0^ 7a8b9c0
8️⃣ 查看合并提交的差异(与两个父提交对比)
git diff -m -c MERGE_COMMIT_SHA
9️⃣ 只看文件名(不显示具体内容)
git diff --name-only
🔟 统计变更行数(增删行数)
git diff --stat
输出示例:
README.md | 3 ++-src/main.py | 1 +2 files changed, 3 insertions(+), 1 deletion(-)
🎯 进阶技巧
✅ 高亮单词级差异(更细粒度)
git diff --word-diff
✅ 生成补丁文件(用于邮件发送或 review)
git diff main..dev > changes.patch
✅ 查看已暂存但未提交的差异(GUI 风格)
git diff --cached --name-only | xargs git diff --cached
📌 小结速查表
命令 | 作用 |
---|---|
git diff | 工作区 vs 暂存区 |
git diff --cached | 暂存区 vs HEAD |
git diff HEAD | 工作区 vs HEAD |
git diff main..dev | 分支 dev vs 分支 main |
git diff HEAD~2 HEAD | 最近两次提交的差异 |
git diff --name-only | 只列出变更文件名 |
git diff --stat | 统计变更行数 |
git diff --word-diff | 单词级差异高亮 |
如需将差异导出为补丁、用于代码审查或 CI 检查,git diff
是最直接可靠的工具。
6-源码中Diff用法
获取git diff命令的返回结果进行打印,README.md文件在return self代码之后,添加了两行文本
git diffdiff --git a/README.md b/README.md
index 43c9850..4bcfc73 100644
--- a/README.md
+++ b/README.md
@@ -140,3 +140,6 @@ git ls-tree HEADself.__bake_it__()return self
+----
+## 5-Diff
+> 获取git diff命令的返回结果进行打印
- 返回Diff对象的属性说明如下
Diff对象属性说明
--------
repo : 仓库对象,用于后续在 Commit 构造时传入
a_path : 旧版本文件路径(a/ 前缀)
b_path : 新版本文件路径(b/ 前缀)
a_commit : 旧版本对应的 Commit 对象;若为新增文件则为 None
b_commit : 新版本对应的 Commit 对象;若为删除文件则为 None
a_mode : 旧版本文件模式(八进制字符串,如 100644)
b_mode : 新版本文件模式(八进制字符串,如 100755)
new_file : True 表示该文件在本次 diff 中被新增
deleted_file: True 表示该文件在本次 diff 中被删除
diff : 该行以下的所有 diff 文本(包括 ---、+++、@@ 等)
4-特殊情况补充
1-git rev-list main执行报错
这个报错的意思是:Git 无法识别你提供的参数 master
,因为它在当前仓库里既找不到叫 master
的分支,也找不到叫 master
的文件或路径。
原因
- 你的仓库默认分支可能不叫
master
现在很多仓库默认分支是main
,而不是master
。 - 你当前目录下可能根本没有
master
分支
你可以用git branch -a
或git branch -r
看看有哪些分支。
解决方法
✅ 1. 查看实际存在的分支
git branch -a
输出示例:
* mainremotes/origin/HEAD -> origin/mainremotes/origin/main
如果你看到的是 main
而不是 master
,那就用 main
代替 master
。
✅ 2. 替换命令
把原来的命令:
git rev-list --branches master
改成:
git rev-list --branches main
或者如果你想列出所有分支的 commit,也可以直接:
git rev-list --branches
✅ 3. 如果你确实需要 master
分支
你可以从远程拉取或创建它:
git checkout -b master origin/master
前提是远程仓库确实有一个叫 master
的分支。
总结
你看到的报错是因为 master
这个分支在当前仓库中不存在。先确认分支名,再替换即可。
2-区分两种git diff两种方式
1-git diff --numstat
# 1-统计文件的改动数量-方式1
git diff --numstat
1 0 README.md
1 0 repo.py
2-git diff --stat
# 2-统计文件的改动数量-方式2
git diff --statREADME.md | 1 +repo.py | 1 +2 files changed, 2 insertions(+)
5-知识总结
1-LazyMixin的实现类
1-LazyMixin类
这个类就提供了一个能力:延迟初始化的属性的加载
- 1)属性写入是【bake】方法,需要每个具体的子类去实现
- 2)属性读取是【getattribute】方法,这里会判断有没有加载过,加载过直接返回,没有重新加载
class LazyMixin(object):lazy_properties = []def __init__(self):self.__baked__ = Falsedef __getattribute__(self, attr):val = object.__getattribute__(self, attr)if val is not None:return valelse:self.__prebake__()return object.__getattribute__(self, attr)def __bake__(self):raise NotImplementedError(" '__bake__' method has not been implemented.")def __prebake__(self):if self.__baked__:returnself.__bake__()self.__baked__ = Truedef __bake_it__(self):self.__baked__ = True
2-Tree实现类
def __bake__(self):# 调用类方法 construct 重新构造 Tree 对象temp = Tree.construct(self.repo, self.id)# 把解析出来的内容列表赋给自身self.contents = temp.contents
3-Commit实现类
def __bake__(self):"""延迟加载:从 Git 仓库中加载完整的提交信息。"""temp = Commit.find_all(self.repo, self.id, **{'max_count': 1})[0]self.parents = temp.parentsself.tree = temp.treeself.author = temp.authorself.authored_date = temp.authored_dateself.committer = temp.committerself.committed_date = temp.committed_dateself.message = temp.message
2-Diff类
Diff类的目标是:把Commit获取的信息转化为Diff对象,并没有直接使用git diff命令来获取仓库的变动内容
- 1)作者认为:展示的diff信息其实是commit的对象信息
- 2)所以git diff原生的命令调用是在Commit中进行实现的
class Diff(object):def __init__(self, repo, a_path, b_path, a_commit, b_commit,a_mode, b_mode, new_file, deleted_file, diff):self.repo = repoself.a_path = a_pathself.b_path = b_path# 如果旧版本为空(全 0 SHA)或传入空值,则置为 Noneif not a_commit or re.search(r'^0{40}$', a_commit):self.a_commit = Noneelse:# 通过 commit.Commit 构造旧版本 Commit 对象self.a_commit = commit.Commit(repo, **{'id': a_commit})# 同理处理新版本if not b_commit or re.search(r'^0{40}$', b_commit):self.b_commit = Noneelse:self.b_commit = commit.Commit(repo, **{'id': b_commit})self.a_mode = a_modeself.b_mode = b_modeself.new_file = new_fileself.deleted_file = deleted_fileself.diff = diff@classmethoddef list_from_string(cls, repo, text):"""将 Git 输出的 diff 字符串解析为 Diff 对象列表。参数----repo : 仓库对象text : Git diff 原始文本(包含一个或多个文件的 diff)返回----diffs : list[Diff]按文件顺序解析得到的 Diff 对象列表"""lines = text.splitlines() # 将 diff 文本按行切分a_mode = Noneb_mode = Nonea_path = Noneb_path = Nonea_commit = Noneb_commit = Nonediffs = [] # 结果列表while lines:# 匹配 diff --git a/xxx b/xxx 行,提取旧/新文件路径m = re.search(r'^diff --git a/(\S+) b/(\S+)$', lines.pop(0))if m:a_path, b_path = m.groups()# 处理纯 mode 变更(无内容变更)if lines and re.search(r'^old mode', lines[0]):m = re.search(r'^old mode (\d+)', lines.pop(0))if m:a_mode, = m.groups()m = re.search(r'^new mode (\d+)', lines.pop(0))if m:b_mode, = m.groups()# 如果下一行还是 diff --git,说明只有 mode 改变if lines and re.search(r'^diff --git', lines[0]):diffs.append(Diff(repo, a_path, b_path,None, None,a_mode, b_mode,False, False, None))continue# 初始化文件状态标志new_file = Falsedeleted_file = False# 处理新增文件if lines and re.search(r'^new file', lines[0]):m = re.search(r'^new file mode (.+)', lines.pop(0))if m:b_mode, = m.groups()a_mode = Nonenew_file = True# 处理删除文件elif lines and re.search(r'^deleted file', lines[0]):m = re.search(r'^deleted file mode (.+)$', lines.pop(0))if m:a_mode, = m.groups()b_mode = Nonedeleted_file = True# 解析 index 行,提取旧/新 blob SHA 与可选的新模式if lines:m = re.search(r'^index ([0-9A-Fa-f]+)\.\.([0-9A-Fa-f]+) ?(.+)?$',lines.pop(0))if m:a_commit, b_commit, b_mode = m.groups()# 去掉可能存在的多余空格if b_mode:b_mode = b_mode.strip()# 收集该文件剩余所有 diff 行,直到遇到下一个 diff --git 或文件结束diff_lines = []while lines and not re.search(r'^diff', lines[0]):diff_lines.append(lines.pop(0))diff = "\n".join(diff_lines)# 将解析结果封装为 Diff 对象并加入列表diffs.append(Diff(repo, a_path, b_path,a_commit, b_commit,a_mode, b_mode,new_file, deleted_file, diff))return diffs
- Commit类中关于git diff的命令调用
@classmethoddef diff(cls, repo, a, b = None, paths = []):if isinstance(b, list):paths = bb = Noneif paths:paths.insert(0, "--")if b:paths.insert(0, b)paths.insert(0, a)text = repo.git.diff(*paths, **{'full_index': True})return diff.Diff.list_from_string(repo, text)
3-Tree类
先思考一个事情,这个Tree返回的数据结构是一个层级很深的树还是一个列表?
根据代码分析,当前的 [Tree](file:///Applications/0ENZO/WorkSpace/Python/20250731_2_GitPython_manual/tree.py#L8-L112) 类实现是一个层级很深的树状结构,而不仅仅是一个列表。让我详细解释:
树状结构分析
1. 树的层次结构
# Tree 类包含一个 contents 属性,这是一个列表
self.contents = [] # 子对象列表# 但这个列表中的每个元素又可能是另一个 Tree 对象
def content_from_string(self, repo, text):# ...if typ == "tree":# 递归创建新的 Tree 对象return Tree(repo, **{'id': id, 'mode': mode, 'name': name})elif typ == "blob":# 或者是文件对象(叶节点)return None # 当前实现返回 None,但正常应该返回 Blob 对象
2. 树的递归特性
- 每个 [Tree](file:///Applications/0ENZO/WorkSpace/Python/20250731_2_GitPython_manual/tree.py#L8-L112) 对象包含一个 [contents](file:///Applications/0ENZO/WorkSpace/Python/20250731_2_GitPython_manual/tree.py#L0-L0) 列表
- [contents](file:///Applications/0ENZO/WorkSpace/Python/20250731_2_GitPython_manual/tree.py#L0-L0) 中的元素可以是:
- 其他 [Tree](file:///Applications/0ENZO/WorkSpace/Python/20250731_2_GitPython_manual/tree.py#L8-L112) 对象(子目录)
- [Blob](file:///Applications/0ENZO/WorkSpace/Python/20250731_2_GitPython_manual/blob.py#L1-L58) 对象(文件)
- 这样就形成了一个递归的树状结构
3. 延迟加载机制
class Tree(LazyMixin):def __bake__(self):# 延迟加载:只有在真正访问时才解析内容temp = Tree.construct(self.repo, self.id)self.contents = temp.contents
这意味着树的结构是按需构建的,只有访问到某个节点时才会加载其子节点。
4. 访问方式
# 可以通过 / 操作符逐层访问
root_tree = Tree.construct(repo, 'master')
sub_tree = root_tree / 'src' # 访问 src 目录
file_obj = sub_tree / 'main.py' # 访问 src/main.py 文件
当前实现的问题
从代码中可以看到一些问题:
-
Blob 对象未实现:
elif typ == "blob":# return blob.Blob(repo, **{'id': id, 'mode': mode, 'name': name})return None # 当前返回 None
-
basename 属性问题:
@property def basename(self):return self.repo.path # 这里返回的是仓库路径,不是当前目录名
结论
当前 Tree 类返回的是一个真正的树状数据结构,具有以下特点:
- 层次性:每个节点可以包含子节点,形成层级结构
- 递归性:目录节点可以包含其他目录节点
- 延迟加载:按需加载节点内容
- 导航性:支持通过 / 操作符进行路径式访问
虽然表面上 contents 是一个列表,但整个数据结构是一个完整的树,因为列表中的元素本身又可以是 Tree 对象。
这与文件系统中的目录结构非常相似:一个目录包含文件和子目录,子目录又可以包含更多文件和子目录,形成一个深层的树状结构。