# 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 的基礎架構**

## Blobs
1. 所有檔案都稱作 blobs 。
2. 該檔名是由 size 以及 contents 的 SHA1,也就是說相同檔案會有相同的 blob,不論是否跨 commits, 跨 repository。
## Trees
所有的 Blobs 被存放於 Trees,我們統稱這些物件叫做 blobs。

## 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
```

第二步,呼叫 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,蒐集起來)
這是原本:

這樣操作之後
```
$ git checkout Z# switch to the Z branch
$ git merge D# merge commits B, C and D into Z
```

(注意到,我去 merge 別人,就是我往前一步)
如果換做 rebase 操作:
```
$ git checkout Z# switch to the Z branch
$ git rebase D# change Z’s base commit to point to D
```

### 什麼時候用 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!

* 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)