progit摘錄筆記

zz1469897535000發表於2018-11-27

Git 基礎

三種狀態

Git 有三種狀態,你的檔案可能處於其中之一:已提交(committed)、已修改(modified)和已暫存(staged)。 已提交表示資料已經全的儲存在本地資料庫中。 已修改表示修改了檔案,但還沒儲存到資料庫中。 已暫存表示對一個已修改檔案的當前版本做了標記,使之包含在下次提交的快照中。

基本的 Git 工作流程如下:

  1. 在工作目錄中修改檔案。
  2. 暫存檔案,將檔案的快照放入暫存區域。
  3. 提交更新,找到暫存區域的檔案,將快照永久性儲存到 Git 倉庫目錄。

如果 Git 目錄中儲存著的特定版本檔案,就屬於已提交狀態。 如果作了修改並已放入暫存區域,就屬於已暫存狀態。 如果自上次取出後作了修改但還沒有放到暫存區域,就是已修改狀態

記錄每次更新

請記住,你工作目錄下的每一個檔案都不外乎這兩種狀態:已跟蹤未跟蹤。 已跟蹤的檔案是指那些被納入了版本控制的檔案,在上一次快照中有它們的記錄,在工作一段時間後,它們的狀態可能處於未修改,已修改或已放入暫存區。 工作目錄中除已跟蹤檔案以外的所有其它檔案都屬於未跟蹤檔案,它們既不存在於上次快照的記錄中,也沒有放入暫存區。 初次克隆某個倉庫的時候,工作目錄中的所有檔案都屬於已跟蹤檔案,並處於未修改狀態。

編輯過某些檔案之後,由於自上次提交後你對它們做了修改,Git 將它們標記為已修改檔案。我們逐步將這些修改過的檔案放入暫存區然後提交所有暫存了的修改,如此反覆。所以使用 Git 時檔案的生命週期如下:(圖片中Unmodified可以理解為上面的commited 已提交狀態)

mark

忽略檔案

我們可以建立一個名為 .gitignore 的檔案,列出要忽略的檔案模式

檔案 .gitignore 的格式規範如下:

  • 所有空行或者以 # 開頭的行都會被 Git 忽略。
  • 可以使用標準的 glob 模式匹配。
  • 匹配模式可以以( / )開頭防止遞迴。
  • 匹配模式可以以( / )結尾指定目錄。
  • 要忽略指定模式以外的檔案或目錄,可以在模式前加上驚歎號( ! )取反。

所謂的 glob 模式是指 shell 所使用的簡化了的正規表示式。 星號()匹配零個或多個任意字元; [abc] 匹配任何一個列在方括號中字元(這個例子要麼匹配一個 a,要麼匹配一個b,要麼匹配一個c);問號( ? )只匹配一個任意字元;如果在方括號中使用短劃線分隔兩個字元,表示所有在這兩個字元範圍內的都可以匹配(比如 [0-9] 示匹配所有 0 到 9 的數字)。 使用兩個星號( ) 表示匹配任意中間目錄,比如 a/**/z 可以匹配 a/z , a/b/z 或a/b/c/z 等。

我們再看一個 .gitignore 檔案的例子:

# no .a files
*.a
# but do track lib.a, even though you`re ignoring .a files above
!lib.a
# only ignore the TODO file in the current directory, not subdir/TODO
/TODO
# ignore all files in the build/ directory
build/
# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt
# ignore all .pdf files in the doc/ directory
doc/**/*.pdf
複製程式碼

比較差異

要檢視尚未暫存的檔案更新了哪些部分,不加引數直接輸入 git diff

若要檢視已暫存的將要新增到下次提交裡的內容,可以用 git diff –cached 命令。(Git 1.6.1 及更高版本還允許使用 git diff –staged ,效果是相同的,但更好記些。)

請注意,git diff 本身只顯示尚未暫存的改動,而不是自上次提交以來所做的所有改動。 所以有時候你一下子暫存了所有更新過的檔案後,執行 git diff 後卻什麼也沒有,就是這個原因。

提交更新

git commit -m `xxxx`
複製程式碼

請記住,提交時記錄的是放在暫存區域的快照。 任何還未暫存的然保持已修改狀態,可以在下次提交時納入版本管理。 每一次執行提交操作,都是對你專案作一次快照,以後可以回到這個狀態,或者進行比較。

跳過使用暫存區域

git commit 加上 -a 選項,Git 就會自動把所有已經跟蹤過的檔案存起來一併提交,從而跳過 git add 步驟:

移除檔案

要從 Git 中移除某個檔案,就必須要從已跟蹤檔案清單中移除(確地說,是從暫存區域移除),然後提交。 可以用 git rm 命令完成此工作,並連帶從工作目錄中刪除指定的檔案,這樣以後就不會出現在未跟蹤檔案清單中了。

撤消操作

有時候我們提交完了才發現漏掉了幾個檔案沒有新增行帶有 –amend 選項的提交命令嘗試重新提交:

git commit --amend
複製程式碼

文字編輯器啟動後,可以看到之前的提交資訊。 編輯後儲存會覆蓋原來的提交資訊。
例如,你提交後發現忘記了暫存某些需要的修改,可以像下面這樣操作:

$ git commit -m `initial commit`
$ git add forgotten_file
$ git commit --amend
複製程式碼

最終你只會有一個提交 – 第二次提交將代替第一次提交的結果。

取消暫存的檔案

接下來的兩個小節演示如何操作暫存區域與工作目錄中已修改的檔案。 這些命令在修改檔案狀態的同時,也會提示如何撤消操作。 例如,你已經修改了兩個檔案並且想要將它們作為兩次獨立的修改提交,但是卻意外地輸入了 git add * 暫存了它們兩個。 如何只取消暫存兩個中的一個呢? git status 命令提示了你:

$ git add *
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
modified: CONTRIBUTING.md
複製程式碼

Changes to be committed文字正下方,提示使用 `git reset HEAD …​ 來取消暫存。 所
以,我們可以這樣來取消暫存 CONTRIBUTING.md 檔案:

$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
複製程式碼

撤消對檔案的修改

如果你並不想保留對 CONTRIBUTING.md 檔案的修改怎麼辦? 你該如何方便地撤消修改 – 將它還原成上次提交時的樣子(或者剛克隆完的樣子,或者剛把它放入工作目錄時的樣子)? 幸運的是, git status 也告訴了你應該如何做。 在最後一個例子中,未暫存區域是這樣:

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
複製程式碼

它非常清楚地告訴了你如何撤消之前所做的修改。 讓我們來按照提示執行:

$ git checkout -- CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
複製程式碼

遠端倉庫的使用

檢視遠端倉庫

如果想檢視你已經配置的遠端倉庫伺服器,可以執行 git remote 命令

你也可以指定選項 -v ,會顯示需要讀寫遠端倉庫使用的 Git 儲存的簡寫與其對應的 URL

$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
複製程式碼

新增遠端倉庫

執行 git remote add 新增一個新的遠端 Git 倉庫

從遠端倉庫中抓取與拉取

$ git fetch [remote-name]
複製程式碼

這個命令會訪問遠端倉庫,從中拉取所有你還沒有的資料。 執行完成後,你將會擁有那個遠端倉庫中所有分支的引用,可以隨時合併或檢視。

如果你使用 clone 命令克隆了一個倉庫,命令會自動將其新增為遠端倉庫並預設以origin 為簡寫。 所以,git fetch origin 會抓取克隆(或上一次抓取)後新推送的所有工作。 必須注意 git fetch 命令會將資料拉取到你的本地倉庫 – 它並不會自動合併或修改你當前的工作。 當準備好時你必須手動將其合併入你的工作。

推送到遠端倉庫

當你想分享你的專案時,必須將其推送到上游。 這個命令很簡單: git push [remote-name][branch-name] 。 當你想要將 master 分支推送到 origin 伺服器時(再次說明,克隆時通常會自動幫你設定好那兩個名字),那麼執行這個命令就可以將你所做的備份到伺服器:

git push origin master
複製程式碼

打標籤

像其他版本控制系統(VCS)一樣,Git 可以給歷史中的某一個提交打上標籤,以示重要。 比較有代表性的是人們會使用這個功能來標記釋出結點(v1.0 等等)。 在本節中,你將會學習如何列出已有的標籤、如何建立新標籤、以及不同型別的標籤分別是什麼。

列出標籤

在 Git 中列出已有的標籤是非常簡單直觀的。 只需要輸入 git tag :

git tag
v0.1
v1.3
複製程式碼

你也可以使用特定的模式查詢標籤。 例如,Git 自身的原始碼倉庫包含標籤的數量超過 500個。 如果只對 1.8.5 系列感興趣,可以執行:

git tag -l `v1.8.5*`
複製程式碼

建立標籤

Git 使用兩種主要型別的標籤:輕量標籤(lightweight)與附註標籤(annotated)。

一個輕量標籤很像一個不會改變的分支 – 它只是一個特定提交的引用。

然而,附註標籤是儲存在 Git 資料庫中的一個完整物件。 它們是可以被校驗的;其中包含打標籤者的名字、電子郵件地址、日期時間;還有一個標籤資訊;並且可以使用 GNU PrivacyGuard (GPG)簽名與驗證。 通常建議建立附註標籤,這樣你可以擁有以上所有資訊;但是如果你只是想用一個臨時的標籤,或者因為某些原因不想要儲存那些資訊,輕量標籤也是可用的。

附註標籤

在 Git 中建立一個附註標籤是很簡單的。 最簡單的方式是當你在執行 tag 命令時指定 -a選項:

$ git tag -a v1.4 -m `my version 1.4`
$ git tag
v0.1
v1.3
v1.4
複製程式碼

-m 選項指定了一條將會儲存在標籤中的資訊。 如果沒有為附註標籤指定一條資訊,Git 會執行編輯器要求你輸入資訊。

通過使用 git show 命令可以看到標籤資訊與對應的提交資訊:

$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date: Sat May 3 20:19:12 2014 -0700
my version 1.4
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the version number
複製程式碼

輸出顯示了打標籤者的資訊、打標籤的日期時間、附註資訊,然後顯示具體的提交資訊。

輕量標籤

另一種給提交打標籤的方式是使用輕量標籤。 輕量標籤本質上是將提交校驗和儲存到一個檔案中 – 沒有儲存任何其他資訊。 建立輕量標籤,不需要使用 -a 、 -s 或 -m 選項,只需要提供標籤名字:

$ git tag v1.4-lw
$ git tag
v0.1
v1.3
v1.4
v1.4-lw
v1.5
複製程式碼

共享標籤

預設情況下, git push 命令並不會傳送標籤到遠端倉庫伺服器上。 在建立完標籤後你必須顯式地推送標籤到共享伺服器上。 這個過程就像共享遠端分支一樣 – 你可以執行 git push origin [tagname] 。

$ git push origin v1.5
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new tag] v1.5 -> v1.5
複製程式碼

檢出標籤

在 Git 中你並不能真的檢出一個標籤,因為它們並不能像分支一樣來回移動。 如果你想要工作目錄與倉庫中特定的標籤版本完全一樣,可以使用 git checkout -b [branchname][tagname] 在特定的標籤上建立一個新分支:

$ git checkout -b version2 v2.0.0
Switched to a new branch `version2`
複製程式碼

當然,如果在這之後又進行了一次提交, version2 分支會因為改動向前移動了,那麼
version2 分支就會和 v2.0.0 標籤稍微有些不同,這時就應該當心了。

總結

現在,你可以完成所有基本的 Git 本地操作-建立或者克隆一個倉庫、做更改、暫存並提交這些更改、瀏覽你的倉庫從建立到現在的所有更改的歷史。 下一步,本書將介紹 Git 的殺手級特性:分支模型。

Git 分支

分支簡介

Git 儲存的不是檔案的變化或者差異,而是一系列不同時刻的檔案快照。

在進行提交操作時,Git 會儲存一個提交物件(commit object)。知道了 Git 儲存資料的方式,我們可以很自然的想到——該提交物件會包含一個指向暫存內容快照的指標。但不僅僅是這樣,該提交物件還包含了作者的姓名和郵箱、提交時輸入的資訊以及指向它的父物件的指標。首次提交產生的提交物件沒有父物件,普通提交操作產生的提交物件有一個父物件,而由多個分支合併產生的提交物件有多個父物件,

在進行提交操作時,Git 會儲存一個提交物件(commit object)。知道了 Git 儲存資料的方式,我們可以很自然的想到——該提交物件會包含一個指向暫存內容快照的指標。 但不僅僅是這樣,該提交物件還包含了作者的姓名和郵箱、提交時輸入的資訊以及指向它的父物件的指標。首次提交產生的提交物件沒有父物件,普通提交操作產生的提交物件有一個父物件,而由多個分支合併產生的提交物件有多個父物件,

$ git add README test.rb LICENSE
$ git commit -m `The initial commit of my project`
複製程式碼

當使用 git commit 進行提交操作時,Git 會先計算每一個子目錄(本例中只有專案根目錄)的校驗和,然後在 Git 倉庫中這些校驗和儲存為樹物件。 隨後,Git 便會建立一個提交物件,它除了包含上面提到的那些資訊外,還包含指向這個樹物件(專案根目錄)的指標。如此一來,Git 就可以在需要的時候重現此次儲存的快照。

現在,Git 倉庫中有五個物件:三個 blob 物件(儲存著檔案快照)、一個樹物件(記錄著目錄結構和 blob 物件索引)以及一個提交物件(包含著指向前述樹物件的指標和所有提交資訊)。

mark

做些修改後再次提交,那麼這次產生的提交物件會包含一個指向上次提交物件(父物件)的指標。

mark

Git 的分支,其實本質上僅僅是指向提交物件的可變指標。 Git 的預設分支名字是 master 。在多次提交操作之後,你其實已經有一個指向最後那個提交物件的 master 分支。 它會在每次的提交操作中自動向前移動。

mark

分支建立

Git 是怎麼建立新分支的呢? 很簡單,它只是為你建立了一個可以移動的新的指標。 比如,建立一個 testing 分支, 你需要使用 git branch 命令:

$ git branch testing
複製程式碼
mark

那麼,Git 又是怎麼知道當前在哪一個分支上呢? 也很簡單,它有一個名為 HEAD 的特殊指標。 請注意它和許多其它版本控制系統(如 Subversion 或 CVS)裡的 HEAD 概念完全不同。 在 Git 中,它是一個指標,指向當前所在的本地分支(譯註:將 HEAD 想象為當前分支的別名)。 在本例中,你仍然在 master 分支上。 因為 git branch 命令僅僅 建立 一個新分支,並不會自動切換到新分支中去。

mark

你可以簡單地使用 git log 命令檢視各個分支當前所指的物件。 提供這一功能的引數是 —
decorate 。

$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit of my project
複製程式碼

分支切換

要切換到一個已存在的分支,你需要使用 git checkout 命令。 我們現在切換到新建立的
testing 分支去:

git checkout testing
複製程式碼

這樣 HEAD 就指向 testing 分支了。

mark

那麼,這樣的實現方式會給我們帶來什麼好處呢? 現在不妨再提交一次:

$ vim test.rb
$ git commit -a -m `made a change`
複製程式碼
mark

如圖所示,你的 testing 分支向前移動了,但是 master 分支卻沒有,它仍然指向執行 git
checkout 時所指的物件。 這就有意思了,現在我們切換回 master 分支看看:

$ git checkout master
複製程式碼
mark

這條命令做了兩件事。 一是使 HEAD 指回 master 分支,二是將工作目錄恢復成 master 分支所指向的快照內容。 也就是說,你現在做修改的話,專案將始於一個較舊的版本。 本質上來講,這就是忽略 testing 分支所做的修改,以便於向另一個方向進行開發。

你可以簡單地使用 git log 命令檢視分叉歷史。 執行 git log –oneline –decorate –graph
–all ,它會輸出你的提交歷史、各個分支的指向以及專案的分支分叉情況。

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project
複製程式碼

分支管理

遠端分支

遠端引用是對遠端倉庫的引用(指標),包括分支、標籤等等。 你可以通過 git ls-remote (remote) 來顯式地獲得遠端引用的完整列表,或者通過 git remote show (remote) 獲得遠端分支的更多資訊。 然而,一個更常見的做法是利用遠端跟蹤分支。
遠端跟蹤分支是遠端分支狀態的引用。 它們是你不能移動的本地引用,當你做任何網路通訊操作時,它們會自動移動。 遠端跟蹤分支像是你上次連線到遠端倉庫時,那些分支所處狀態的書籤。
它們以 (remote)/(branch) 形式命名。 例如,如果你想要看你最後一次與遠端倉庫 origin
通訊時 master 分支的狀態,你可以檢視 origin/master 分支。 你與同事合作解決一個問題並且他們推送了一個 iss53 分支,你可能有自己的本地 iss53 分支;但是在伺服器上的分支會指向 origin/iss53 的提交。

mark

如果你在本地的 master 分支做了一些工作,然而在同一時間,其他人推送提交到
git.ourcompany.com 並更新了它的 master 分支,那麼你的提交歷史將向不同的方向前進。
也許,只要你不與 origin 伺服器連線,你的 origin/master 指標就不會移動。

mark

如果要同步你的工作,執行 git fetch origin 命令。 這個命令查詢 origin`` 是哪一個伺服器(在本例中,它是git.ourcompany.com ),從中抓取本地沒有的資料,並且更新本地資料庫,移動 origin/master 指標指向新的、更新後的位置。

mark

推送

當你想要公開分享一個分支時,需要將其推送到有寫入許可權的遠端倉庫上。 本地的分支並不會自動與遠端倉庫同步 – 你必須顯式地推送想要分享的分支。 這樣,你就可以把不願意分享的內容放到私人分支上,而將需要和別人協作的內容推送到公開分支。如果希望和別人一起在名為 serverfix 的分支上工作,你可以像推送第一個分支那樣推送它。 執行 git push (remote) (branch) :

$ git push origin serverfix
複製程式碼

這裡有些工作被簡化了。 Git 自動將 serverfix 分支名字展開為
refs/heads/serverfix:refs/heads/serverfix ,那意味著, 推送本地的 serverfix 分支來更新遠端倉庫上的 serverfix 分支。“ 我們將會詳細學習 [_git_internals] 的 refs/heads/ 部分,但是現在可以先把它放在兒。 你也可以執行 git push origin serverfix:serverfix,它會做同樣的事 – 相當於它說, 推送本地的 serverfix 分支,將其作為遠端倉庫的 serverfix 分支“ 可以通過這種格式來推送本地分支到一個命名不相同的遠端分支。 如果並不想讓遠端倉庫上的分支叫做 serverfix ,可以執行git push origin serverfix:awesomebranch 來將本地的 serverfix 分支推送到遠端倉庫上的awesomebranch 分支。

如何避免每次輸入密碼
如果你正在使用 HTTPS URL 來推送,Git 伺服器會詢問使用者名稱與密碼。 預設情
況下它會在終端中提示伺服器是否允許你進行推送。
如果不想在每一次推送時都輸入使用者名稱與密碼,你可以設定一個 credential cache``。 最簡單的方式就是將其儲存在記憶體中幾分鐘,可以簡單地執行git config –global credential.helper cache 來設定它。
想要了解更多關於不同驗證快取的可用選項

跟蹤分支

從一個遠端跟蹤分支檢出一個本地分支會自動建立一個叫做 跟蹤分支“(有時候也叫做 上游分支“)。 跟蹤分支是與遠端分支有直接關係的本地分支。 如果在一個跟蹤分支上輸入 git pull ,Git 能自動地識別去哪個伺服器上抓取、合併到哪個分支。

當克隆一個倉庫時,它通常會自動地建立一個跟蹤 origin/master 的 master 分支。 然而,如果你願意的話可以設定其他的跟蹤分支 – 其他遠端倉庫上的跟蹤分支,或者不跟蹤 master分支。 最簡單的就是之前看到的例子,執行 git checkout -b [branch]
[remotename]/[branch] 。 這是一個十分常用的操作所以 Git 提供了 –track 快捷方式:

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch `serverfix`
複製程式碼

設定已有的本地分支跟蹤一個剛剛拉取下來的遠端分支,或者想要修改正在跟蹤的上游分
支,你可以在任意時間使用 -u 或 –set-upstream-to 選項執行 git branch 來顯式地設
置。

$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
複製程式碼

如果想要檢視設定的所有跟蹤分支,可以使用 git branch 的 -vv 選項。 這會將所有的本
地分支列出來並且包含更多的資訊,如每一個分支正在跟蹤哪個遠端分支與本地分支是否是領先、落後或是都有。

$ git branch -vv
iss53 7e424c3 [origin/iss53: ahead 2] forgot the brackets
master 1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
testing 5ea463a trying something new
複製程式碼

拉取

當 git fetch 命令從伺服器上抓取本地沒有的資料時,它並不會修改工作目錄中的內容。 它只會獲取資料然後讓你自己合併。 然而,有一個命令叫作 git pull 在大多數情況下它的含義是一個 git fetch 緊接著一個 git merge 命令。 如果有一個像之前章節中演示的設定好的跟蹤分支,不管它是顯式地設定還是通過 clone 或 checkout 命令為你建立的, git
pull 都會查詢當前分支所跟蹤的伺服器與分支,從伺服器上抓取資料然後嘗試合併入那個遠端分支。
由於 git pull 的魔法經常令人困惑所以通常單獨顯式地使用 fetch 與 merge 命令會更好
一些。

刪除遠端分支

假設你已經通過遠端分支做完所有的工作了 – 也就是說你和你的協作者已經完成了一個特性並且將其合併到了遠端倉庫的 master 分支(或任何其他穩定程式碼分支)。 可以執行帶有 –delete 選項的 git push 命令來刪除一個遠端分支。 如果想要從伺服器上刪serverfix分支,執行下面的命令:

$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
- [deleted] serverfix
複製程式碼

變基

在 Git 中整合來自不同分支的修改主要有兩種方法: merge 以及 rebase 。 在本節中我們將學習什麼是“變基”,怎樣使用“變基”,並將展示該操作的驚豔之處,以及指出在何種情況下你應避免使用它。

變基的基本操作

請回顧之前在 [_basic_merging] 中的一個例子,你會看到開發任務分叉到兩個不同分支,又各自提交了更新。

mark

之前介紹過,整合分支最容易的方法是 merge 命令。 它會把兩個分支的最新快照( C3 和 C4 )以及二者最近的共同祖先( C2 )進行三方合併,合併的結果是生成一個新的快照(並提交)。

mark

其實,還有一種方法:你可以提取在 C4 中引入的補丁和修改,然後在 C3 的基礎上再應用一次。 在 Git 中,這種操作就叫做 變基。 你可以使用 rebase 命令將提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一樣。

在上面這個例子中,執行:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
複製程式碼

它的原理是首先找到這兩個分支(即當前分支 experiment 、變基操作的目標基底分支
master )的最近共同祖先 C2 ,然後對比當前分支相對於該祖先的歷次提交,提取相應的修改並存為臨時檔案,然後將當前分支指向目標基底 C3 , 最後以此將之前另存為臨時檔案的修改依序應用。(譯註:寫明瞭 commit id,以便理解,下同)

mark

現在回到 master 分支,進行一次快進合併。

$ git checkout master
$ git merge experiment
複製程式碼
mark

此時, C4` 指向的快照就和上面使用 merge 命令的例子中 C5 指向的快照一模一樣了。 這兩種整合方法的最終結果沒有任何區別,但是變基使得提交歷史更加整潔。 你在檢視一個經過變基的分支的歷史記錄時會發現,儘管實際的開發工作是並行的,但它們看上去就像是先後序列的一樣,提交歷史是一條直線沒有分叉。

一般我們這樣做的目的是為了確保在向遠端分支推送時能保持提交歷史的整潔——例如向某個別人維護的專案貢獻程式碼時。 在這種情況下,你首先在自己的分支裡進行開發,當開發完成時你需要先將你的程式碼變基到 origin/master 上,然後再向主專案提交修改。 這樣的話,該專案的維護者就不再需要進行整合工作,只需要快進合併便可。

請注意,無論是通過變基,還是通過三方合併,整合的最終結果所指向的快照始終是一樣
的,只不過提交歷史不同罷了。 變基是將一系列提交按照原有次序依次應用到另一分支上,而合併是把最終結果合在一起。

變基的風險

呃,奇妙的變基也並非完美無缺,要用它得遵守一條準則:

不要對在你的倉庫外有副本的分支執行變基。

變基操作的實質是丟棄一些現有的提交,然後相應地新建一些內容一樣但實際上不同的提
交。 如果你已經將提交推送至某個倉庫,而其他人也已經從該倉庫拉取提交併進行了後續工作,此時,如果你用 git rebase 命令重新整理了提交併再次推送,你的同伴因此將不得不再次將他們手頭的工作與你的提交進行整合,如果接下來你還要拉取並整合他們修改過的提交,事情就會變得一團糟。

mark

然後,某人又向中央伺服器提交了一些修改,其中還包括一次合併。 你抓取了這些在遠端分支上的修改,並將其合併到你本地的開發分支,然後你的提交歷史就會變成這樣:

mark

接下來,這個人又決定把合併操作回滾,改用變基;繼而又用 git push –force 命令覆蓋了伺服器上的提交歷史。 之後你從伺服器抓取更新,會發現多出來一些新的提交。

mark

結果就是你們兩人的處境都十分尷尬。 如果你執行 git pull 命令,你將合併來自兩條提交
歷史的內容,生成一個新的合併提交,最終倉庫會如圖所示:

mark

此時如果你執行 git log 命令,你會發現有兩個提交的作者、日期、日誌居然是一樣的,這會令人感到混亂。 此外,如果你將這一堆又推送到伺服器上,你實際上是將那些已經被變基拋棄的提交又找了回來,這會令人感到更加混亂。 很明顯對方並不想在提交歷史中看到 C4和 C6 ,因為之前就是他們把這兩個提交通過變基丟棄的。

只要你把變基命令當作是在推送前清理提交使之整潔的工具,並且只在從未推送至共用倉庫的提交上執行變基命令,你就不會有事。 假如你在那些已經被推送至共用倉庫的提交上執行變基命令,並因此丟棄了一些別人的開發所基於的提交,那你就有大麻煩了,你的同事也會因此鄙視你。

如果你或你的同事在某些情形下決意要這麼做,請一定要通知每個人執行 git pull –rebase命令,這樣儘管不能避免傷痛,但能有所緩解。

總的原則是,只對尚未推送或分享給別人的本地修改執行變基操作清理歷史,從不對已推送至別處的提交執行變基操作,這樣,你才能享受到兩種方式帶來的便利。

Git工具

重置揭密

三棵樹

理解 reset 和 checkout 的最簡方法,就是以 Git 的思維框架(將其作為內容管理器)來管理三棵不同的樹。 樹“ 在我們這裡的實際意思是 檔案的集合“,而不是指特定的資料結構。 (在某些情況下索引看起來並不像一棵樹,不過我們現在的目的是用簡單的方式思考它。)

Git 作為一個系統,是以它的一般操作來管理並操縱這三棵樹的:

用途
HEAD 上一次提交的快照,下一次提交的父結點
Index 預期的下一次提交的快照
Working Directory 沙盒
HEAD

HEAD 是當前分支引用的指標,它總是指向該分支上的最後一次提交。 這表示 HEAD 將是下
一次提交的父結點。 通常,理解 HEAD 的最簡方式,就是將它看做 你的上一次提交 的快
照。

索引

索引是你的 預期的下一次提交。 我們也會將這個概念引用為 Git 的 暫存區域``,這就是當你執行git commit 時 Git 看起來的樣子。

Git 將上一次檢出到工作目錄中的所有檔案填充到索引區,它們看起來就像最初被檢出時的樣
子。 之後你會將其中一些檔案替換為新版本,接著通過 git commit 將它們轉換為樹來用作
新的提交。

工作目錄

最後,你就有了自己的工作目錄。 另外兩棵樹以一種高效但並不直觀的方式,將它們的內容
儲存在 .git 資料夾中。 工作目錄會將它們解包為實際的檔案以便編輯。 你可以把工作目錄
當做 沙盒。在你將修改提交到暫存區並記錄到歷史之前,可以隨意更改。

工作流程

Git 主要的目的是通過操縱這三棵樹來以更加連續的狀態記錄專案的快照。

mark

讓我們來視覺化這個過程:假設我們進入到一個新目錄,其中有一個檔案。 我們稱其為該文
件的 v1 版本,將它標記為藍色。 現在執行 git init ,這會建立一個 Git 倉庫,其中的
HEAD 引用指向未建立的分支( master 還不存在)。

mark

現在我們想要提交這個檔案,所以用 git add 來獲取工作目錄中的內容,並將其複製到索引
中。

mark

接著執行 git commit ,它首先會移除索引中的內容並將它儲存為一個永久的快照,然後建立
一個指向該快照的提交物件,最後更新 master 來指向本次提交。

mark

此時如果我們執行 git status ,會發現沒有任何改動,因為現在三棵樹完全相同。
現在我們想要對檔案進行修改然後提交它。 我們將會經歷同樣的過程;首先在工作目錄中修
改檔案。 我們稱其為該檔案的 v2 版本,並將它標記為紅色。

mark

如果現在執行 git status ,我們會看到檔案顯示在 Changes not staged for commit,`` 下面並 被標記為紅色,因為該條目在索引與工作目錄之間存在不同。 接著我們執行git add 來將它暫存到索引中。

mark

此時,由於索引和 HEAD 不同,若執行 git status 的話就會看到 Changes to be committed`` 下的該檔案變為綠色 ——也就是說,現在預期的下一次提交與上一次提交不同。 最後,我們執行git commit 來完成提交。

mark

現在執行 git status 會沒有輸出,因為三棵樹又變得相同了。
切換分支或克隆的過程也類似。 當檢出一個分支時,它會修改 HEAD 指向新的分支引用,將
索引 填充為該次提交的快照,然後將 索引 的內容複製到 工作目錄 中。

重置的作用

在以下情景中觀察 reset 命令會更有意義。
為了演示這些例子,假設我們再次修改了 file.txt 檔案並第三次提交它。 現在的歷史看起
來是這樣的:

mark

讓我們跟著 reset 看看它都做了什麼。 它以一種簡單可預見的方式直接操縱這三棵樹。 它
做了三個基本操作。

第 1 步:移動 HEAD

reset 做的第一件事是移動 HEAD 的指向。 這與改變 HEAD 自身不同( checkout 所做
的); reset 移動 HEAD 指向的分支。 這意味著如果 HEAD 設定為 master 分支(例如,
你正在 master 分支上),執行 git reset 9e5e64a 將會使 master 指向 9e5e64a 。

mark

無論你呼叫了何種形式的帶有一個提交的 reset ,它首先都會嘗試這樣做。 使用 reset —
soft ,它將僅僅停在那兒。
現在看一眼上圖,理解一下發生的事情:它本質上是撤銷了上一次 git commit 命令。 當你
在執行 git commit 時,Git 會建立一個新的提交,並移動 HEAD 所指向的分支來使其指向該
提交。 當你將它 reset 回 HEAD~ (HEAD 的父結點)時,其實就是把該分支移動回原來的
位置,而不會改變索引和工作目錄。 現在你可以更新索引並再次執行 git commit 來完成
git commit –amend 所要做的事情了(見 [_git_amend])。

第 2 步:更新索引(–mixed)

注意,如果你現在執行 git status 的話,就會看到新的 HEAD 和以綠色標出的它和索引之
間的區別。
接下來, reset 會用 HEAD 指向的當前快照的內容來更新索引

mark

如果指定 –mixed 選項, reset 將會在這時停止。 這也是預設行為,所以如果沒有指定任
何選項(在本例中只是 git reset HEAD~ ),這就是命令將會停止的地方。
現在再看一眼上圖,理解一下發生的事情:它依然會撤銷一上次 提交 ,但還會 取消暫存 所
有的東西。 於是,我們回滾到了所有 git add 和 git commit 的命令執行之前。

第 3 步:更新工作目錄(–hard)

reset 要做的的第三件事情就是讓工作目錄看起來像索引。 如果使用 –hard 選項,它將會
繼續這一步。

mark

現在讓我們回想一下剛才發生的事情。 你撤銷了最後的提交、 git add 和 git commit 命令
以及工作目錄中的所有工作。

必須注意, –hard 標記是 reset 命令唯一的危險用法,它也是 Git 會真正地銷燬資料的僅
有的幾個操作之一。 其他任何形式的 reset 呼叫都可以輕鬆撤消,但是 –hard 選項不
能,因為它強制覆蓋了工作目錄中的檔案。 在這種特殊情況下,我們的 Git 資料庫中的一個
提交內還留有該檔案的 v3 版本,我們可以通過 reflog 來找回它。但是若該檔案還未提交,
Git 仍會覆蓋它從而導致無法恢復。

回顧

reset 命令會以特定的順序重寫這三棵樹,在你指定以下選項時停止:

  1. 移動 HEAD 分支的指向 (若指定了 –soft ,則到此停止)
  2. 使索引看起來像 HEAD (若未指定 –hard ,則到此停止)
  3. 使工作目錄看起來像索引

通過路徑來重置

前面講述了 reset 基本形式的行為,不過你還可以給它提供一個作用路徑。 若指定了一個
路徑, reset 將會跳過第 1 步,並且將它的作用範圍限定為指定的檔案或檔案集合。 這樣做
自然有它的道理,因為 HEAD 只是一個指標,你無法讓它同時指向兩個提交中各自的一部
分。 不過索引和工作目錄 可以部分更新,所以重置會繼續進行第 2、3 步。
現在,假如我們執行 git reset file.txt (這其實是 git reset –mixed HEAD file.txt 的簡
寫形式,因為你既沒有指定一個提交的 SHA-1 或分支,也沒有指定 –soft 或 –hard ),
它會:

  1. 移動 HEAD 分支的指向 (已跳過)
  2. 讓索引看起來像 HEAD (到此處停止)
    所以它本質上只是將 file.txt 從 HEAD 複製到索引中。
mark

它還有 取消暫存檔案 的實際效果。 如果我們檢視該命令的示意圖,然後再想想 git add 所
做的事,就會發現它們正好相反。

mark

這就是為什麼 git status 命令的輸出會建議執行此命令來取消暫存一個檔案。 (檢視
[_unstaging] 來了解更多。)

我們可以不讓 Git 從 HEAD 拉取資料,而是通過具體指定一個提交來拉取該檔案的對應版
本。 我們只需執行類似於 git reset eb43bf file.txt 的命令即可。

mark

它其實做了同樣的事情,也就是把工作目錄中的檔案恢復到 v1 版本,執行 git add 新增
它,然後再將它恢復到 v3 版本(只是不用真的過一遍這些步驟)。 如果我們現在執行 git
commit ,它就會記錄一條“將該檔案恢復到 v1 版本”的更改,儘管我們並未在工作目錄中真正
地再次擁有它。

壓縮

我們來看看如何利用這種新的功能來做一些有趣的事情 – 壓縮提交。
假設你的一系列提交資訊中有 oops.“、 WIP“ 和 forgot this file``, 聰明的你就能使用reset 來輕鬆快速地將它們壓縮成單個提交,也顯出你的聰明。 ([_squashing] 展示了另一
種方式,不過在本例中用 reset 更簡單。)
假設你有一個專案,第一次提交中有一個檔案,第二次提交增加了一個新的檔案並修改了第
一個檔案,第三次提交再次修改了第一個檔案。 由於第二次提交是一個未完成的工作,因此
你想要壓縮它。

mark

那麼可以執行 git reset –soft HEAD~2 來將 HEAD 分支移動到一箇舊一點的提交上(即你
想要保留的第一個提交):

mark

然後只需再次執行 git commit :

mark

現在你可以檢視可到達的歷史,即將會推送的歷史,現在看起來有個 v1 版 file-a.txt 的提
交,接著第二個提交將 file-a.txt 修改成了 v3 版並增加了 file-b.txt 。 包含 v2 版本的
檔案已經不在歷史中了。

檢出

最後,你大概還想知道 checkout 和 reset 之間的區別。 和 reset 一樣, checkout 也操
縱三棵樹,不過它有一點不同,這取決於你是否傳給該命令一個檔案路徑。

不帶路徑

執行 git checkout [branch] 與執行 git reset –hard [branch] 非常相似,它會更新所有三
棵樹使其看起來像 [branch] ,不過有兩點重要的區別。

首先不同於 reset –hard , checkout 對工作目錄是安全的,它會通過檢查來確保不會將已
更改的檔案吹走。 其實它還更聰明一些。它會在工作目錄中先試著簡單合併一下,這樣所有
還未修改過的檔案都會被更新。 而 reset –hard 則會不做檢查就全面地替換所有東西。

第二個重要的區別是如何更新 HEAD。 reset 會移動 HEAD 分支的指向,而 checkout 只
會移動 HEAD 自身來指向另一個分支。

例如,假設我們有 master 和 develop 分支,它們分別指向不同的提交;我們現在在
develop 上(所以 HEAD 指向它)。 如果我們執行 git reset master ,那麼 develop 自身
現在會和 master 指向同一個提交。 而如果我們執行 git checkout master 的話, develop
不會移動,HEAD 自身會移動。 現在 HEAD 將會指向 master 。
所以,雖然在這兩種情況下我們都移動 HEAD 使其指向了提交 A,但做法是非常不同的。
reset 會移動 HEAD 分支的指向,而 checkout 則移動 HEAD 自身。

mark

帶路徑

執行 checkout 的另一種方式就是指定一個檔案路徑,這會像 reset 一樣不會移動 HEAD。
它就像 git reset [branch] file 那樣用該次提交中的那個檔案來更新索引,但是它也會覆蓋
工作目錄中對應的檔案。 它就像是 git reset –hard [branch] file (如果 reset 允許你這
樣執行的話)- 這樣對工作目錄並不安全,它也不會移動 HEAD。

Git 內部原理

從根本上來講 Git是一個內容定址(content-addressable)檔案系統,並在此之上提供了一個版本控制系統的使用者介面。

底層命令和高層命令

本書旨在討論如何通過 checkout 、 branch 、 remote 等大約 30 個諸如此類動詞形式的命令
來玩轉 Git。 然而,由於 Git 最初是一套面向版本控制系統的工具集,而不是一個完整的、用
戶友好的版本控制系統,所以它還包含了一部分用於完成底層工作的命令。 這些命令被設計
成能以 UNIX 命令列的風格連線在一起,抑或藉由指令碼呼叫,來完成工作。 這部分命令一般
被稱作“底層(plumbing)”命令,而那些更友好的命令則被稱作“高層(porcelain)”命令。
本書前九章專注於探討高層命令。 然而在本章,我們將主要面對底層命令。 因為,底層命令
得以讓你窺探 Git 內部的工作機制,也有助於說明 Git 是如何完成工作的,以及它為何如此運
作。 多數底層命令並不面向終端使用者:它們更適合作為新命令和自定義指令碼的組成部分。

當在一個新目錄或已有目錄執行 git init 時,Git 會建立一個 .git 目錄。 這個目錄包含
了幾乎所有 Git 儲存和操作的物件。 如若想備份或複製一個版本庫,只需把這個目錄拷貝至
另一處即可。 本章探討的所有內容,均位於這個目錄內。 該目錄的結構如下所示:

$ ls -F1
HEAD
config*
description
hooks/
info/
objects/
refs/
複製程式碼

description 檔案僅供 GitWeb 程式使用,我們無需關心。 config 檔案包含專案特有的配置選項。 info 目錄包含一個全域性性排除(global exclude)檔案,用以放置那些不希望被記錄在 .gitignore 檔案中的忽略模式(ignored patterns)。 hooks 目錄包含客戶端或服務端的鉤子指令碼(hook scripts),在 [_git_hooks] 中這部分話題已被詳細探討過。

剩下的四個條目很重要: HEAD 檔案、(尚待建立的) index 檔案,和 objects 目
錄、 refs 目錄。 這些條目是 Git 的核心組成部分。 objects 目錄儲存所有資料內
容; refs 目錄儲存指向資料(分支)的提交物件的指標; HEAD 檔案指示目前被檢出的分
支; index 檔案儲存暫存區資訊。 我們將詳細地逐一檢視這四部分,以期理解 Git 是如何運
轉的。

Git 物件

Git 是一個內容定址檔案系統。 看起來很酷, 但這是什麼意思呢? 這意味著,Git 的核心部
分是一個簡單的鍵值對資料庫(key-value data store)。 你可以向該資料庫插入任意型別的
內容,它會返回一個鍵值,通過該鍵值可以在任意時刻再次檢索(retrieve)該內容。 可以通
過底層命令 hash-object 來演示上述效果——該命令可將任意資料儲存於 .git 目錄,並返
回相應的鍵值。 首先,我們需要初始化一個新的 Git 版本庫,並確認 objects 目錄為空:

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f
複製程式碼

可以看到 Git 對 objects 目錄進行了初始化,並建立了 pack 和 info 子目錄,但均為空。
接著,往 Git 資料庫存入一些文字:

$ echo `test content` | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
複製程式碼

-w 選項指示 hash-object 命令儲存資料物件;若不指定此選項,則該命令僅返回對應的鍵
值。 –stdin 選項則指示該命令從標準輸入讀取內容;若不指定此選項,則須在命令尾部給
出待儲存檔案的路徑。 該命令輸出一個長度為 40 個字元的校驗和。 這是一個 SHA-1 雜湊值
——一個將待儲存的資料外加一個頭部資訊(header)一起做 SHA-1 校驗運算而得的校驗
和。後文會簡要討論該頭部資訊。 現在我們可以檢視 Git 是如何儲存資料的:

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
複製程式碼

可以在 objects 目錄下看到一個檔案。 這就是開始時 Git 儲存內容的方式——一個檔案對應
一條內容,以該內容加上特定頭部資訊一起的 SHA-1 校驗和為檔案命名。 校驗和的前兩個字
符用於命名子目錄,餘下的 38 個字元則用作檔名。
可以通過 cat-file 命令從 Git 那裡取回資料。 這個命令簡直就是一把剖析 Git 物件的瑞士
軍刀。 為 cat-file 指定 -p 選項可指示該命令自動判斷內容的型別,併為我們顯示格式友
好的內容:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
複製程式碼

樹物件

接下來要探討的物件型別是樹物件(tree object),它能解決檔名儲存的問題,也允許我們
將多個檔案組織到一起。 Git 以一種類似於 UNIX 檔案系統的方式儲存內容,但作了些許簡
化。 所有內容均以樹物件和資料物件的形式儲存,其中樹物件對應了 UNIX 中的目錄項,數
據物件則大致上對應了 inodes 或檔案內容。 一個樹物件包含了一條或多條樹物件記錄(tree
entry),每條記錄含有一個指向資料物件或者子樹物件的 SHA-1 指標,以及相應的模式、類
型、檔名資訊。 例如,某專案當前對應的最新樹物件可能是這樣的:

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib
複製程式碼

master^{tree} 語法表示 master 分支上最新的提交所指向的樹物件。 請注意, lib 子目
錄(所對應的那條樹物件記錄)並不是一個資料物件,而是一個指標,其指向的是另一個樹
物件:

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb
複製程式碼

從概念上講,Git 內部儲存的資料有點像這樣:

mark

提交物件

現在有三個樹物件,分別代表了我們想要跟蹤的不同專案快照。然而問題依舊:若想重用這
些快照,你必須記住所有三個 SHA-1 雜湊值。 並且,你也完全不知道是誰儲存了這些快照,
在什麼時刻儲存的,以及為什麼儲存這些快照。 而以上這些,正是提交物件(commit
object)能為你儲存的基本資訊。

可以通過呼叫 commit-tree 命令建立一個提交物件,為此需要指定一個樹物件的 SHA-1 值,
以及該提交的父提交物件(如果有的話)。 我們從之前建立的第一個樹物件開始

$ echo `first commit` | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
複製程式碼

現在可以通過 cat-file 命令檢視這個新提交物件:

$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700
first commit
複製程式碼

接著,我們將建立另兩個提交物件,它們分別引用各自的上一個提交(作為其父提交對
象):

$ echo `second commit` | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo `third commit` | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9
複製程式碼

這三個提交物件分別指向之前建立的三個樹物件快照中的一個。 現在,如果對最後一個提交
的 SHA-1 值執行 git log 命令,會出乎意料的發現,你已有一個貨真價實的、可由 git
log 檢視的 Git 提交歷史了:

$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
third commit
bak/test.txt | 1 +
1 file changed, 1 insertion(+)
commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:14:29 2009 -0700
second commit
new.txt | 1 +
test.txt | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:09:34 2009 -0700
first commit
test.txt | 1 +
1 file changed, 1 insertion(+)
複製程式碼

太神奇了: 就在剛才,你沒有藉助任何上層命令,僅憑几個底層操作便完成了一個 Git 提交
歷史的建立。 這就是每次我們執行 git add 和 git commit 命令時, Git 所做的實質工作
——將被改寫的檔案儲存為資料物件,更新暫存區,記錄樹物件,最後建立一個指明瞭頂層
樹物件和父提交的提交物件。 這三種主要的 Git 物件——資料物件、樹物件、提交物件——
最初均以單獨檔案的形式儲存在 .git/objects 目錄下。 下面列出了目前示例目錄內的所有
物件,輔以各自所儲存內容的註釋:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # `test content`
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1
複製程式碼

如果跟蹤所有的內部指標,將得到一個類似下面的物件關係圖:

mark

物件儲存

前文曾提及,在儲存內容時,會有個頭部資訊一併被儲存。 讓我們略花些時間來看看 Git 是
如何儲存其物件的。 通過在 Ruby 指令碼語言中互動式地演示,你將看到一個資料物件——本
例中是字串“what is up, doc?”——是如何被儲存的。
可以通過 irb 命令啟動 Ruby 的互動模式:

$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"
複製程式碼

Git 以物件型別作為開頭來構造一個頭部資訊,本例中是一個“blob”字串。 接著 Git 會新增
一個空格,隨後是資料內容的長度,最後是一個空位元組(null byte):

>> header = "blob #{content.length}

相關文章