Git系列之Refs 與 Reflog

原文地址
Git是一切關於commit的藝術:你暫存commit,提交commit,瀏覽以往的commit,在不一樣的倉庫切換commit,這一切使用不一樣的命令來實現。這些命令中大部分以各類形式操做commit,一些能夠接受commit做爲參數。例如,你可使用git checkout命令來查看以往的commit,只須要傳入該commit的哈希便可,抑或傳入分支名在不一樣分支間切換。
圖片描述
經過理解這些使用commit的不一樣方式,將使得這些命令變得更增強大。本章,我將經過探究commit引用的多種方式來闡述常見命令的內部工做原理,這些常見命令包括git checkout, git branchgit pushgit

咱們也將學到怎樣去恢復看似「丟失」的命令,經過Git的reflog機制來訪問到它們。github

哈希

引用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引用。fetch

引用(Refs)

引用(Refs)是一種間接引用commit的方式。它是一種對用戶來講更親和的commit哈希的別名。使Git表示分支與標籤的內部機制。url

引用被做爲一個普通的文本文件保存在.git/refs路徑下,where .git is usually called .git。要瀏覽在你的倉庫之中的refs,請訪問你的.git/refs路徑。你將看到如下結構,結構包含的文件因你倉庫中的分支,標籤,遠程分支而異。spa

.git/refs/
    heads/
        master
        some-feature
    remotes/
        origin/
            master
    tags/
        v0.9

heads目錄描述了了在你倉庫中全部的本地分支。每個文件名對應了相應的分支,在文件夾內部的文件中你會看他對應的commit哈希。這個哈希是如今的分支最末端的那個commit的哈希。爲了證明這點,你能夠在Git所在的根目錄,執行下面兩段代碼:code

# 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 pushgit 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 pushgit 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進行高級pushfetch,以及如何使用相對^字符在分支結構中切換。

咱們還了解了reflog,這是一種引用經過任何其餘方式不可用的commit的方式。這一個你有種「起死回生」之感的操做。

全部這一切的要點是可以精確地在開發方案中挑選出你的須要的commit。運用本文學到的知識對你已有的Git知識體系將有很大的提高:即對經常使用的命令git log,git show,git checkout,git reset,git revert,git rebase等命令使用refs做爲參數。

相關文章
相關標籤/搜索