Git 核心的附加價值之一就是編輯歷史記錄的能力。與將歷史記錄視爲神聖的記錄的版本控制系統不一樣,在 Git 中,咱們能夠修改歷史記錄以適應咱們的須要。這爲咱們提供了不少強大的工具,讓咱們能夠像使用重構來維護良好的軟件設計實踐同樣,編織良好的提交歷史。這些工具對於新手甚至是有經驗的 Git 用戶來講可能會有些使人生畏,但本指南將幫助咱們揭開強大的 git-rebase 的神祕面紗。linux
值得注意的是:通常建議不要修改公共分支、共享分支或穩定分支的歷史記錄。編輯特性分支和我的分支的歷史記錄是能夠的,編輯尚未推送的提交也是能夠的。在編輯完提交後,可使用
git push -f
來強制推送你的修改到我的分支或特性分支。git
儘管有這麼可怕的警告,但值得一提的是,本指南中提到的一切都是非破壞性操做。實際上,在 Git 中永久丟失數據是至關困難的。本指南結尾介紹了在犯錯誤時進行糾正的方法。github
咱們不想破壞你的任何實際的版本庫,因此在整個指南中,咱們將使用一個沙盒版本庫。運行這些命令來開始工做。[1]shell
git init /tmp/rebase-sandbox
cd /tmp/rebase-sandbox
git commit --allow-empty -m"Initial commit"
複製代碼
若是你遇到麻煩,只需運行 rm -rf /tmp/rebase-sandbox
,並從新運行這些步驟便可從新開始。本指南的每一步均可以在新的沙箱上運行,因此沒有必要重作每一個任務。ruby
讓咱們從簡單的事情開始:修復你最近的提交。讓咱們向沙盒中添加一個文件,並犯個錯誤。bash
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"
。讓咱們正常地寫個提交來解決這個問題:fetch
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 程序 [2] 開始(你仍然能夠將此代碼段複製並粘貼到你的 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 <stdio.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 <stdio.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,?]?
複製代碼
提示:若是你對其餘選項感到好奇,請按
?
彙總顯示。
這個大塊看起來更好:單1、獨立的更改。讓咱們按 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 --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 將爲你完成其他工做。請注意,在實踐中這樣作可能會致使衝突,參看下面章節以獲取解決衝突的幫助。
若是你一直在由上游更新的分支 <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 變基功能是它以之命名的功能:變基分支。假設你有如下分支:
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
)[5] ,它只是簡單地將不在 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
或任何其餘操做。
via: git-rebase.io/
做者:git-rebase 選題:lujun9972 譯者:wxy 校對:wxy
咱們添加了一個空的初始提交以簡化本教程的其他部分,由於要對版本庫的初始提交進行變基須要特殊的命令(即git rebase --root
)。 ↩︎
若是要編譯此程序,請運行 cc -o main main.c
,而後運行 ./main
查看結果。 ↩︎
實際上,這是「混合重置」。「軟重置」(使用 git reset --soft
完成)將暫存更改,所以你無需再次 git add
添加它們,而且能夠一次性提交全部更改。這不是咱們想要的。咱們但願選擇性地暫存部分更改,以拆分提交。 ↩︎
實際上,這取決於上游分支自己是否已變基或刪除/壓扁了某些提交。git pull --rebase
嘗試經過在 git rebase
和 git merge-base
中使用 「復刻點」 機制來從這種狀況中恢復,以免變基非本地提交。 ↩︎
實際上,這取決於 Git 的版本。直到 2.26.0 版,默認的非交互行爲之前與交互行爲稍有不一樣,這種方式一般並不重要。 ↩︎