從撤銷 rebase 談談 git 原理

假設咱們有兩個分支,a 和 b,它們的提交都有一個相同的父提交(master 指向的那次提交)。如圖所示:git

如今咱們在分支 b 上,而後 rabase 到分支 a 上。如圖所示:shell

平時開發中常常遇到這種狀況,假設分支 a 和 b 是兩個獨立的 feature 分支,可是不當心被咱們錯誤的 rebase 了。如今至關於兩個 feature 分支中本來獨立的業務被揉起來了,固然是咱們不想看到的結果,那麼如何撤銷呢?bash

一種方案是利用 reflog 命令。spa

利用 reflog 撤銷變基

咱們先不考慮原理,直接上解決方案,首先輸入 git reflog,你會看到以下圖所示的日誌:3d

最後的輸出實際上是最先的操做,咱們逐條分析下:版本控制

  1. HEAD@{8}: 這裏咱們建立了初始的提交
  2. HEAD@{7}:檢出了分支 a
  3. HEAD@{6}:在分支 a 上作了一次提交,注意 master 分支沒有變更
  4. HEAD@{5}:從分支 a 回到分支 master,至關於向後退了一次
  5. HEAD@{4}:檢出了分支 b
  6. HEAD@{3}:在分支 b 上作了一次提交,注意 master 分支沒有變更
  7. HEAD@{2}:這一步開始變基到分支 a,首先切換到分支 a 上
  8. HEAD@{1}:把分支 b 對應的那次提交變基到分支 a 上
  9. HEAD@{0}:變基結束,由於是在 b 上發起的變基,因此最後還切回分支 b

若是咱們想撤銷這次 rebase,只要輸入如下命令就能夠了:指針

git reset --hard HEAD@{3}
複製代碼

此時再看,已經「恢復」到 rebase 前的狀態了。的是否是感受很神奇呢,先彆着急,後面會介紹這麼作的原理。日誌

git 工做原理簡介

爲了搞懂 git 是如何工做的,以及這些命令背後的原理,我想有必要對 git 的模型有基礎的瞭解。code

首先,每個 git 目錄都有一個名爲 .git 的隱藏目錄,關於 git 的一切都存儲於這個目錄裏面(全局配置除外)。這個目錄裏面有一些子目錄和文件,文件其實不重要,都是一些配置信息,後面會介紹其中的 HEAD 文件。子目錄有如下幾個:cdn

  1. info:這個目錄不重要,裏面有一個 exclude 文件和 .gitignore 文件的做用類似,區別是這個文件不會被歸入版本控制,因此能夠作一些我的配置。
  2. hooks:這個目錄很容易理解, 主要用來放一些 git 鉤子,在指定任務觸發先後作一些自定義的配置,這是另一個單獨的話題,本文不會具體介紹。
  3. objects:用於存放全部 git 中的對象,下面單獨介紹。
  4. logs:用於記錄各個分支的移動狀況,下面單獨介紹。
  5. refs:用於記錄全部的引用,下面單獨介紹。

本文主要會介紹後面三個文件夾的做用。

git 對象

git 是面向對象的! git 是面向對象的! git 是面向對象的!

沒錯,git 是面向對象的,並且不少東西都是對象。我舉個簡單的例子,來幫助你們理解這個概念。假設咱們在一個空倉庫裏,編輯了 2 個文件,而後提交。此時都會有那些對象呢?

首先會有兩個數據對象,每一個文件都對應一個數據對象。當文件被修改時,即便是新增了一個字母,也會生成一個新的數據對象。

其次,會有一個樹對象用來維護一系列的數據對象,叫樹對象的緣由是它持有的不只能夠是數據對象,還能夠是另外一個樹對象。好比某次提交了兩個文件和一個文件夾,那麼樹對象裏面就有三個對象,兩個是數據對象,文件夾則用另外一個樹對象表示。這樣遞歸下去就能夠表示任意層次的文件了。

最後則是提交對象,每一個提交對象都有一個樹對象,用來表示某一次提交所涉及的文件。除此之外,每個提交還有本身的父提交,指向上一次提交的對象。固然,提交對象還會包含提交時間、提交者姓名、郵箱等輔助信息,就很少說了。

假設咱們只有一個分支,以上知識點就足夠解釋 git 的提交歷史是如何計算的了。它並不存儲完整的提交歷史,而是經過父提交的對象不斷向前查找,得出完整的歷史。

注意開頭那張圖片,分支 b 指向的提交是 9cbb015,不妨來看下它是何方神聖:

git cat-file -t 9cbb015
git cat-file -p 9cbb015
複製代碼

這裏咱們使用 cat-file 命令,其中 -t 參數打印對象的類型,-p 參數會智能識別類型,並打印其中的內容。輸出結果如圖所示:

可見 9cbb015 是一個提交對象,裏面包含了樹對象、父提交對象和各類配置信息。咱們能夠再打印樹對象看看:

這表示本次提交只修改了 begin 這個文件,而且輸出了 begin 這個文件對於的數據對象。

git 引用

既然 git 是面向對象的,那麼有沒有指正呢?還真是有的,分支和標籤都是指向提交對象的指針。這一點能夠驗證:

cat .git/refs/heads/a
複製代碼

全部的本地分支都存儲在 git/refs/heads 目錄下,每個分支對應一個文件,文件的內容如圖所示:

可見,4a3a88d 恰好是本文第一張圖中分支 a 所指向的提交。

咱們已經搞明白了 git 分支的祕密,如今有了全部分支的記錄,又有了每次提交的父提交對象,就可以得出像 SourceTree 或者文章開頭第一張圖那樣的提交狀態了。

至於標籤,它其實也是一種引用,能夠理解爲不能移動的分支。只能永遠指向某個固定的提交。

最後一個比較特殊的引用是 HEAD,它能夠理解爲指針的指針,爲了證實這一點,咱們看看 .git/HEAD 文件:

它的內容記錄了當前指向哪一個分支,refs/heads/b 實際上是一個文件,這個文件的內容是分支 b 指向的那個提交對象。理解這一點很是重要,不然你會沒法理解 checkoutreset 的區別。

這兩個命令都會改變 HEAD 的指向,區別是 checkout 不改變 HEAD 指向的分支的指向,而 reset 會。舉個例子, 在分支 b 上執行如下兩個命令都會讓 HEAD 指向 4a3a88d 此次提交(分支 a 指向的提交):

git checkout a
git reset --hard a
複製代碼

checkout 僅改變 HEAD 的指向,不會改變分支 b 的指向。而 reset 不只會改變 HEAD 的指向,還由於 HEAD 指向分支 b,就把 b 也指向 4a3a88d 此次提交。

git 日誌

.git/logs 目錄中,有一個文件夾和一個 HEAD 文件,每當 HEAD 引用改變了指向的位置,就會在 .git/logs/HEAD 中添加了一個記錄。而 .git/logs/refs/heads 這個目錄中則有多個文件,每一個文件對應一個分支,記錄了這個分支 的指向位置發生改變的狀況。

當咱們執行 git reflog 的時候,其實就是讀取了 .git/logs/HEAD 這個文件。

撤銷 rebase 的原理

首先咱們要排除一個誤區,那就是 git 會維護每次提交的提交對象、樹對象和數據對象,但並不會維護每次提交時,各個分支的指向。在介紹分支的那一節中咱們已經看到,分支僅僅是一個保留了提交對象的文件而已,並不記錄歷史信息。即便在上一節中,咱們知道分支的變化信息會被記錄下來,但也不會和某個提交對象綁定。

也就是說,git 中並不存在某次提交時的分支快照

那麼咱們是如何經過 reset 來撤銷 rebase 的呢,這裏還要澄清另外一個事實。前文曾經說過,某個時刻下你經過 SourceTree 或者 git log 看到的分支狀態,實際上是由全部分支的列表、每一個分支所指向的提交,和每一個提交的父提交共同繪製出來的。

首先 git/refs/heads 下的文件告訴咱們有多少分支,每一個文件的內容告訴咱們這個分支指向那個提交,有了這個提交不斷向前追溯就繪製出了這個分支的提交歷史。全部分子的提交歷史也就組成了咱們看到的狀態。

但咱們要明確:不是全部提交對象都能看到的,舉個例子若是咱們把某個分支向前移一次提交,那個分支的提交線就會少一個節點,若是沒有別的提交線包含這個節點,這個節點就看不到了。

因此在 rebase 完成後,咱們覺得看到了下面這樣的提交線:

df0f2c5(master) --- 4a3a88d(a) --- 9cbb015(b)
複製代碼

其實是這樣的:

df0f2c5(master) --- 4a3a88d(a) --- 9d0618e(b)
   |
9cbb015
複製代碼

master 分支上依然有分叉,原來 9cbb015 此次提交依然存在,只不過沒有分支的提交線包含它,因此沒法看到而已。可是經過 reflog,咱們能夠找回 HEAD 頭的每一次移動,因此能看到此次提交。

當咱們執行這個命令時:

git reset --hard HEAD@{3}
複製代碼

再看一次 reflog 的輸出:

HEAD@{3} 實際上是它左側 9cbb015 此次提交的縮寫,因此上述命令等價於:

git reset --hard 9cbb015
複製代碼

前文說過,reset 不只會移動 HEAD,還會移動 HEAD 所指向的分支,因此這個命令的執行結果就是讓 HEAD 和分支 b 同時指向 9cbb015 這個提交,看起來像是撤銷了 rebase。

但別忘了,分支 a 的上面仍是有一次提交的,9d0618e 此次提交僅僅是沒有分支指向它,因此不顯示而已。但它真實的存在着,嚴格意義上來講,咱們並無真正的撤銷這次 rebase

相關文章
相關標籤/搜索