首頁  >  文章  >  電腦教學  >  Git 提交是差異、快照還是歷史記錄?

Git 提交是差異、快照還是歷史記錄?

PHPz
PHPz轉載
2024-02-19 11:39:421098瀏覽

Git 提交是差异、快照还是历史记录?

#

理解Git提交實作方式對我來說很簡單,但理解他人對提交的看法卻有難度。因此,我在Mastodon上向他人提出了一些問題。

大家是怎麼看待 Git 提交的?

#我進行了 非常不科學的調查,詢問大家是怎麼看待 Git 提交的:是快照、差異,還是所有之前提交的清單? (當然,把它看作這三者都是合理的,但我很好奇人們的 主要

結果是:

  • 51% 差異
  • 42% 快照
  • 4% 所有先前的提交的歷史記錄
  • 3% “其他”

我很驚訝差異和快照兩個選項的比例如此接近。人們也提出了一些有趣但相互矛盾的觀點,例如
「在我看來,提交是一個差異,但我認為它實際上是以快照的形式實現的」 和
「在我看來,提交是一個快照,但我認為它實際上是以差異的形式實現的」。關於提交的實際實現方式,我們稍後再詳談。

在我們進一步討論之前:我們的說 「一個差異」 或 「一個快照」 都是什麼意思?

什麼是差異?

#我說的「差異」可能相當明顯:差異就是你在執行 git show COMMIT_ID 時所得到的。例如,這是一個 rbspy 專案中的拼字錯誤修復:

diff --git a/src/ui/summary.rs b/src/ui/summary.rs
index 5c4ff9c..3ce9b3b 100644
--- a/src/ui/summary.rs
+++ b/src/ui/summary.rs
@@ -160,7 +160,7 @@ mod tests {
";
let mut buf: Vec = Vec::new();
-stats.write(&mut buf).expect("Callgrind write failed");
+stats.write(&mut buf).expect("summary write failed");
let actual = String::from_utf8(buf).expect("summary output not utf8");
assert_eq!(actual, expected, "Unexpected summary output");
}

你可以在 GitHub 上看到它: https://github.com/rbspy/rbspy/commit/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b

什麼是快照?

#我說的 「快照」 是指 「當你執行 git checkout COMMIT_ID 時得到的所有檔案」。

Git 通常將提交的檔案清單稱為 「樹」(如「目錄樹」),你可以在 GitHub 上看到上述提交的所有檔案:

https://github.com/rbspy/rbspy/tree/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b(它是 /tree/ 而不是 /commit/

#「Git 是如何實現的」真的是正確的解釋方式嗎?

#我最常聽到的關於學習 Git 的建議大概是 「只要學會 Git 在內部是如何表示事物的,一切都會變得清晰明了」。我顯然非常喜歡這種觀點(如果你花了一些時間閱讀這個博客,你就會知道我 喜歡

但是作為一個學習 Git 的方法,它並沒有我希望的那麼成功!通常我會興奮地開始解釋“好的,所以Git
提交是一個快照,它有一個指向它的父提交的指針,然後一個分支是一個指向提交的指針,然後……”,但是我試圖幫助的人會告訴我,他們並沒有真正發現這個解釋有多有用,他們仍然不明白。所以我一直在考慮其他方案。

但是讓我們還是先談談內部實作吧。

Git 是如何在內部表示提交的 —— 快照

#在內部,Git 將提交表示為快照(它儲存每個檔案目前版本的 「樹」)。我在 在一個 Git 倉庫中,你的文件在哪裡? 中寫過這個,但下面是一個非常快速的內部格式概述。

這是一個提交的表示方式:

$ git cat-file -p 24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b
tree e197a79bef523842c91ee06fa19a51446975ec35
parent 26707359cdf0c2db66eb1216bf7ff00eac782f65
author Adam Jensen1672104452 -0500
committer Adam Jensen1672104890 -0500
Fix typo in expectation message

以及,當我們查看這個樹物件時,我們會看到這個提交中倉庫根目錄下每個檔案/子目錄的清單:

$ git cat-file -p e197a79bef523842c91ee06fa19a51446975ec35
040000 tree 2fcc102acd27df8f24ddc3867b6756ac554b33ef.cargo
040000 tree 7714769e97c483edb052ea14e7500735c04713eb.github
100644 blob ebb410eb8266a8d6fbde8a9ffaf5db54a5fc979a.gitignore
100644 blob fa1edfb73ce93054fe32d4eb35a5c4bee68c5bf5ARCHITECTURE.md
100644 blob 9c1883ee31f4fa8b6546a7226754cfc84ada5726CODE_OF_CONDUCT.md
100644 blob 9fac1017cb65883554f821914fac3fb713008a34CONTRIBUTORS.md
100644 blob b009175dbcbc186fb8066344c0e899c3104f43e5Cargo.lock
100644 blob 94b87cd2940697288e4f18530c5933f3110b405bCargo.toml

這意味著檢出一個 Git 提交總是很快的:對 Git 來說,檢出昨天的提交和檢出 100 萬個提交之前的提交一樣容易。 Git 永遠不需要重新應用 10000 個差異來確定當前狀態,因為提交根本就不是以差異的形式儲存的。

快照使用 packfile 進行壓縮

我剛剛提到了Git 提交是一個快照,但是,當有人說「在我看來,提交是一個快照,但我認為它在實現上是一個差異」
時,這其實也是正確的! Git
提交並不是以你可能習慣的差異的形式表示的(它們不是以與上一個提交的差異的形式存儲在磁碟上的),但基本的直覺是,如果你要對一個10,000
行的文件編輯500 次,那麼儲存500 份文件的效率會很低。

Git 有一個將檔案以差異的形式儲存的方法。這稱為 “packfile”,Git 會定期進行垃圾回收,將你的資料壓縮成 packfile 以節省磁碟空間。當你 git clone 一個倉庫時,Git 也會壓縮資料。

這裡,我沒有足夠的篇幅來完整地解釋 packfile 是如何運作的(Aditya Mukerjee 的 《解壓縮 Git packfile》是我最喜歡的解釋它們是如何運作的文章)。不過,我可以在這裡簡單總結一下我對 deltas 工作原理的理解,以及它們與 diff 的區別:

  • 物件儲存為 「原始檔案」 和一個 “變化量delta” 的引用
  • 變化量是一系列例如 “讀取第 0 到 100 字節,然後插入字節 ‘hello there’,然後讀取第 120 到 200 字節” 的指令。它從原始文件中拼湊出新的文字。所以沒有 “刪除” 的概念,只有複製和添加。
  • 我認為變化量的層次較少:我不知道如何檢查 Git 究竟要經過多少層變化量才能得到一個給定的對象,但我的印像是通常不會很多。可能少於 10 層?不過,我很想知道如何才能真正查出來。
  • 原始文件不一定來自上一個提交,它可以是任何東西。也許它甚至可以來自一個更晚的提交?我不確定。
  • 沒有一個 “正確的” 演算法來計算變化量,Git 只是有一些近似的啟發式演算法

當你查看差異時,實際上發生了一些奇怪的事情

#當我們執行 git show SOME_COMMIT 來查看某個提交的差異時,實際上發生的事情有點反直覺。我的理解是:

  • Git 會在 packfile 中尋找並套用變化量來重建該提交和其父提交的樹。
  • Git 會對兩個目錄樹(目前提交的目錄樹和父親提交的目錄樹)進行差異比較。通常這很快,因為幾乎所有的檔案都是完全一樣的,所以 git 只需比較相同檔案的雜湊值就可以了,幾乎所有時候都不用做什麼。
  • 最後 Git 會顯示差異
  • 所以,Git 會將變化量轉換為快照,然後計算差異。它感覺有點奇怪,因為它從一個類似差異的東西開始,最終得到另一個類似差異的東西,但是變化量和差異實際上是完全不同的,所以這是說得通的。

    也就是說,我認為 Git 將提交儲存為快照,而 packfile 只是實作細節,目的是節省磁碟空間並加快克隆速度。我其實從來沒必要知道 packfile 是如何運作的,但它確實能幫助我理解 Git 是如何在不佔用太多磁碟空間的情況下將提交快照化的。

    一個 “錯誤的” Git 理解:提交是差異

    #我認為一個相當常見的,對 Git 的 “錯誤” 的理解是:

    • 提交是以基於上一個提交的差異的形式儲存的(加上指向父提交的指標和作者和訊息)。
    • 要取得提交的目前狀態,Git 需要從頭開始重新套用所有先前的提交。

    這個理解當然是錯誤的(在現實中,提交是以快照的形式存儲的,差異是從這些快照計算出來的),但是對我來說它似乎非常有用而且有意義!在考慮合併提交時會有一點奇怪,但是或許我們可以說這只是基於合併提交的第一個父親提交的差異。

    我認為這個錯誤的理解有的時候非常有用,而且對於日常 Git 使用來說它似乎並沒有什麼問題。我真的很喜歡它將我們最常使用的東西(差異)作為最基本的元素——它對我來說非常直觀。

    我也一直在思考一些其他有用但 「錯誤」 的 Git 理解,例如:

    • 提交資訊可以被編輯(實際上不能,你只是複製了一個相同的提交然後給了它一個新的信息,舊的提交仍然存在)
    • 提交可以被移動到一個不同的基礎上(類似地,它們是被複製了)

    我認為有一系列非常有意義的、 “錯誤” 的對 Git 的理解,它們在很大程度上都受到 Git 用戶界面的支持,並且在大多數情況下都不會產生什麼問題。但是當你想要撤銷一個更改或出現問題時,它可能會變得混亂。

    將提交視為差異的一些優勢

    就算我知道在 Git 中提交是快照,我可能大部分時間也都將它們視為差異,因為:

    • 大多時候我都在關注我正在做的 更改 —— 如果我只是改變了一行程式碼,顯然我主要是在考慮那一行程式碼而不是整個程式碼庫的當前狀態
    • 點擊 GitHub 上的 Git 提交或使用 git show 時,你會看到差異,所以這只是我習慣看到的東西
    • 我經常使用變基,它是關於重新應用差異的

    將提交視為快照的一些優勢

    #但我有時也會將提交視為快照,因為:

    • Git 經常對檔案的移動感到困惑:有時我移動了一個檔案並編輯了它,Git 無法識別它是否被移動過,而是顯示為
      「刪除了 old.py,新增了 new.py」。這是因為 Git 只儲存快照,所以當它顯示 “移動 old.py -> new.py”
      時,只是猜測,因為 old.py 和 new.py 的內容相似。
    • 這種方式更容易理解 git checkout COMMIT_ID 在做什麼(重新應用 10000 個提交的想法讓我感到很有壓力)
    • 合併提交在我看來更像是快照,因為合併的提交實際上可以是任何東西(它只是一個新的快照!)。它幫助我理解為什麼在解決合併衝突時可以進行任意更改,以及為什麼在解決衝突時要小心。

    其他一些關於提交的理解

    #Mastodon 的一些回覆中也提到了:

    • 有關提交的 「額外的」 帶外信息,例如電子郵件、GitHub 拉取請求或你和同事的對話
    • 將「差異」視為一個「之前的狀態 之後的狀態」
    • 以及,當然,許多人根據情況的不同以不同的方式看待提交

    人們在談論提交時使用的其他一些單字可能不那麼含糊:

    • 「修訂」(似乎更像是快照)
    • 「補丁」(看起來更像是差異)

    就到這裡吧!

    #我很難了解人們對 Git 有哪些不同的理解。尤其棘手的是,儘管「錯誤」 的理解往往非常有用,但人們卻非常熱衷於警惕「錯誤」
    的心智模式,所以人們不願意分享他們「錯誤」 的想法,生怕有什麼Git 解釋者會站出來向他們解釋為什麼他們是錯的。 (這些 Git
    解釋者通常是出於善意的,但是無論如何它都會產生一種負面影響)

    但是我學到了很多!我仍然不完全清楚該如何談論提交,但是我們最終會弄清楚的。

    感謝 Marco Rogers、Marie Flanagan 以及 Mastodon 上的所有人和我討論 Git 提交。

    以上是Git 提交是差異、快照還是歷史記錄?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述:
    本文轉載於:mryunwei.com。如有侵權,請聯絡admin@php.cn刪除