Git系列之Refs 與 Reflog
Git是一切關于commit的藝術:你暫存commit,提交commit,瀏覽以往的commit,在不同的倉庫切換commit,這一切使用不同的命令來實現。這些命令中大部分以各種形式操作commit,一些可以接受commit作為參數。例如,你可以使用 git checkout 命令來查看以往的commit,只需要傳入該commit的哈希即可,抑或傳入分支名在不同分支間切換。
通過理解這些使用commit的不同方式,將使得這些命令變得更加強大。本章,我將通過探究commit引用的多種方式來闡述常見命令的內部工作原理,這些常見命令包括 git checkout , git branch 和 git push 。
我們也將學到怎樣去恢復看似“丟失”的命令,通過Git的reflog機制來訪問到它們。
哈希
引用commit最直接的方式就是通過它的SHA-1哈希。這是每個commit獨一無二的ID。在 git log 的輸出中你可以找到每個commit的哈希。
commit 0c708fdec272bc4446c6cabea4f0022c2b616eba Author: Mary Johnson <mary@example.com> Date: Wed Jul 9 16:37:42 2014 -0500 Some commit message
當你向其他命令傳commit時,你只需要輸入足夠的字符來標明這個獨一無二的提交即可(譯注:即你不需要將40位的哈希都輸入)例如,你可以查看某個commit通過像下面這樣運行 git show 命令:
git show 0c708f
工作中有時需要將一個分支(branch),標簽(tag)或其他間接引用解析成相應的commit哈希時。此時你需要使用 git rev-parse 命令。以下命令執行后將顯示主分支當前commit的哈希。
git rev-parse master
這在編寫接受commit引用的自定義腳本時非常有用。你可以使用 git rev-parse 命令來使你的輸入規范化,而非手動編譯你的commit引用。
引用(Refs)
引用(Refs)是一種間接引用commit的方式。它是一種對用戶來說更親和的commit哈希的別名。使Git表示分支與標簽的內部機制。
引用被作為一個普通的文本文件保存在 .git/refs 路徑下,where .git is usually called .git。要瀏覽在你的倉庫之中的refs,請訪問你的 .git/refs 路徑。你將看到以下結構,結構包含的文件因你倉庫中的分支,標簽,遠程分支而異。
.git/refs/ heads/ master some-feature remotes/ origin/ master tags/ v0.9
heads 目錄描述了了在你倉庫中所有的本地分支。每一個文件名對應了相應的分支,在文件夾內部的文件中你會看他對應的commit哈希。這個哈希是現在的分支最末端的那個commit的哈希。為了證實這點,你可以在 Git 所在的根目錄,執行下面兩段代碼:
# Output the contents of `refs/heads/master` file: cat .git/refs/heads/master # Inspect the commit at the tip of the `master` branch: git log -1 master
由 cat 命令得到的commit哈希應與 git log 得到的哈希一致。
要更改主分支的位置就必須要改到 refs/heads/master 的內容。同樣地,創建一個新的分支就是把commit哈希寫入新文件這樣簡單。這也是為何Git與SVN相比是如此輕量的部分原因。
tag文件夾實際上以同樣的方式工作著,只是其中存放的是tag而非分支。remotes文件夾將所有由 git remote 命令創建的所有遠程分支存儲為單獨的子目錄。在每個子目錄中,可以發現被fetch進倉庫的對應的遠程分支。
規范引用(refs)
當你把引用傳給Git命令時,你可以使用引用的全稱,也可以使用縮寫去讓Git匹配符合的引用。你應該對引用縮寫足夠熟悉,以便在你每次通過其來切換分支。
git show some-feature
上面命令的 some-feature 參數實際上就是分支的縮寫。在使用前Git會將其解析為 refs/heads/some-feature 。你也可以使用引用的全名:
git show refs/heads/some-feature
這樣寫能避免引用位置產生歧義。這是很必要的,例如,你有標簽與分支都叫做 some-feature 然而,當你使用正確的命名規范,標簽與分支間的歧義將不再困擾你。
在 Refspecs 部分,我們將看到更多的全名引用。
Packed Refs
對于大型倉庫,Git將會周期性地運行垃圾回收將移除不必需要的對象,并將引用壓縮至單個文件中,來提高性能。你可以執行下面命令來強制啟動這一過程:
git gc
這將把在refs文件夾所有單獨的分支與標簽文件移動到在 .git 根目錄中的一個叫做 packed-refs 的文件。如果你打開這個文件,你將會發現commit哈希與引用映射表:
00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature 0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/master bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9
垃圾回收對于正常的Git功能并不會有任何影響。但是,如果你想知道你的 .git/refs 文件為什么是空的話,現在你知道答案了。
特殊的引用(Refs)
除了引用目錄之外,還有一些特別的引用存在于 .git 路徑的頂部:
-
HEAD – 當前檢出的 commit/branch.
-
FETCH_HEAD – 最新從遠程倉庫獲取的分支。
-
ORIG_HEAD – 作為備份指向危險操作前的HEAD。
-
MERGE_HEAD – 使用 git merge 命令合并進當前分支的提交。
-
CHERRY_PICK_HEAD – 使用 git cherry-pick 命令的提交。
當需要時這些 引用 會被創建或更新。例如,當執行 git pull 命令時,首先會執行 git fetch 命令,此時會更新 FETCH_HEAD 引用,其后執行 git merge FETCH_HEAD 命令將獲取的分支導入倉庫。當然上述這些引用可以像普通引用一樣使用,我想你一定使用過HEAD作為參數吧。
由于你倉庫的類型與狀態的差異,這些文件會包含不同的內容。HEAD引用有可能是一個指向其他引用的象征性的引用,也可能是一個commit哈希。當你在主分支下,查看你的HEAD文件內容:
git checkout master cat .git/HEAD
你將看到 ref: refs/heads/master ,這意味著HEAD指向refs/heads/master的引用。這就是為什么Git能獲悉當前主分支被檢出了的原因。如果切換到其他分支,HEAD的內容將被更新為指向那個分支。但是如果你在commit的層面使用 check out 而非分支層面,HEAD的內容將會是一個commit哈希而非引用。這就是為什么Git能獲悉它處在獨立的狀態的原因。
多數情況,HEAD僅僅是一個你可以直接使用的引用。其他僅僅在使用Git內部工作的底層腳本時才會用到。
Refspecs
每個 refspec 都會創建一個本地倉庫分支到遠程倉庫分支的映射。這讓通過本地Git命令操作遠程分支成為可能,并且配置一些高級的 git push 與 git fetch 行為。
refspec 被表示為 [+]<src>:<dst> 。 <src> 參數表示本地倉庫的分支, <src> 參數表示遠程倉庫的目標分支,可選參數 + 表示是否讓遠程倉庫執行 non-fast-forward 更新。
Refspec可與 git push 命令聯合使用來為遠程分支添加不同的名字。例如,以下命令推送主分支到遠程分支與尋常 git push 命令無二,所不同的是使用了 qa-master 作為分支名。這樣的做法常用于需要將自己的分支推送到遠程倉庫的QA團隊中。
git push origin master:refs/heads/qa-master
你也可以通過 refspecs 來刪除遠程分支。在使用特性分支工作流的團隊里,將特性分支推送到遠程倉庫是一個很常見的場景(例如出于備份的目的)。遠程特性分支在本地分支從倉庫中刪除后會依舊存在于遠程倉庫中,這意味著隨著你項目的推進死分支的數量會一直疊加。可以通過以下命令來刪除他們:
git push origin :some-feature
這是非常方便的,因為你不需要登錄到遠程倉庫去手動刪除遠程分支。請注意,在Git v1.7.0你可以使用 --delete 來替代上述方法。下面的命令具有同樣的效果:
git push origin --delete some-feature
通過添加幾行代碼到Git配置文件中,你可以使用refspec來改變 git fetch 命令的行為。通常, git fetch 命令會獲取遠程倉庫所有分支,由于.git/confi文件中的一下部分:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/*:refs/remotes/origin/*
fetch 一行告訴 git fetch 從源倉庫下載所有分支。但是在一些工作流中,你并不需要把他們都下載下來。例如,許多持續集成的工作流只關注主分支。為了只獲取主分支,可將 fetch 行修改為:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/master:refs/remotes/origin/master
你可以用相同的方式來配置 git push 。例如你總是想要將本地的 qa-master 推送至遠程(像前問所述),你可以按下述方式修改配置文件:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/master:refs/remotes/origin/master push = refs/heads/master:refs/heads/qa-master
Refspecs提供了各種能在倉庫間轉移分支的Git命令的一個全面控制。有了這些命令你可以重命名或刪除本地倉庫中的分支,通過別名提交/獲取分支,控制 git push 和 git fetch 命令作用于你指定的分支。
相對引用
你可以通過 ~ 字符來引用相對于另一個commit的commit。例如:下面的代碼引用了HEAD的祖父級:
git show HEAD~2
但是,當用于合并提交時,事情變的有點復雜。因為合并提交存在一個以上的父級,意味著至少有兩條路徑可以選擇。對于3路合并(兩條分支合并為一體),第一父級在你執行合并命令時所在的分支,第二父級在你傳入 git merge 命令的那個分支上。
~ 字符將在第一父級上追蹤,如果你想要在別的父級上追蹤,你需要使用 ^ 字符來指定對那一個父級進行追蹤。例如,如果你合并提交,下面的命令會追蹤第二父級:
git show HEAD^2
可以使用多個 ^ 來移動多代。例如,下面代碼展示了追蹤第二父級的HEAD的祖父級(假設其為一個合并)
git show HEAD^2^1
為了說明 ~ 和 ^ 是如何工作的,下圖展示了基于A通過相對引用如何追蹤的每個具體的引用。在一些情況下可以通過多種方式來得到同一個提交:
使用普通引用的命令也能使用相對引用。例如,以下的命令:
# 列出合并提交第二父級上的提交(commits) git log HEAD^2 # 從當前分支上移除最近三次提交 git reset HEAD~3 # 在當前分支上動態rebase最近三次提交 git rebase -i HEAD~3
Reflog
reflog是Git的安全網,其中記錄了基本上所有的本地倉庫中的改變,不論你是否提交了快照。你可以把它想象成你對本地倉庫做的多有操作的歷史記錄。可以運行 git reflog 命令查看reflog。將會輸出如下結果:
400e4b7 HEAD@{0}: checkout: moving from master to HEAD~2 0e25143 HEAD@{1}: commit (amend): Integrate some awesome feature into `master` 00f5425 HEAD@{2}: commit (merge): Merge branch ';feature'; ad8621a HEAD@{3}: commit: Finish the feature
上面代碼可解讀為:
-
執行checked out HEAD~2
-
在此之前,修改了提交信息
-
在此之前,將特性分支合并進主分支
-
在此之前,提交了快照
通過 HEAD{<n>} 語法你可以引用存在reflog中的提交。這與之前章節的 HEAD~<n> 有著相似的用法,但<n>引用reflog中的記錄而不是commit歷史中的記錄。
你可以使用此方法回滾在別的記錄中丟失的狀態。例如,剛用 git reset 刪除一個特性后,你的reflog會像下面這樣:
ad8621a HEAD@{0}: reset: moving to HEAD~3 298eb9f HEAD@{1}: commit: Some other commit message bbe9012 HEAD@{2}: commit: Continue the feature 9cb79fa HEAD@{3}: commit: Start a new feature
在 git reset 命令之前執行的三個操作現在處在懸空狀態,這意味著若非使用reflog你將無法通過任何方法找到他們的引用。現在你知道你不應該丟掉你所有的工作了吧。你現在需要做的就是檢出HEAD@{1}提交,將你的倉庫退回到執行 git reset 之前的狀態。
git checkout HEAD@{1}
這將把你的HEAD分離出來(和分支)從這步你可以創建一個新的分支繼續你的特性開發工作。
小結
你現在應該很愉快地引用一個Git倉庫中的commit。 我們學習了如何將分支和標簽存儲為.git子目錄中的refs,如何讀取packed-refs文件,如何表示HEAD,如何使用refspec進行高級 push 和 fetch ,以及如何使用相對 ? 和 ^ 字符在分支結構中切換。
我們還了解了reflog,這是一種引用通過任何其他方式不可用的commit的方式。這一個你有種“起死回生”之感的操作。
所有這一切的要點是能夠精確地在開發方案中挑選出你的需要的commit。運用本文學到的知識對你已有的Git知識體系將有很大的提升:即對常用的命令 git log , git show , git checkout , git reset , git revert , git rebase 等命令使用 refs 作為參數。
來自:https://segmentfault.com/a/1190000007996197