Git提交引用和引用日志

jopen 10年前發布 | 23K 次閱讀 Git 版本控制系統

BY 童仲毅(geeeeeeeeek@github)

這是一篇在原文(BY atlassian)基礎上演繹的譯文。除非另行注明,頁面上所有內容采用知識共享-署名(CC BY 2.5 AU)協議共享。

</blockquote>

提交是Git的精髓所在,你無時不刻不在創建和緩存提交、查看以前的提交,或者用各種Git命令在倉庫間轉移你的提交。大多數的命令都對同一個提交操作,而有些會接受提交的引用作為參數。比如,你可以給git checkout傳入一個引用來查看以前的提交,或者傳入一個分支名來切換到對應的分支。

引用一次提交的各種方式

知道提交的各種引用方式之后,Git的命令就會變得更加強大。在這章中,我們研究提交的各種引用方式,來一窺git checkout、git branch、git push等命令的工作原理。

我們還會學到如何使用Git的引用日志查看似乎已被刪除的提交。

哈希字串

引用一個提交最直接的方式是通過SHA-1的哈希字串,這是每個提交唯一的ID。你可以在git log的輸出中找到提交的哈希字串。

commit 0c708fdec272bc4446c6cabea4f0022c2b616eba
Author: Mary Johnson <mary@example.com>
Date:   Wed Jul 9 16:37:42 2014 -0500

一些提交信息</pre> <p>在Git命令中傳遞時,你只需要提供足以確定那個提交的哈希子串即可。比如,你可以這樣用git show的命令顯示上面的提交: </p>

git show 0c708f

有時,我們需要把分支、標簽或者其他間接的引用轉變成對應提交的哈希。git rev-parse命令正是你需要的。下面這個命令返回master分支提交的哈希字串:

git rev-parse master

當你寫的自定義腳本中需要將提交引用作為參數時,這個命令非常有用。你可以讓git rev-parse幫你處理轉換,而不用手動做這件事。

引用

ref是提交的間接引用。你可以把它當做哈希字串的別名,但對用戶更友好。這就是Git內部表示分支和標簽的機制。

引用以一段普通的文本存在于.git/refs目錄中,就是我們平時說的那個.git。你去.git/refs文件夾查看倉庫中的引用。你可以看到下面這樣的結構,但具體的文件取決于你的倉庫中有什么分支和標簽,以及你的遠程倉庫。

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

heads目錄定義了你本地倉庫中的所有分支。每一個文件名和你的分支名一一對應,文件中包含一個提交的哈希字串。這個就是分支頂端的所在位置。為了驗證這一點,試試在Git根目錄運行下面這兩個命令:

# 輸出refs/heads/master文件內容
cat .git/refs/heads/master

查看master分支尾端的提交

git log -1 master</pre>

cat命令返回的哈希字串和git log命令顯示的哈希字串應該是一致的。

如果要改變master分支的位置,Git只需要更改refs/heads/master的文件內容。同樣地,創建新的分支也只需要將當前提交的哈希字串寫入到新的文件中。這也是為什么Git分支比SVN輕量那么多的其中一個原因。

tags目錄也是以相同的方式存儲,只不過其中存的是標簽而不是分支。remotes目錄將你之前用git remote命令創建的所有遠程倉庫以子目錄的形式一一列出。在每個文件夾中,你可以找到所有fetch到本地倉庫的遠程分支。

指定引用

當你向Git命令傳入引用的時候,你既可以指定引用完整的名稱,也可以使用縮寫,然后讓Git來尋找匹配。你應該已經對引用的縮寫很熟悉了,每次你通過名稱引用分支的時候都會這么做。

git show some-feature

這里的some-feature參數其實是分支名的縮寫。Git在使用前將它解析成refs/heads/some-feature。你也可以在命令行中指定引用的全稱,就像這樣:

git show refs/heads/some-feature

這避免了引用可能產生的所有歧義。這是非常必要的,比如你同時有一個標簽和分支都叫some-feature。然而,如果使用正常的命名規范,你不應該有這樣的歧義。

我們會在refspec一節見到更多引用名稱。

打包引用目錄

對于大型倉庫,Git會周期性地執行垃圾回收來移除不需要的對象,將所有引用文件壓縮成單個文件來獲得更好的性能。你可以使用這個命令強制垃圾回收來執行壓縮:

git gc

這個命令把refs文件夾中所有單獨的分支和標簽移動到了.git根目錄下的packed-refs文件中。如果你打開這個文件,你會發現提交的哈希字串和引用之間的映射關系:

00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature
0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/master
bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9

另一方面,正常的Git功能不會受到任何影響。但如果你好奇你的.git/refs文件夾為什么是空的,這一節告訴你了答案。

特殊的引用

除了refs文件夾外,.git根目錄還有一些特殊的引用。如下所示:

  • HEAD – 當前所在的提交或分支。
  • FETCH_HEAD – 遠程倉庫中fetch到的最新一次提交。
  • ORIG_HEAD – HEAD的備份引用,避免損壞。
  • MERGE_HEAD – 你通過git merge并入當前分支的引用(們)。
  • CHERRY_PICK_HEAD – 你cherry pick使用的引用。
  • </ul>

    這些引用由Git在需要時創建和更新。比如說,git pull命令首先運行git fetch,而FETCH_HEAD引用隨之改變。然后,運行git merge FETCH_HEAD來將fetch到的分支最終并入倉庫。當然,你也可以使用其他任何引用,因為我相信你已經對HEAD很熟悉了。

    這些文件包含的內容取決于它們的類型和你的倉庫狀態。HEAD引用可以包含符號鏈接(指向另一個引用而不是哈希字串),或是提交的哈希字串。比如說,看看當你在master分支上時HEAD的內容:

    git checkout master
    cat .git/HEAD

    這個命令會輸出ref: refs/heads/master,也就是說HEAD指向refs/heads/master這個引用。這也正是Git如何知道現在所在的是master分支。如果你要切換分支,HEAD的內容將會被更新到新的分支。但如果你要切換到一個提交而不是分支,HEAD會包含一個提交的哈希而不是符號引用。這就是Git如何知道現在HEAD處于分離狀態。

    在大多數情況下,HEAD是你唯一用得到的引用。其它引用一般只在寫底層腳本,接觸到Git內部的工作機制時才會用到。

    refspec

    refspec將本地分支和遠程分支對應起來。我們可以通過它用本地的Git命令管理遠程分支,設置一些高級的git push和git fetch行為。

    refspec的定義是這樣的:[+]<src>:<dst>。<src>參數是本地的源分支,<dst>是遠程的目標分支。可選的+號強制遠程倉庫采用非快速向前的更新策略。

    refspec可以和git push一起使用,用來指定遠程的分支的名稱。比如,下面這個命令將master分支推送到遠程origin,就像一般的git push一樣,但它使用qa-master作為遠程倉庫中的分支名。對于QA團隊來說,這個方法非常有用。

    git push origin master:refs/heads/qa-master

    你也可以用refspec來刪除遠程分支。feature分支的工作流經常會遇到這種情況,將feature分支推送到遠程倉庫中(比如說為了備 份)。你刪除本地的feature分支之后,遠程的feature分支依然存在,雖然現在我們已經不再需要它。你可以push一個<src>參數為空的refspec來刪除它們,就像這樣:

    git push origin:some-feature

    這非常方便,因為你不需要登錄到你的遠程倉庫然后手動刪除這些遠程分支。注意,在Git v1.7.0之后你可以用--delete標記代替上面這個方法。下面這個命令和上面的命令作用相同:

    git push origin --delete some-feature

    在Git配置文件中增加幾行,你就可以更改git fetch的行為。默認地,git fetch會fetch遠程倉庫中所有分支。原因就是.git/config文件的這段配置:

    [remote "origin"]
        url = https://git@github.com:mary/example-repo.git
        fetch = +refs/heads/*:refs/remotes/origin/*

    fetch這一行告訴git fetch從origin倉庫中下載所有分支。但是,一些工作流不需要所有分支。比如,很多持續集成工作流只關心master分支。為了做到這一點,我們需要將fetch這行改成下面這樣:

    [remote "origin"]
        url = https://git@github.com:mary/example-repo.git
        fetch = +refs/heads/master:refs/remotes/origin/master

    你還可以類似地修改git push的配置。比如,如果你總是將master分支推送到origin倉庫的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

    refspec給了你完全的掌控權,可以定制Git命令如何在倉庫之間轉移分支。你可以重命名或是刪除你的本地分支,fetch或是push不同的分支名,修改git push和git fetch的設置,只對你想要的分支進行操作。

    相對引用

    你還可以通過提交之間的相對關系來引用。~符號讓你訪問父節點的提交。比如說,下面這個命令顯示HEAD祖父節點的提交:

    git show HEAD~2

    但是,面對合并提交(merge commit)的時候,事情就會變得有些復雜。因為合并提交有多個父節點,所以你可以找到多條回溯的路徑。對于3路合并,第一個父節點是你執行合并時的分支,第二個父節點是你傳給git merge命令的分支。

    ~符號總是選擇合并提交的第一個父節點。如果你想選擇其他父節點,你需要用^符號來指定。比如說,HEAD是一個合并提交,下面這個命令返回HEAD的第二個父節點:

    git show HEAD^2

    你可以使用不止一個^來查看超過一層的節點。比如,下面的命令顯示的是HEAD的祖父節點,也就是HEAD第二個父節點的父節點。

    git show HEAD^2^1

    為了闡明~和^是如何工作的,下面這張圖告訴你如何使用相對引用,來指向任意的提交。有的提交可以通過多種方式引用。

    Accessing commits using relative refs

    相對引用在命令中的用法和普通的引用相同。比如,下面所有命令中使用的都是相對引用:

    # 只列出合并提交的第二個父節點的父節點
    git log HEAD^2

    移除當前分支最新的3個提交

    git reset HEAD~3

    交互式rebase當前分支最新的3個提交

    git rebase -i HEAD~3</pre>

    引用日志

    引用日志是Git的安全網。它記錄了你在倉庫中做的所有更改,不管你有沒有提交。你也可以認為這是你本地更改的完整歷史記錄。運行git reflog命令查看引用日志。它應該會打印出像下面這樣的信息:

    400e4b7 HEAD@{0}: checkout: moving from master to HEAD~2
    0e25143 HEAD@{1}: commit (amend): 將一些很贊的新特性引入`master`
    00f5425 HEAD@{2}: commit (merge): 合并'feature'分支
    ad8621a HEAD@{3}: commit: 結束feature分支開發

    說人話就是:

    • 你剛剛切換到HEAD~2
    • 你剛剛修改了一個提交信息
    • 你剛剛把feature分支合并到了master分支
    • 你剛剛提交了一份緩存
    • </ul>

      HEAD{<n>}語法允許你引用保存在日志中的提交。這和上一節的HEAD~<n>引用差不多,不過<n>指的是引用日志中的對象,而不是提交歷史。

      你可以用辦法回到之前可能已經丟失的狀態。比如,你剛剛用git reset方法粉碎了新的feature分支。你的引用日志看上去可能會是這樣的:

      ad8621a HEAD@{0}: reset: moving to HEAD~3
      298eb9f HEAD@{1}: commit: 一些提交信息
      bbe9012 HEAD@{2}: commit: 繼續開發
      9cb79fa HEAD@{3}: commit: 開始新特性開發

      git reset前的三個提交現在都成了懸掛的了,也就是說除了引用日志之外沒有辦法再引用到它們。現在,假設你意識到了你不應該丟掉你全部的工作。你只需要切換到HEAD@{1}這個提交就能回到你運行git reset之前倉庫的狀態。

      git checkout HEAD@{1}

      這會讓你處于HEAD分離的狀態。你可以從這里開始,創建新的分支,繼續你的工作。

      總結

      你現在對Git提交的引用應該已經相當熟悉了。我們知道了分支和標簽是如何存在于.git的子文件夾refs中,如何讀取打包的引用文件,如何使用refspec來進行更高級的push和fetch操作,如何使用~和^符號來遍歷分支結構。

      我們還了解了引用日志,來引用到其他方式已經不存在的提交。這是一種很好的恢復誤刪提交的方法。

      它的意義在于:在任何開發場景下,你都能找到你需要的特定提交。你很容易就可以把這些技巧用在你一有的Git知識中,因為很多常用的命令都接受引用作為參數,包括git log、git show、git checkout、git reset、git revert、git rebase等等。

      這篇文章是『git-recipes』的一部分,點擊目錄查看所有章節。

      如果你覺得文章對你有幫助,歡迎點擊右上角的Star:star2:Fork:fork_and_knife:

      如果你發現了錯誤,或是想要加入協作,請參閱Wiki協作說明

      </blockquote> 來自:https://github.com/geeeeeeeeek/git-recipes/blob/master/sources/Git提交引用.md

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!