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 對象。
這與文件系統中的目錄結構非常相似:一個目錄包含文件和子目錄,子目錄又可以包含更多文件和子目錄,形成一個深層的樹狀結構。