Git 核心的附加價值之一:編輯歷史記錄

Git 核心的附加價值之一就是編輯歷史記錄的能力。與將歷史記錄視爲神聖的記錄的版本控制系統不同,在 Git 中,我們可以修改歷史記錄以適應我們的需要。這爲我們提供了很多強大的工具,讓我們可以像使用重構來維護良好的軟件設計實踐一樣,編織良好的提交歷史。這些工具對於新手甚至是有經驗的 Git 用戶來說可能會有些令人生畏,但本指南將幫助我們揭開強大的 git-rebase 的神祕面紗。

值得注意的是:一般建議不要修改公共分支、共享分支或穩定分支的歷史記錄。編輯特性分支和個人分支的歷史記錄是可以的,編輯還沒有推送的提交也是可以的。在編輯完提交後,可以使用 git push -f 來強制推送你的修改到個人分支或特性分支。

儘管有這麼可怕的警告,但值得一提的是,本指南中提到的一切都是非破壞性操作。實際上,在 Git 中永久丟失數據是相當困難的。本指南結尾介紹了在犯錯誤時進行糾正的方法。

設置沙盒

我們不想破壞你的任何實際的版本庫,所以在整個指南中,我們將使用一個沙盒版本庫。運行這些命令來開始工作。

git init /tmp/rebase-sandbox
cd /tmp/rebase-sandbox
git commit --allow-empty -m"Initial commit"

如果你遇到麻煩,只需運行rm -rf /tmp/rebase-sandbox,並重新運行這些步驟即可重新開始。本指南的每一步都可以在新的沙箱上運行,所以沒有必要重做每個任務。

修正最近的提交

讓我們從簡單的事情開始:修復你最近的提交。讓我們向沙盒中添加一個文件,並犯個錯誤。

echo "Hello wrold!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"

修復這個錯誤是非常容易的。我們只需要編輯文件,然後用 --amend 提交就可以了,就像這樣:

echo "Hello world!" >greeting.txt
git commit -a --amend

指定 -a 會自動將所有 Git 已經知道的文件進行暫存(例如 Git 添加的),而 --amend 會將更改的內容壓扁到最近的提交中。保存並退出你的編輯器(如果需要,你現在可以修改提交信息)。你可以通過運行 git show 看到修復的提交。

commit f5f19fbf6d35b2db37dcac3a55289ff9602e4d00 (HEAD -> master)
Author: Drew DeVault 
Date: Sun Apr 28 11:09:47 2019 -0400
 
Add greeting.txt
 
diff --git a/greeting.txt b/greeting.txt
new file mode 100644
index 0000000..cd08755
--- /dev/null
+++ b/greeting.txt
@@ -0,0 +1 @@
+Hello world!

修復較舊的提交

--amend 僅適用於最近的提交。如果你需要修正一個較舊的提交會怎麼樣?讓我們從相應地設置沙盒開始:

echo "Hello!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"
 
echo "Goodbye world!" >farewell.txt
git add farewell.txt
git commit -m"Add farewell.txt"

看起來 greeting.txt 像是丟失了 "world"。讓我們正常地寫個提交來解決這個問題:

echo "Hello world!" >greeting.txt
git commit -a -m"fixup greeting.txt"

現在文件看起來正確,但是我們的歷史記錄可以更好一點 —— 讓我們使用新的提交來“修復”(fixup)最後一個提交。爲此,我們需要引入一個新工具:交互式變基。我們將以這種方式編輯最後三個提交,因此我們將運行 git rebase -i HEAD~3(-i 代表交互式)。這樣會打開文本編輯器,如下所示:

pick 8d3fc77 Add greeting.txt
pick 2a73a77 Add farewell.txt
pick 0b9d0bb fixup greeting.txt
 
# Rebase f5f19fb..0b9d0bb onto f5f19fb (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# f, fixup <commit> = like "squash", but discard this commit's log message

是變基計劃,通過編輯此文件,你可以指導 Git 如何編輯歷史記錄。我已經將該摘要削減爲僅與變基計劃這一部分相關的細節,但是你可以在文本編輯器中瀏覽完整的摘要。

當我們保存並關閉編輯器時,Git 將從其歷史記錄中刪除所有這些提交,然後一次執行一行。默認情況下,它將選取(pick)每個提交,將其從堆中召喚出來並添加到分支中。如果我們對此文件根本沒有做任何編輯,則將直接回到起點,按原樣選取每個提交。現在,我們將使用我最喜歡的功能之一:修復(fixup)。編輯第三行,將操作從 pick 更改爲 fixup,並將其立即移至我們要“修復”的提交之後:

pick 8d3fc77 Add greeting.txt
fixup 0b9d0bb fixup greeting.txt
pick 2a73a77 Add farewell.txt

技巧:我們也可以只用 f 來縮寫它,以加快下次的速度。
保存並退出編輯器,Git 將運行這些命令。我們可以檢查日誌以驗證結果:

$ git log -2 --oneline
fcff6ae (HEAD -> master) Add farewell.txt
a479e94 Add greeting.txt

將多個提交壓扁爲一個

在工作時,當你達到較小的里程碑或修復以前的提交中的錯誤時,你可能會發現寫很多提交很有用。但是,在將你的工作合併到 master 分支之前,將這些提交“壓扁”(squash)到一起以使歷史記錄更清晰可能很有用。爲此,我們將使用“壓扁”(squash)操作。讓我們從編寫一堆提交開始,如果要加快速度,只需複製並粘貼這些:

git checkout -b squash
for c in H e l l o , ' ' w o r l d; do
echo "$c" >>squash.txt
git add squash.txt
git commit -m"Add '$c' to squash.txt"
done

要製作出一個寫着 “Hello,world” 的文件,要做很多事情!讓我們開始另一個交互式變基,將它們壓扁在一起。請注意,我們首先簽出了一個分支來進行嘗試。因此,因爲我們使用 git rebase -i master 進行的分支,我們可以快速變基所有提交。結果:

pick 1e85199 Add 'H' to squash.txt
pick fff6631 Add 'e' to squash.txt
pick b354c74 Add 'l' to squash.txt
pick 04aaf74 Add 'l' to squash.txt
pick 9b0f720 Add 'o' to squash.txt
pick 66b114d Add ',' to squash.txt
pick dc158cd Add ' ' to squash.txt
pick dfcf9d6 Add 'w' to squash.txt
pick 7a85f34 Add 'o' to squash.txt
pick c275c27 Add 'r' to squash.txt
pick a513fd1 Add 'l' to squash.txt
pick 6b608ae Add 'd' to squash.txt
 
# Rebase 1af1b46..6b608ae onto 1af1b46 (12 commands)
#
# Commands:
# p, pick <commit> = use commit
# s, squash <commit> = use commit, but meld into previous commit

技巧:你的本地 master 分支獨立於遠程 master 分支而發展,並且 Git 將遠程分支存儲爲 origin/master。結合這種技巧,git rebase -i origin/master 通常是一種非常方便的方法,可以變基所有尚未合併到上游的提交!
我們將把所有這些更改壓扁到第一個提交中。爲此,將第一行除外的每個“選取”(pick)操作都更改爲“壓扁”(squash),如下所示:

pick 1e85199 Add 'H' to squash.txt
squash fff6631 Add 'e' to squash.txt
squash b354c74 Add 'l' to squash.txt
squash 04aaf74 Add 'l' to squash.txt
squash 9b0f720 Add 'o' to squash.txt
squash 66b114d Add ',' to squash.txt
squash dc158cd Add ' ' to squash.txt
squash dfcf9d6 Add 'w' to squash.txt
squash 7a85f34 Add 'o' to squash.txt
squash c275c27 Add 'r' to squash.txt
squash a513fd1 Add 'l' to squash.txt
squash 6b608ae Add 'd' to squash.txt

保存並關閉編輯器時,Git 會考慮片刻,然後再次打開編輯器以修改最終的提交消息。你會看到以下內容:

# This is a combination of 12 commits.
# This is the 1st commit message:
 
Add 'H' to squash.txt
 
# This is the commit message #2:
 
Add 'e' to squash.txt
 
# This is the commit message #3:
 
Add 'l' to squash.txt
 
# This is the commit message #4:
 
Add 'l' to squash.txt
 
# This is the commit message #5:
 
Add 'o' to squash.txt
 
# This is the commit message #6:
 
Add ',' to squash.txt
 
# This is the commit message #7:
 
Add ' ' to squash.txt
 
# This is the commit message #8:
 
Add 'w' to squash.txt
 
# This is the commit message #9:
 
Add 'o' to squash.txt
 
# This is the commit message #10:
 
Add 'r' to squash.txt
 
# This is the commit message #11:
 
Add 'l' to squash.txt
 
# This is the commit message #12:
 
Add 'd' to squash.txt
 
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Sun Apr 28 14:21:56 2019 -0400
#
# interactive rebase in progress; onto 1af1b46
# Last commands done (12 commands done):
# squash a513fd1 Add 'l' to squash.txt
# squash 6b608ae Add 'd' to squash.txt
# No commands remaining.
# You are currently rebasing branch 'squash' on '1af1b46'.
#
# Changes to be committed:
# new file: squash.txt
#

默認情況下,這是所有要壓扁的提交的消息的組合,但是像這樣將其保留肯定不是你想要的。不過,舊的提交消息在編寫新的提交消息時可能很有用,所以放在這裏以供參考。

提示:你在上一節中瞭解的“修復”(fixup)命令也可以用於此目的,但它會丟棄壓扁的提交的消息。
讓我們刪除所有內容,並用更好的提交消息替換它,如下所示:

Add squash.txt with contents "Hello, world"
 
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Sun Apr 28 14:21:56 2019 -0400
#
# interactive rebase in progress; onto 1af1b46
# Last commands done (12 commands done):
# squash a513fd1 Add 'l' to squash.txt
# squash 6b608ae Add 'd' to squash.txt
# No commands remaining.
# You are currently rebasing branch 'squash' on '1af1b46'.
#
# Changes to be committed:
# new file: squash.txt
#

保存並退出編輯器,然後檢查你的 Git 日誌,成功!

commit c785f476c7dff76f21ce2cad7c51cf2af00a44b6 (HEAD -> squash)
Author: Drew DeVault
Date: Sun Apr 28 14:21:56 2019 -0400
 
Add squash.txt with contents "Hello, world"

在繼續之前,讓我們將所做的更改拉入 master 分支中,並擺脫掉這一草稿。我們可以像使用 git merge 一樣使用 git rebase,但是它避免了創建合併提交:

git checkout master
git rebase squash
git branch -D squash

除非我們實際上正在合併無關的歷史記錄,否則我們通常希望避免使用 git merge。如果你有兩個不同的分支,則 git merge 對於記錄它們合併的時間非常有用。在正常工作過程中,變基通常更爲合適。

 

將一個提交拆分爲多個

 

有時會發生相反的問題:一個提交太大了。讓我們來看一看拆分它們。這次,讓我們寫一些實際的代碼。從一個簡單的 C 程序開始(你仍然可以將此代碼段複製並粘貼到你的 shell 中以快速執行此操作):

cat <<EOF >main.c
int main(int argc, char *argv[]) {
return 0;
}
EOF

首先提交它:

git add main.c
git commit -m"Add C program skeleton"

然後把這個程序擴展一些:

cat <<EOF >main.c
#include &ltstdio.h>
 
const char *get_name() {
static char buf[128];
scanf("%s", buf);
return buf;
}
 
int main(int argc, char *argv[]) {
printf("What's your name? ");
const char *name = get_name();
printf("Hello, %s!\n", name);
return 0;
}
EOF

提交之後,我們就可以準備學習如何將其拆分:

git commit -a -m"Flesh out C program"

第一步是啓動交互式變基。讓我們用 git rebase -i HEAD~2 來變基這兩個提交,給出的變基計劃如下:

pick 237b246 Add C program skeleton
pick b3f188b Flesh out C program
 
# Rebase c785f47..b3f188b onto c785f47 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# e, edit <commit> = use commit, but stop for amending

將第二個提交的命令從 pick 更改爲 edit,然後保存並關閉編輯器。Git 會考慮一秒鐘,然後向你建議:

Stopped at b3f188b... Flesh out C program
You can amend the commit now, with
 
git commit --amend
 
Once you are satisfied with your changes, run
 
git rebase --continue

可以按照以下說明爲提交添加新的更改,但我們可以通過運行 git reset HEAD^ 來進行“軟重置” 3。如果在此之後運行 git status,你將看到它取消了提交最新的提交,並將其更改添加到工作樹中:

Last commands done (2 commands done):
pick 237b246 Add C program skeleton
edit b3f188b Flesh out C program
No commands remaining.
You are currently splitting a commit while rebasing branch 'master' on 'c785f47'.
(Once your working directory is clean, run "git rebase --continue")
 
Changes not staged for commit:
(use "git add ..." to update what will be committed)
(use "git checkout -- ..." to discard changes in working directory)
 
modified: main.c
 
no changes added to commit (use "git add" and/or "git commit -a")

爲了對此進行拆分,我們將進行交互式提交。這使我們能夠選擇性地僅提交工作樹中的特定更改。運行 git commit -p 開始此過程,你將看到以下提示:

diff --git a/main.c b/main.c
index b1d9c2c..3463610 100644
--- a/main.c
+++ b/main.c
@@ -1,3 +1,14 @@
+#include &ltstdio.h>
+
+const char *get_name() {
+ static char buf[128];
+ scanf("%s", buf);
+ return buf;
+}
+
int main(int argc, char *argv[]) {
+ printf("What's your name? ");
+ const char *name = get_name();
+ printf("Hello, %s!\n", name);
return 0;
}
Stage this hunk [y,n,q,a,d,s,e,?]?

Git 僅向你提供了一個“大塊”(即單個更改)以進行提交。不過,這太大了,讓我們使用 s 命令將這個“大塊”拆分成較小的部分。

Split into 2 hunks.
@@ -1 +1,9 @@
+#include <stdio.h>
+
+const char *get_name() {
+ static char buf[128];
+ scanf("%s", buf);
+ return buf;
+}
+
int main(int argc, char *argv[]) {
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

提示:如果你對其他選項感到好奇,請按 ? 彙總顯示。
這個大塊看起來更好:單一、獨立的更改。讓我們按 y 來回答問題(並暫存那個“大塊”),然後按 q 以“退出”交互式會話並繼續進行提交。會彈出編輯器,要求輸入合適的提交消息。

Add get_name function to C program
 
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto c785f47
# Last commands done (2 commands done):
# pick 237b246 Add C program skeleton
# edit b3f188b Flesh out C program
# No commands remaining.
# You are currently splitting a commit while rebasing branch 'master' on 'c785f47'.
#
# Changes to be committed:
# modified: main.c
#
# Changes not staged for commit:
# modified: main.c
#

保存並關閉編輯器,然後我們進行第二次提交。我們可以執行另一次交互式提交,但是由於我們只想在此提交中包括其餘更改,因此我們將執行以下操作:

git commit -a -m"Prompt user for their name"
git rebase --continuegit commit -a -m"Prompt user for their name"
git rebase --continue

最後一條命令告訴 Git 我們已經完成了此提交的編輯,並繼續執行下一個變基命令。這樣就行了!運行 git log 來查看你的勞動成果:

$ git log -3 --oneline
fe19cc3 (HEAD -> master) Prompt user for their name
659a489 Add get_name function to C program
237b246 Add C program skeleton

 

重新排序提交

 

這很簡單。讓我們從設置沙箱開始:

echo "Goodbye now!" >farewell.txt
git add farewell.txt
git commit -m"Add farewell.txt"
 
echo "Hello there!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"
 
echo "How're you doing?" >inquiry.txt
git add inquiry.txt
git commit -m"Add inquiry.txt"

現在 git log 看起來應如下所示:

f03baa5 (HEAD -> master) Add inquiry.txt
a4cebf7 Add greeting.txt
90bb015 Add farewell.txt

顯然,這都是亂序。讓我們對過去的 3 個提交進行交互式變基來解決此問題。運行 git rebase -i HEAD~3,這個變基規劃將出現:

pick 90bb015 Add farewell.txt
pick a4cebf7 Add greeting.txt
pick f03baa5 Add inquiry.txt
 
# Rebase fe19cc3..f03baa5 onto fe19cc3 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
#
# These lines can be re-ordered; they are executed from top to bottom.

現在,解決方法很簡單:只需按照你希望提交出現的順序重新排列這些行。應該看起來像這樣:

pick a4cebf7 Add greeting.txt
pick f03baa5 Add inquiry.txt
pick 90bb015 Add farewell.txt

保存並關閉你的編輯器,而 Git 將爲你完成其餘工作。請注意,在實踐中這樣做可能會導致衝突,參看下面章節以獲取解決衝突的幫助。

git pull –rebase

如果你一直在由上游更新的分支 <branch>(比如說原始遠程)上做一些提交,通常 git pull 會創建一個合併提交。在這方面,git pull 的默認行爲等同於:

git fetch origin <branch>
git merge origin/<branch>

假設本地分支 <branch> 配置爲從原始遠程跟蹤 <branch> 分支,即:

$ git config branch.<branch>.remote
origin
$ git config branch.<branch>.merge
refs/heads/<branch>

還有另一種選擇,它通常更有用,並且會讓歷史記錄更清晰:git pull --rebase。與合併方式不同,這基本上 4 等效於以下內容:

git fetch origin
git rebase origin/<branch>

合併方式更簡單易懂,但是如果你瞭解如何使用 git rebase,那麼變基方式幾乎可以做到你想要做的任何事情。如果願意,可以將其設置爲默認行爲,如下所示:

git config --global pull.rebase true

當你執行此操作時,從技術上講,你在應用我們在下一節中討論的過程……因此,讓我們也解釋一下故意執行此操作的含義。

使用 git rebase 來變基

具有諷刺意味的是,我最少使用的 Git 變基功能是它以之命名的功能:變基分支。假設你有以下分支:

A--B--C--D--> master
\--E--F--> feature-1
\--G--> feature-2

事實證明,feature-2 不依賴於 feature-1 的任何更改,它依賴於提交 E,因此你可以將其作爲基礎脫離 master。因此,解決方法是:

git rebase --onto master feature-1 feature-2

非交互式變基對所有牽連的提交都執行默認操作(pick) ,它只是簡單地將不在 feature-1 中的 feature-2 中提交重放到 master 上。你的歷史記錄現在看起來像這樣:

A--B--C--D--> master
| \--G--> feature-2
\--E--F--> feature-1

解決衝突

解決合併衝突的詳細信息不在本指南的範圍內,將來請你注意另一篇指南。假設你熟悉通常的解決衝突的方法,那麼這裏是專門適用於變基的部分。

有時,在進行變基時會遇到合併衝突,你可以像處理其他任何合併衝突一樣處理該衝突。Git 將在受影響的文件中設置衝突標記,git status 將顯示你需要解決的問題,並且你可以使用 git add 或 git rm 將文件標記爲已解決。但是,在 git rebase 的上下文中,你應該注意兩個選項。

首先是如何完成衝突解決。解決由於 git merge 引起的衝突時,與其使用 git commit 那樣的命令,更適當的變基命令是 git rebase --continue。但是,還有一個可用的選項:git rebase --skip。 這將跳過你正在處理的提交,它不會包含在變基中。這在執行非交互性變基時最常見,這時 Git 不會意識到它從“其他”分支中提取的提交是與“我們”分支上衝突的提交的更新版本。

幫幫我! 我把它弄壞了!

毫無疑問,變基有時會很難。如果你犯了一個錯誤,並因此而丟失了所需的提交,那麼可以使用 git reflog 來節省下一天的時間。運行此命令將向你顯示更改一個引用(即分支和標記)的每個操作。每行顯示你的舊引用所指向的內容,你可對你認爲丟失的 Git 提交執行 git cherry-pick、git checkout、git show 或任何其他操作。

我們添加了一個空的初始提交以簡化本教程的其餘部分,因爲要對版本庫的初始提交進行變基需要特殊的命令(即git rebase --root)。

如果要編譯此程序,請運行 cc -o main main.c,然後運行 ./main 查看結果。

實際上,這是“混合重置”。“軟重置”(使用 git reset --soft 完成)將暫存更改,因此你無需再次 git add 添加它們,並且可以一次性提交所有更改。這不是我們想要的。我們希望選擇性地暫存部分更改,以拆分提交。

實際上,這取決於上游分支本身是否已變基或刪除/壓扁了某些提交。git pull --rebase 嘗試通過在 git rebase 和 git merge-base 中使用 “復刻點fork-point” 機制來從這種情況中恢復,以避免變基非本地提交。

實際上,這取決於 Git 的版本。直到 2.26.0 版,默認的非交互行爲以前與交互行爲稍有不同,這種方式通常並不重要。

本文地址:https://www.linuxprobe.com/git-hole.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章