Git 內部機制與歷史編輯

最後更新: 2024-03-07 13:17:55 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

Git 儲存不可變物件,並在它們之間移動小型參照。日常工作應使用 porcelain 命令,而儲存模型能說明為何重寫會建立新歷史,以及為何在一段期間內仍能復原。

檢查儲存庫資料庫 #

.git 目錄包含儲存庫本機狀態:

路徑用途
HEAD已簽出的分支符號名稱,或分離的物件 ID
config儲存庫設定
index下一次提交的提議快照
objects/鬆散物件與封裝檔
refs/ 與 packed-refs分支、標籤與遠端追蹤名稱
logs/參照的本機 reflog
hooks/用戶端 hook 範例與已安裝的 hook
modules/已初始化子模組的 Git 目錄

工作樹保存已簽出的檔案。索引記錄候選樹。提交指向一棵樹以及父代 ID 與中繼資料。分支是指向提交的可移動參照,而不是可變的提交容器。

不必手動解碼檔案即可檢查名稱與物件型別:

sh
git rev-parse --git-dir
git show-ref --head
git cat-file -t HEAD
git cat-file -p HEAD
git count-objects -vH

物件模型與內容定址 #

Git 會對標準位元組序列雜湊,以識別物件:

text
<type><space><decimal-size><NUL><content>

SHA-1 儲存庫使用 160 位元物件 ID。新儲存庫可以使用 SHA-256。物件 ID 識別內容與結構;它不編碼時間或序號。

Git 有四種核心物件型別:

  1. blob 儲存不含檔名的檔案位元組。
  2. tree 將路徑名稱與模式對映到 blob 與子樹。
  3. commit 命名根樹、零個或多個父代、作者與提交者身分及時間,以及訊息。
  4. annotated tag 儲存標籤中繼資料並指向另一個物件。

樹項目模式 100644 是一般不可執行檔案,100755 是可執行檔案,120000 是符號連結,160000 是子模組 gitlink。gitlink 指向另一個儲存庫中的提交,而不是嵌入該儲存庫的檔案。

在可丟棄的儲存庫中建立與檢查物件:

sh
printf 'hello\n' | git hash-object -w --stdin
git cat-file -p <object-id>
git ls-tree HEAD
git ls-tree -r HEAD

鬆散物件位於 .git/objects/aa/bb... 下,其中 aa 是十六進位 ID 的第一個位元組。Git 會以 zlib 壓縮其標準表示。git cat-file 比手動加入壓縮標頭或使用臨時解壓縮管線更安全。

了解封裝、差異與可達性 #

大量鬆散物件會增加檔案系統中繼資料操作。git gc 可以將可達物件收集到 .git/objects/pack/ 中的封裝檔。.pack 檔案保存物件表示法,.idx 檔案將物件 ID 對映到封裝偏移,而 .rev 檔案可以加速需要封裝順序的操作。

封裝檔可以將物件相對於其他物件進行差異壓縮。差異是儲存最佳化,而不是 Git 的歷史模型:每個提交仍然命名完整的根樹,Git 可以透過物件圖重建任何被參照的 blob。提交不會只儲存相對於父代的文字差異。

可達性從分支、標籤、遠端追蹤分支與 reflog 等參照開始。垃圾回收會保留可達物件,並可在設定的到期時間後修剪不可達物件。git fsck 會回報連通性,並可以定位不可達物件:

sh
git fsck --full --no-reflogs --unreachable
git cat-file -p <unreachable-object-id>

讀取參照與索引 #

HEAD 通常包含 ref: refs/heads/main。分離的簽出會直接包含物件 ID。鬆散分支名稱是 refs/heads/ 下的檔案,而許多參照可以合併到 packed-refs。

索引不是變更檔案清單。它儲存下一棵樹的路徑、模式、物件 ID 與 stat 快取。合併衝突期間,索引可以為單一路徑攜帶多個階段:

階段意義
1合併基底
2目前分支版本
3其他分支版本

使用以下命令檢查索引項目:

sh
git ls-files --stage
git diff --cached

解決衝突並執行 git add 會將階段 1–3 替換成一個階段 0 項目。

解釋重寫為何會變更 ID #

提交包含其父代物件 ID。變更訊息、樹、作者、提交者、時間戳記或父代,會產生不同的提交 ID。每個後代都必須重新建立,因為其記錄的父代已變更。

當重寫能改善歷史時,可以自由重寫本機未發布工作。重寫共用分支前要先協調。強制更新可能捨棄協作者新的遠端提交;需要強制推送時使用 --force-with-lease:

sh
git push --force-with-lease origin feature/topic

修改最新提交 #

git commit --amend 會為 HEAD 建立替代提交,並移動目前分支:

sh
git add README.md
git commit --amend --no-edit

不使用 --no-edit 執行 git commit --amend 可替換訊息。原始提交在到期與修剪前,仍可透過 reflog 復原。

互動式編輯較早的提交 #

互動式 rebase 會重寫選定端點之後的提交:

sh
git rebase -i HEAD~3

命令會呈現最後三個提交。其待辦命令包括:

命令效果
pick保留提交
reword替換其訊息
edit套用後停止
squash與前一個提交合併並編輯訊息
fixup合併並捨棄其訊息
drop略過

若要變更較早提交的內容,將它標記為 edit,在 rebase 停止時修改,然後繼續:

sh
git add path/to/file
git commit --amend --no-edit
git rebase --continue

解決衝突、暫存已解決路徑,然後繼續。git rebase --abort 會將分支返回 rebase 前的狀態。git rebase --edit-todo 會在 rebase 暫停時變更剩餘指示。

使用 Reflog 復原 #

每次本機參照移動都可能產生 reflog 項目。在進行有風險的重寫前建立明確的備份名稱:

sh
git branch backup/before-rewrite

接著定位並命名較早的狀態:

sh
git reflog show --date=iso HEAD
git switch -c recovered-work HEAD@{1}

Reflog 是本機實作歷史,不是分散式備份。發布重要工作、保留遠端備份,並避免在復原完成前執行破壞性清理。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)