標籤:

聊聊 Git 「改變歷史」

非常感謝你為 mint-ui 修復了這個 issue。不過你的 commit 信息能修改成如下格式嗎?「issue 666: Any message about this issue」。

當我興高采烈向 Element 提交 PR 的時候,維護者告訴我你能把你多個 commits 合併成一個 commit 嗎?我們需要保持提交歷史清晰明了。

修復了一個線上 master 分支的Bug,發現這個 Bug 在當前 dev 分支也是存在的,怎麼將master分支上的 bugfix 的 commit 移植到 dev 分支呢?

其實上面的問題會經常出現在我們的開發過程中,或者是在向一些開源項目提交 PR 的時候。在本篇文章中,我將重現以上問題,聊聊 Git 怎麼改變歷史記錄

重寫最後一次提交

在我們開發的過程中,我們經常會遇到這樣的問題,當我們進行了一次「衝動」的 Git 提交後。發現我們的 commit 信息有誤,或者我們把不應該這次提交的文件添加到了此次提交中,或者有的文件忘記提交了,怎麼辦?這些問題都可以通過如下命令來進行彌補。

git commit --amend

舉個例子,在一個剛初始化的 Git 倉庫中,有如下兩個文件:

total 0n-rw-r--r-- 1 ransixi staff 0B 9 19 16:35 should-commit.jsn-rw-r--r-- 1 ransixi staff 0B 9 19 16:35 should-not-commit.jsn

其中 should-commit.js 文件應該被提交,而 should-not-commit.js 不應該被提交,但是由於「衝動」,我把 should-not-commit.js 文件提交了。

# 其實應該添加 should-commit.js 文件ngit add should-not-commit.jsn# 啊哈,由於筆誤,我把 commit 寫成了 commmitngit commit -m commmit 1n

通過 git log 命令列印下當前的歷史提交記錄:

commit fba6199e7fd5f325cc0bfcec4c599c93603d48f8 (HEAD -> master)nAuthor: ran.luo03 <ran.luo03@ele.me>nDate: Tue Sep 19 16:49:57 2017 +0800nn commmit 1n

這樣的錯誤的提交一定不能夠給別人看到!是時候該祭出 git commit --amend 了。

# 首先,需要將 should-commit.js 文件添加到暫存區ngit add should-commit.jsn# 其次,將 should-not-commit.js 文件從已暫存狀態轉為未暫存狀態,不會刪除 should-not-commit.js 文件。ngit rm --cache should-not-commit.jsn# 最後,通過git commit --amend 修改提交信息ngit commit--amendn

當鍵入 git commit —amend 命令後,會打開 Git 默認編輯器,內容包括了上次錯誤提交的信息,我們只需將 commmit 1 改為 commit 1 就行了,然後保存退出編輯器。這樣我們就完成了錯誤提交的修改,讓我們再通過 git log 來查看一下歷史提交記錄:

commit 2a410384e14dadaff9b98f823b9f239da055637d (HEAD -> master)nAuthor: ran.luo03 <ran.luo03@ele.me>nDate: Tue Sep 19 16:49:57 2017 +0800nn commit 1n

啊哈,整個歷史記錄中只有我最新修改後的歷史提交,你完全找不到上一次的提交蹤跡了。是不是很酷呢?

思考1:怎麼使用 git reset 命令修改最後一次提交記錄?

提交的刪除、排序、合併操作

在一個大型項目中,為了保持提交歷史的簡潔和可逆,往往一個功能點或者一個 bug fix 對應一個提交,但是在我們實際開發的過程中,我們並不是完成整個功能才進行一次提交的,往往是開發了功能點的一部分,就需要給小夥伴們進行 code review,小的 commit 保證了 code review 的效率和準確性,想像一下如果一次給小夥伴 review 上千行代碼,幾十個文件,他一定會瘋掉的。同時 code review 後的反饋,我們可能需要修改代碼,然後再次提交。但是這些提交之間的反覆修改不應該體現在最終的 PR 上面,因此, 我們需要根據功能點的前後對 commit 進行排序,對相同功能的commits 進行合併,並刪除一些不需要的 commit,根據最終的提交歷史提 PR。

舉個例子,將 王之渙 的 登鸛雀樓 摘抄到我的讀書筆記中。

首先創建 poem 文件,將「黃河入海流」這句詩添加到了文件中,創建第一個 commit 如下:

通過 git log --oneline 命令來看看提交記錄。

da5ee49 (HEAD -> master) add 黃河入海流n

後來覺得,摘抄一句有些單調,不如將其前面一句也摘抄到筆記中吧,於是又出現了第二個 commit 如下:

622c3c8 (HEAD -> master) add 白日依山盡nda5ee49 add 黃河入海流n

...

覺得自己太隨性,摘抄一首詩竟然添加了如此之多的 commits,commits 如下:

953aabb (HEAD -> master) add 文章出處n7fad941 add 摘抄時間n731d00b add 作者:王煥之n9a22044 add 標題:登鸛雀樓n4fee22a add 更上一層樓nd1293c5 add 欲窮千里目n622c3c8 add 白日依山盡nda5ee49 add 黃河入海流n

再看看上面的提交歷史,覺得如此多的 commits 確實有些冗餘了,commits 的順序似乎也有些問題,因為 commits 的順序並不是按照正常摘抄一首詩的順序來組織的。而且覺得添加摘抄時間有些多餘了,因為 git 的歷史提交記錄就已經幫我記錄了添加時間。

讓我們來一步一步通過「重寫歷史」來解決上面的問題。

這次我使用的命令是 git rebase -i 或者 git rebase - -interactive, Git 官方文檔對其如下解釋:

Make a list of the commits which are about to be rebased. Let the user edit that list before rebasing. This mode can also be used to split commits

可以看出,該命令羅列了將要 rebase 的提交記錄,打開 Git 設置的編輯器,讓用戶有更多的選擇,可以進行 commit 合併,對 commits 重新排序,刪除 commit 等。

第一步:刪除「add 摘抄時間」commit

運行命令

git rebase -i HEAD~2

Git 打開默認編輯器,出來如下對話信息:

pick 7fad941 add 摘抄時間npick 953aabb add 文章出處nn# Rebase 731d00b..953aabb onto 731d00b (2 commands)n#n# Commands:n# p, pick = use commitn# r, reword = use commit, but edit the commit messagen# e, edit = use commit, but stop for amendingn# s, squash = use commit, but meld into previous commitn# f, fixup = like "squash", but discard this commits log messagen# x, exec = run command (the rest of the line) using shelln# d, drop = remove commitn

上面的對話信息中包含七條可選命令,很明顯最後一條 d,drop 正是我需要的,因為我正打算刪除 commit。於是我把第一行中的 pick 命令改為了 drop 命令。

drop 7fad941 add 摘抄時間npick 953aabb add 文章出處n

保存並退出編輯器。

Auto-merging poemnCONFLICT (content): Merge conflict in poemnerror: could not apply 953aabb... add 文章出處nnWhen you have resolved this problem, run "git rebase --continue".nIf you prefer to skip this patch, run "git rebase --skip" instead.nTo check out the original branch and stop rebasing, run "git rebase --abort".nnCould not apply 953aabb... add 文章出處n

OMG! 竟然竟然提示 poem 文件中有衝突!打開 poem 文件,手動刪除不需要的內容及衝突的標記符號,按照上面的提示,運行 git rebase --continue 命令。心想,這下總該好了吧!

poem: needs mergenYou must edit all merge conflicts and thennmark them as resolved using git addn

rebase 依然沒有成功,原來忘記將解決衝突的修改添加到暫存區了,通過運行 git add 命令後,再次執行 git rebase —continue。

出來一個對話框,提示我可以修改 commit 信息,我沒有修改,直接保存退出。來看看此時的提交歷史記錄。

b8f0233 (HEAD -> master) add 文章出處n731d00b add 作者:王煥之n9a22044 add 標題:登鸛雀樓n4fee22a add 更上一層樓nd1293c5 add 欲窮千里目n622c3c8 add 白日依山盡nda5ee49 add 黃河入海流n

和之前的 commits log 信息進行對比,發現 「7fad941 add 摘抄時間」 提交,已經被我成功地刪除了,雖然期間有些波折。同時我還注意到了,「add 文章出處」的 SHA1的 hash 值也從 953aabb 變成了 b8f0233。說明,該 commit 是新創建的 commit。

第二步:調整 commits 順序

看著上面提交歷史記錄總會有些彆扭,因為不是按照詩本身的順序來進行提交的,現在我需要修改提交的順序。好吧,又該是 git rebase -i 命令大顯身手的時候到了。

但是現在有個問題,git rebase -i 命令並不能夠編輯第一個提交。不巧的是,我正需要改變第一個 commit 的順序,這兒需要一點小技巧,用到 --root 選項,通過該選項,我們就能夠編輯初始化的提交了。運行命令如下:

git rebase -i —root

Git 再次打開編輯器,提示如下對話信息:

pick da5ee49 add 黃河入海流npick 622c3c8 add 白日依山盡npick d1293c5 add 欲窮千里目npick 4fee22a add 更上一層樓npick 9a22044 add 標題:登鸛雀樓npick 731d00b add 作者:王煥之npick b8f0233 add 文章出處nn# Rebase b8f0233 onto a69da76 (7 commands)n#n# Commands:n# p, pick = use commitn# r, reword = use commit, but edit the commit messagen# e, edit = use commit, but stop for amendingn# s, squash = use commit, but meld into previous commitn# f, fixup = like "squash", but discard this commits log messagen# x, exec = run command (the rest of the line) using shelln# d, drop = remove commitn# ...n

修改上面的提交順序如下:

pick 622c3c8 add 白日依山盡npick da5ee49 add 黃河入海流npick d1293c5 add 欲窮千里目npick 4fee22a add 更上一層樓npick 9a22044 add 標題:登鸛雀樓npick 731d00b add 作者:王煥之npick b8f0233 add 文章出處n...n

然後保存並退出編輯器。

OMG, 依然存在衝突,通過第一步的方法,解決衝突,運行 git add . 和 git rebase —continue。最後來看看現在的歷史提交記錄:

ddb6576 (HEAD -> master) add 文章出處na6e40b3 add 作者:王煥之nce83346 add 標題:登鸛雀樓ncae4916 add 更上一層樓nf79b9ac add 欲窮千里目nfb65570 add 黃河入海流n8e25185 add 白日依山盡n

第三步:合併 commits

添加標題和添加作者貌似應該放到一個 commit 裡面,也就是說,我需要將「a6e40b3 add 作者:王煥之」 和 「ce83346 add 標題:登鸛雀樓」 合併成一個提交。這樣顯得提交更加簡潔明晰。

依然使用 git rebase -i 命令 :

git rebase -i HEAD~3

Git 大概如下對話框:

pick ce83346 add 標題:登鸛雀樓npick a6e40b3 add 作者:王煥之npick ddb6576 add 文章出處nn# Rebase cae4916..ddb6576 onto cae4916 (3 commands)n#n# Commands:n# p, pick = use commitn# r, reword = use commit, but edit the commit messagen# e, edit = use commit, but stop for amendingn# s, squash = use commit, but meld into previous commitn# f, fixup = like "squash", but discard this commits log messagen# x, exec = run command (the rest of the line) using shelln# d, drop = remove commitn# ...n

這次我使用的命令是 s, squash。該命令用於合併兩個或多個 commits,會將選擇的 commit 合併到前一個 commt 中。修改上面對話第二行如下:

pick ce83346 add 標題:登鸛雀樓nsquash a6e40b3 add 作者:王煥之npick ddb6576 add 文章出處n

然後保存並退出編輯器,啊哈,Git 似乎有點疑惑,它並不知道選擇哪個 commit 信息作為合併的最終 commit 信息,於是 Git 打開了新的對話框,讓我自己輸入新的合併提交信息。

# This is a combination of 2 commits.n# This is the 1st commit message:nnadd 標題:登鸛雀樓nn# This is the commit message #2:nnadd 作者:王煥之nn# ...n

修改如下:

# This is a combination of 2 commits.n# This is the 1st commit message:nnadd 標題:登鸛雀樓 作者:王煥之nn# This is the commit message #2:nn# add 作者:王煥之nn# ...n

保存上面的修改,並退出編輯器。

再來看看最後的歷史提交記錄。

* b907e51 - (2 hours ago) add 文章出處 - ran.luo (HEAD -> master)n* bd0bfed - (3 hours ago) add 標題:登鸛雀樓 作者:王煥之 - ran.luon* cae4916 - (3 hours ago) add 更上一層樓 - ran.luon* f79b9ac - (3 hours ago) add 欲窮千里目 - ran.luon* fb65570 - (3 hours ago) add 黃河入海流 - ran.luon* 8e25185 - (3 hours ago) add 白日依山盡 - ran.luon

啊哈,該歷史提交記錄終於是我想要的了。

思考2:假如通過 rebase 合併了多個 commits 後,發現並不是我們想要的結果,怎麼使用 git reset 將其恢復到合併前狀態?

思考3: 在上面的例子中,由於 git rebase -i 不能夠直接編輯第一個提交記錄,因而使用了 --root 選項,那麼有沒有什麼方法可以在最初的 commit 之前添加一個 root commit 呢?這樣 git rebase -i 就可以直接使用了。

思考4:在上面的例子中是分步驟進行提交的刪除、排序和合併的,那麼可不可以在一次 git

rebase -i 的操作中全部完成呢?當然你可能面臨著更多的衝突需要解決。

將其他分支的某個提交附加到當前分支

還記得文章開頭提及的那個問題嗎?修復了一個 master 分支上的線上 Bug,完成了項目的測試發布後,發現當前開發分支 dev 也存在同樣的問題,怎麼辦?是把修復 Bug 的代碼從 master 分支上線複製一遍到 dev 分支上,這顯然效率不高,而且容易複製錯誤。還是以一個最小的例子來分析 Git 怎麼幫我們解決這個問題。

當前版本庫有兩個分支,master 分支和 dev 分支,master 分支包含一個文件 file1,已經發布到線上,dev 分支是從 master 分支上分離出來的一個新的分支,並且已經完成了新功能的開發,添加了另外一個文件 file2。當前的提交圖如下:

* 132cabb - (4 minutes ago) dev add file2 - ran.luo (dev)n* daaae54 - (4 minutes ago) add file1 - ran.luo (HEAD -> master)n

上面代碼可以看出,當前 HEAD 指向 master 分支,並且發現一個線上 bug,需要緊急修復,我對 file1文件內容進行修改,修復了該 bug。並提交一個新的 commit。當前的提交圖如下:

* c6607dc - (4 seconds ago) master fix bug - ran.luo (HEAD -> master)n| * 132cabb - (6 minutes ago) dev add file2 - ran.luo (dev)n|/n* daaae54 - (7 minutes ago) add file1 - ran.luon

因為 dev 分支是從 master 分支上分離出來的新分支,因此先前 master 分支上面的 bug 在 dev 分支上也存在,但是又有誰想再次手寫代碼修復一遍 bug 呢?這時候我們就需要用到 git cherry-pick 命令。Git 官方文檔對其解釋如下:

git-cherry-pick - Apply the changes introduced by some existing commits

由官網文檔可知,git-cherry-pick 命令常用於將版本庫的一個分支上的特定提交引入到另一個分支上,也就是說,其可以將其他分支帶來的改變直接作用到當前分支,這不就是本例所需要的嗎?

首先需要切換到 dev 分支,由於我們需要的是版本庫中 master 分支上面的最新的一個關於 bug fix 的提交,將其附加到 dev 分支後面,使用如下命令:

git cherry-pick master

執行完畢後,我們切回 master 分支,再來看看當前的提交圖:

* 439cb35 - (14 minutes ago) master fix bug - ran.luo (dev)n* 132cabb - (20 minutes ago) dev add file2 - ran.luon| * c6607dc - (14 minutes ago) master fix bug - ran.luo (HEAD -> master)n|/n* daaae54 - (21 minutes ago) add file1 - ran.luon

啊哈,成功地將 master 分支的最新提交附加到了 dev 分支上面,又雙叒叕一次改變了歷史,心中的自豪感悠然而生。

思考5:既然 git cherry-pick 可以將某一分支上面的制定提交附加到當前分支上線,那麼這樣是否可能通過不同的操作順序來對將要附加的提交進行排序呢?

思考6:有時候可能一次需要將版本庫中某一分支上面的多個連續的提交一次性地附加到當前分支上面,git cherry-pick (git cherry-pick X..Y)命令是否也能夠滿足我們的需求呢?

寫在最後

當我還沉浸在改變歷史的成就中難以自拔的時候,身邊大佬的一句話讓我清醒過來:「歷史(記錄)沒有因你而變,而只是改變了歷史(記錄)的呈現方式」。當我查閱了.git/objects中的關於記錄 commit 的文件後,才發現我還是too young too simple。我並沒有改變或刪除這些記錄 commit 的文件,而只是生成了一些新的 commit 文件,盡然以為我改變了歷史記錄,可笑!這也是我們為什麼能夠恢復到改變歷史記錄前狀態的原因,關於Git 中 hash、commit、history 的實質,請參考 git inside --simplified --part 1。

Warning

改變歷史提交記錄並非完美,你需要遵循如下準則,只要沒有其他開發人員獲取到你版本庫的副本,或者沒有共享你的提交記錄,那麼你就可以盡情的完善你的提交記錄,可以修改提交信息,合併或者拆分多個提交,對多個提交進行排序等等。不過,記住一點,如果你的版本庫已經公開,並且其他開發人員已經共享了你的提交記錄,那麼你就不應該重寫、修改該版本庫中的任意部分。否則,你的合作者會埋怨你,你的家人和朋友也會嘲笑你、拋棄你。

推薦閱讀:

Git由淺入深之存儲原理
git合併分支,為什麼會比svn容易?
Git算不算程序員的必備技能?
git 怎麼在倉庫裡面上傳一個文件夾到github?

TAG:Git |