天天看點

Git回退代碼

revert

首先肯定的是 revert,

git revert commit_id

 能産生一個 與 commit_id 完全相反的送出,即 commit_id 裡是添加, revert 送出裡就是删除。但是使用 git log 檢視了送出記錄後,我就打消了這種想法,因為送出次數太多了,中途還有幾次從其他分支的 merge 操作。”利益于”我們不太幹淨的送出記錄,要完成從 C 版本到 N 版本的 revert,我需要倒序執行 revert 操作幾十次,如果其中順序錯了一次,最終結果可能就是不對的。另外我們知道我們在進行代碼 merge 時,也會把 merge 資訊産生一次新的送出,而 revert 這次 merge commit 時需要指定 m 參數,以指定 

mainline

這個 mainline 是主線,也是我們要保留代碼的主分支,從 feature 分支往 develop 分支合并,或由 develop 分支合并到 master 的送出還好确定,但 feature 分支互相合并時,我哪知道哪個是主線啊。

是以 revert 的文案被廢棄了。

Reset(暴力,不保留任何曆史記錄)

reset 也能使代碼回到某次送出,但跟 revert 不同的是, reset 是将送出的 HEAD 指針指到某次送出,之後的送出記錄會消失,就像從沒有過這麼一次送出。但由于我們都在 feature 分支開發,我在 feature 分支上将代碼回退到某次送出後,将其合并到 develop 分支時卻被提示報錯。這是因為 feature 分支回退了送出後,在 git 的 workflow 裡,feature 分支是落後于 develop 分支的,而合并向 develop 分支,又需要和 develop 分支保持最新的同步,需要将 develop 分支的資料合并到 feature 分支上,而合并後,原來被 reset 的代碼又回來了。

這個時候另一個可選項是在 master 分支上執行 reset,使用 

--hard

 選項完全抛棄這些舊代碼,reset 後再強制推到遠端。

master> git reset --hard commit_id
master> git push --force origin master           

但是還是有問題,首先,我們的 master 分支在 gitlab 裡是被保護的,不能使用 force push,畢竟風險挺大了,萬一有人 reset 到最開始的送出再強制 push 的話,雖然可以使用 

reflog

 恢複,但也是一番折騰。

另外,reset 畢竟太野蠻,我們還是想能保留送出曆史,以後排查問題也可以參考。

rebase

有人提出可以先使用 

rebase

 把多個送出合并成一個送出,再使用 revert 産生一次反送出,這種方法的思路非常清晰,把 revert 和 rebase 兩個指令搭配得很好,相當于使用 revert 回退的更新版。

rebase 是”變基”的意思,這裡的”基”,在我了解是指[多次] commit 形成的 git workflow,使用 rebase,我們可以改變這些曆史送出,修改 commit 資訊,将多個 commit 進行組合。

介紹 rebase 的文檔有很多,我們直接來說用它來進行代碼回退的步驟。

  1. 首先,切出一個新分支 F,使用 git log 查詢一下

    要回退到

    的 commit 版本 N。
  2. 使用指令 

    git rebase -i N

    , -i 指定互動模式後,會打開 git rebase 編輯界面,形如:
    pick 6fa5869 commit1
    pick 0b84ee7 commit2
    pick 986c6c8 commit3
    pick 91a0dcc commit4           
  3. 這些 commit 自舊到新由上而下排列,我們隻需要在 commit_id 前添加操作指令即可。

    在合并 commit 這個需求裡,我們可以選擇 

    pick(p)

     最舊的 commit1,然後在後續的 commit_id 前添加 

    squash(s)

     指令,将這些 commits 都合并到最舊的 commit1 上。
  4. 儲存 rebase 結果後,再編輯 commit 資訊,使這次 rebase 失效,git 會将之前的這些 commit 都删除,并将其更改合并為一個新的 commit5

    如果出錯了,也可以使用 

    git rebase --abort/--continue/--edit-todo

    對之前的編輯進行撤銷、繼續編輯。
  5. 這個時候,主分支上的送出記錄是 

    older, commit1, commit2, commit3, commit4

    而 F 分支上的送出記錄是 

    older, commit5

    ,由于 F 分支的祖先節點是 older,明顯落後于主分支的 commit4,将 F 分支向主分支合并是不允許的

    是以我們需要執行 

    git merge master

     将主分支向 F 分支合并,合并後 git 會發現 commit1 到 commit4 送出的内容和 F 分支上 commit5 的修改内容是完全相同的,會自動進行合并,内容不變,但多了一個 commit5。
  6. 再在 F 分支上對 commit5 進行一次 revert 反送出,就實作了把 commit1 到 commit4 的送出全部回退。

這種方法的取巧之處在于巧妙地利用了 rebase 操作曆史送出的功能和 git 識别修改相同自動合并的特性,操作雖然複雜,但曆史送出保留得還算完整。

rebase 這種修改曆史送出的功能非常實用,能夠很好地解決我們遇到的一個小功能送出了好多次才好使,而把 git 曆史弄得亂七八糟的問題,隻需要注意避免在多人同時開發的分支使用就行了。遺憾的是,當天我并沒有了解到 rebase 的這種思想,又由于試了幾個方法都不行太過于慌亂,在 rebase 完成後,向主分支合并被拒之後對這些方式的可行性産生了懷疑,又加上有同僚提出聽起來更可行的方式,就中斷了操作。

檔案操作(推薦,靠譜)

這種更可行的方式就是對檔案操作,然後讓 git 來識别變更,具體是:

  1. 從主分支上切出一個跟主分支完全相同的分支 F。
  2. 從檔案管理系統複制項目檔案夾為 bak,在 bak 内使用 

    git checkout N

     将代碼切到想要的曆史送出,這時候 git 會将 bak 内的檔案恢複到 N 狀态。
  3. 在從檔案管理系統内,将 bak 檔案夾下 

    除了 .git

     檔案夾下的所有内容複制粘貼到原項目目錄下。git 會純從檔案級别識别到變更,然後更新工作區。
  4. 在原項目目錄下執行 

    add 和 commit

    ,完成反送出。

這種方式的巧妙之處在于利用 git 本身對檔案的識别,不牽涉到對 workflow 操作。

小結

最後終于靠着檔案操作方式成功完成了代碼回退,事後想來真是一把心酸淚。

為了讓我的五個小時不白費,複盤一下當時的場景,學習并總結一下四種代碼回退的方式:

  • revert 适合需要回退的曆史送出不多,且無合并沖突的情景。
  • 如果你可以向 master 強推代碼,且想讓 git log 裡不再出現被回退代碼的痕迹,可以使用 

    git reset --hard + git push --force

    的方式。