# Git 貫徹底層 這是一篇被電之後,重新閱讀 [Git from the Bottom Up](https://jwiegley.github.io/git-from-the-bottom-up/) 的筆記。 原文長度約 31 頁 A4, 文章長度中等,是可以一天看完的長度。 > 筆者覺得 pdf 比 html 方便閱讀,讀者亦可參考:[pdf](https://github.com/tpn/pdfs/blob/master/Git%20from%20the%20Bottom%20Up.pdf) 有鑑於中文的 Git 教學大多停留在 command line 等級,為了貢獻開源社群,以及繁體中文的材料。所以寫這篇筆記供大家有一個比較全面的理解。 * 相關專有名詞: |原文|繁體中文|解釋| |---|---|---| |repository|儲存庫|整體 commits 的載體| |the index|(提交)索引|該筆 commit 的索引| |working tree|工作目錄|受 repository 下轄的樹狀目錄| |branch|分支|一個標籤,標記 commit(s) 給予別名| |tag|標記|不會向前平移的標籤,並且帶有自己的說明區塊| |master|master [^master]|一個預設分支名稱與其他分支沒什麼不同| |HEAD|當前位置|你當前的 working tree 位置| **Repository && Working tree && Index 的基礎架構** ![](https://i.imgur.com/RFquorv.png) ## Blobs 1. 所有檔案都稱作 blobs 。 2. 該檔名是由 size 以及 contents 的 SHA1,也就是說相同檔案會有相同的 blob,不論是否跨 commits, 跨 repository。 ## Trees 所有的 Blobs 被存放於 Trees,我們統稱這些物件叫做 blobs。 ![](https://i.imgur.com/3v9w5LO.png) ## Commits 一筆 Commit 是索引到一棵樹(這是一個 blob,裡面存有一堆 blobs 紀錄)。 > a single commit, which references a tree that holds a blob — the blob containing the contents I want to record. 換句話說,第一筆 Commit 會在 objects 放三個物件 ``` commit └── tree └── blob ``` 舉書中的例子: ``` $ echo 'Hello, world!' > greeting ``` ``` $ git hash-object greeting af5626b4a114abcb82d63db7c8082c3c4756e51b ``` ``` $ git init $ git add greeting $ git commit -m "Added my greeting" ``` ``` $ git cat-file -t af5626b blob $ git cat-file blob af5626b Hello, world! ``` 觀察 HEAD 所參考到的樹: ``` $ git ls-tree HEAD 100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting ``` 觀察 HEAD 所參考到的絕對路徑(檔案): ``` $ git rev-parse HEAD 588483b99a46342501d99e3f10630cfc1219ea32 # different on your system ``` 這筆 Commit id 會不同於大家的裝置,因為這是由 author 跟 時間決定的 Hash 值。 ``` $ git cat-file commit HEAD tree 0563f77d884e4f79ce95117e2d686d7d6e282887 author John Wiegley <johnw@newartisans.com> 1209512110 -0400 committer John Wiegley <johnw@newartisans.com> 1209512110 -0400 Added my greeting ``` ## 樹是怎麼產生的? git commit,分成三個階段: 1. write-tree 2. commit-tree 3. update-ref (更新 branch 標籤) 第一步 write-tree routine 會把這些 **index** (.git/index)蒐集成一棵樹 ``` $ rm -fr greeting .git $ echo 'Hello, world!' > greeting $ git init $ git add greeting $ git log # this will fail, there are no commits! fatal: bad default revision 'HEAD' $ git ls-files --stage # list blob referenced by the index 100644 af5626b4a114abcb82d63db7c8082c3c4756e51b 0 greeting $ git write-tree # record the contents of the index in a tree 0563f77d884e4f79ce95117e2d686d7d6e282887 ``` ![](https://i.imgur.com/LFiG8Nk.png) 第二步,呼叫 commit-tree ``` $ echo "Initial commit" | git commit-tree 0563f77 5f1bc85745dcccce6121494fdd37658cb4ad441f ``` 第三步 ``` $ echo 5f1bc85745dcccce6121494fdd37658cb4ad441f > .git/refs/heads/master ``` 這步指令等價於 ``` $ git update-ref refs/heads/master 5f1bc857 ``` ## 分支(Branch)與標籤(Tag) 大致上 Git 只有三種東西: blobs, trees and commits. 注意,Tag 是特殊的例外,他有自己的「描述區段」,放在 .git/refs/tags/ ### 分支相關專有名詞: 自己看:[參考資料](https://jwiegley.github.io/git-from-the-bottom-up/1-Repository/6-a-commit-by-any-other-name.html) 裡面有: 1. branchname 2. tagname 3. HEAD 4. c82a22c39cbc32… 5. c82a22c 6. name^ 7. name^^ 8. name^2 9. name~10 10. name:path 11. name^{tree} 12. name1..name2 (注意兩個點) 13. name1...name2 14. master.. 15. ..master 16. –since="2 weeks ago" 17. –until="1 week ago" 18. –grep=pattern 19. –committer=pattern 20. –author=pattern 21. –no-merges ## Rebase 每一個分支,有一到多個 commits(想想他的樹狀 blobs,蒐集起來) 這是原本: ![](https://i.imgur.com/Pmga6HQ.png) 這樣操作之後 ``` $ git checkout Z# switch to the Z branch $ git merge D# merge commits B, C and D into Z ``` ![](https://i.imgur.com/JdJIUuB.png) (注意到,我去 merge 別人,就是我往前一步) 如果換做 rebase 操作: ``` $ git checkout Z# switch to the Z branch $ git rebase D# change Z’s base commit to point to D ``` ![](https://i.imgur.com/aSF0hgg.png) ### 什麼時候用 rebase ? 1. 合併多筆 commits 2. 重新排序 commits 3. 移除不要的變更 4. 移動基底到你的分支 5. 變更久遠以前的 commit ==書本建議大家去讀 rebase 的 [man-page](https://git-scm.com/docs/git-rebase)== ## reset **reset is a reference editor, an index editor, and a working tree editor.** * mixed * Default * 移除新增未建立 commit 的 index * soft * 改變 HEAD 參考到的 commit ``` $ git reset --soft HEAD^ # backup HEAD to its parent, # effectively ignoring the last commit $ git update-ref HEAD HEAD^ # does the same thing, albeit manually ``` 注意到:如果你是下游的 git (你幾乎都是),你直接改 HEAD 的結果,在下次 pull 的時候,會產生一筆新的 merge! ![](https://i.imgur.com/njAvFeI.png) * hard * 直接拉回去看過去的 commit * 移除所有未 commit 的變更,除非你有 stash ``` $ git stash# because it's always a good thing to do $ git reset --hard HEAD~3# go back in time $ git reset --hard HEAD@{1}# oops, that was a mistake, undo it! $ git stash apply# and bring back my working tree changes ``` ## stash and reflog (前提是沒被 gc 丟掉) reflog,一個 local 端對 repository 的所有變更,屬於 commit 的 meta-data ``` $ git reflog 5f1bc85... HEAD@{0}: commit (initial): Initial commit ``` 這可以看到你所有 local 端的操作,然後透過 reset 可以將其復原 而 stash 有點像 stack,推當前這些檔案到 blobs ### 製作好用的 snapshot 功能 You can even use stash on a regular basis if you like, with something like the following snapshot script: ``` $ cat <<EOF > /usr/local/bin/git-snapshot #!/bin/sh git stash && git stash apply EOF $ chmod +x $_ $ git snapshot ``` [^master]: 後來因為 [Floyd 事件](https://en.wikipedia.org/wiki/Murder_of_George_Floyd) GitHub 表示改名為 main。筆者覺得沒差,重點是那顆心有沒有歧視,不是名字的問題。亦可參考 [topjohnwu 的貼文](https://www.facebook.com/topjohnwu/posts/pfbid02pgmD1wN37un5uq4mEbL7vCuSrjKc8te9PFfkq25mZ51gdfbm2j4CkbK8LDyC4p3Zl)