根据我们的需要改变历史记录是Git的一项核心功能,这项功能为我们很多强大的工具,同时允许我们能够使用重构来实践软件设计原则。这些工具对新手甚至中级git用户来说可能有点吓人,但本文将为你揭开git-rebase的神秘面纱。

需要注意的是:通常不建议修改公共的、共享的或者稳定的分支。建议编辑个人分支与特性分支以及尚未推送的提交。git push -f在编辑提交后强制将更改推送到个人分支或功能分支到远程分支。

构建沙箱

为了不影响到你任何现有存储库,本文将使用沙箱仓库。从运行这些命令以开始:

1
2
3
git init /tmp/rebase-sandbox
cd /tmp/rebase-sandbox
git commit --allow-empty -m"Initial commit" # 1

如果遇到任何问题,运行rm -rf /tmp/rebase-sandbox删除现有的沙箱仓库并按照上述步骤重新构建沙箱仓库即可,后续每个章节的内容都是独立的,因此在构建沙箱仓库后,按照指定章节内容继续操作即可,不需要重新执行所有章节的操作。

修改上次提交

让我们从以下步骤开始:修复你最近一次的提交。

首先,添加一个文件到仓库中,并犯下一个错误(world单词拼错):

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

修复这个错误很简单,只需要重新编辑文件,并在提交时带上--amend参数:

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

指定-a参数自动重新追踪所有git已经提交的文件(同:git add's),--amend参数将修改压缩到最近的提交中。保存并退出编辑器(如果需要,可以修改对应的提交信息)。之后可以通过运行git show查看修复的提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
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!

修复旧的提交

上面的操作只适用于修改最近一次的提交,如果需要修改更久远的提交历史该怎么办呢?首先按照如下步骤设置仓库:

1
2
3
4
5
6
7
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,让我们通过正常提交修复:

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

现在文件看起来是对的了,但我们的历史提交显得有些啰嗦,现在让我们通过新提交来修复上一个提交,为了实现这个功能,需要引入新的工具:交互式rebase。我们将以这种方式编辑最后三个提交,因此我们将运行git rebase -i HEAD~3(-i用于交互式)。这会打开你的文本编辑器,如下所示:

1
2
3
4
5
6
7
8
9
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

这是rebase计划,通过编辑此文件,可以指示git如何编辑历史记录。我已将摘要修改为与rebase指南的这一部分相关的详细信息,但您可以随意浏览文本编辑器中的完整摘要。

当我们保存并关闭我们的编辑器时,git将从其历史记录中删除所有这些提交,然后一次执行每一行。默认情况下,它将选择每个提交,从堆中召唤它并将其添加到分支。如果我们根本不编辑这个文件,我们最终会回到我们开始的地方——按原样选择每个提交。我们现在将使用我最喜欢的功能之一:fixup。编辑第三行以将操作从“pick”更改为“fixup”,并将该行内容移动到我们想要“修复”提交的下一行:

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

提示:我们也可以将其缩写为“f”,以便下次加快速度。

保存并退出编辑器 - git将运行这些命令。我们可以检查日志以验证结果:

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

将几个提交压缩成一个

在你工作时,你可能会发现在你达到小型里程碑或修复先前提交中的错误时编写大量提交很有用。但是在到达里程碑、修复完成后,将这些提交“压缩”在一起可能会更有用,它可以在你的工作合并到主分支之前为你提供更清晰的历史记录。为此,我们将使用“squash”操作。让我们从写一堆提交开始:

1
2
3
4
5
6
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

这会创建很多提交,接下来我们将使用rebase将这些提交合并。请注意,我们首先检出一个分支以尝试此操作。因为通过检出新分支我们可以使用git rebase -i master编辑master分支的所有历史提交,而不再需要使用git rebase -i $commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

提示:你的本地主分支独立于远程主分支,git将远程分支存储为 origin/master。结合这个技巧,git rebase -i origin/master通常是一种非常方便的方法来修改尚未合并到上游的所有提交!

我们要将所有提交压缩到第一次提交中。要做到这一点,将每个“pick”操作更改为“squash”,第一行除外,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
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会根据上述修改重新计算提交信息,然后再次打开编辑器以供你修改最终的提交消息。你会看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 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

git默认将所有的提交信息组合,但是保留这样的消息不太可能是你想要的。但是,旧的提交消息能够为编写新提交信息提供参考。

提示:您在上一节中了解到的“fixup”命令也可以用于此目的 - 但它会丢弃压缩提交的消息。

让我们删除所有内容并用更好的提交消息替换它,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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日志 - 成功!

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

在继续下一步操作之前,让我们将我们的更改并入主分支并删除该分支。我们可以像使用git merge一样使用git rebase,但相比于git merge,git rebase避免了合并提交:

1
2
3
git checkout master
git rebase squash
git branch -D squash

除非我们实际上合并不相关的历史,否则我们通常会避免使用git merge。如果你有两个不同的分支,git merge对于记录它们何时被合并是很有用的。在正常工作过程中,rebase通常更合适。

将一个提交拆分为多个

有时会发生相反的问题 - 一次提交太大了。让我们看看将它拆分开。这一次,让我们写一些实际的代码。从简单的C程序开始:

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

接着提交:

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

之后,再尝试扩展程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

再次提交:

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

接下来我们将尝试拆分git提交:
第一步是启动交互式rebase。让我们通过执行git rebase -i HEAD~2rebase两次提交:

1
2
3
4
5
6
7
8
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会计算一秒,然后展示:

1
2
3
4
5
6
7
8
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^来执行“软重置” 。如果在此之后运行git status,您将看到它取消最新提交并将其更改添加到工作树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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以启动此过程,您将看到以下提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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为您提供了一个“hunk”(即单个更改)来考虑提交。但是这个太大了 - 让我们使用“s”命令将大块“拆分”成更小的提交。

1
2
3
4
5
6
7
8
9
10
11
12
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,?]?

提示:如果你对其他选项感到好奇,请按“?” 查看相关说明。
Tip: If you’re curious about the other options, press “?” to summarize them.

这个hunk作为一个单独的,自足的变化,看起来更好。让我们点击“y”来回答问题(以及“hunk”的阶段),然后点击“q”以“退出”交互式会话并继续提交。您的编辑器会弹出,要求您输入合适的提交消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
#

保存并关闭编辑器,然后我们将进行第二次提交。我们可以做另一个交互式提交,但由于我们只想在此提交中包含其余的更改,我们将执行此操作:

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

最后一个命令告诉git我们已完成编辑此提交,并继续执行下一个rebase命令。运行git log看看你的劳动成果:

1
2
3
4
$ 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

对提交重新排序

这部分比较简单。让我们从以下操作开始:

1
2
3
4
5
6
7
8
9
10
11
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日志现在看起来是这样的:

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

显然,顺序是不对的。让我们对过去3次提交进行交互式变更以解决此问题。运行git rebase -i HEAD~3

1
2
3
4
5
6
7
8
9
10
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.

修复现在很简单:只需按照希望提交的顺序重新排列这些行:

1
2
3
pick a4cebf7 Add greeting.txt
pick f03baa5 Add inquiry.txt
pick 90bb015 Add farewell.txt

保存并关闭您的编辑器,git将为您完成剩下的工作。

git pull –rebase

如果您在已经上游更新的分支上编写了一些提交,通常git pull会创建一个合并提交。在这方面,git pull默认情况下的行为相当于:

1
2
git fetch origin
git merge origin/master

还有另一种选择,且它通常更有用,能够带来更清晰的历史:git pull –rebase。与合并方法不同,这相当于以下内容:

1
2
git fetch origin
git rebase origin/master

merge方法更简单,更容易理解,但如果您了解如何使用git rebase,则rebase方法几乎总是您想要做的。如果您愿意,可以将其设置为默认行为,如下所示:

1
git config --global pull.rebase true

当你这样做时,从技术上讲,你正在应用我们在下一节讨论的程序……所以让我们解释一下故意这样做的意义。

使用git rebase来… rebase

假设您有以下分支:

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

可以看出:feature-2不依赖于feature-1中的任何更改,即在提交E上,因此您可以将其基于master。因此修复:

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

非交互式rebase对所有涉及的提交(“pick”)执行默认操作,它只是重放feature-2中不在master基础上的feature-1中的提交。您的历史现在看起来像这样:

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

解决冲突

有关解决合并冲突的详细信息超出了本文的范围 - 请留意未来的其他指南。假设您熟悉一般解决冲突,以下是适用于变基的细节。

有时你在做一个rebase时会遇到合并冲突,你可以像任何其他合并冲突一样处理它。Git将在受影响的文件中设置冲突标记,git status向您显示需要解决的内容,并且可以使用git addgit rm标记已解析的文件。但是,在git rebase的上下文中,您应该注意两个选项。

第一个是如何完成冲突解决,使用git rebase --continue。但是,还有另一种选择:git rebase --skip。这将跳过您正在处理的提交,并且它不会包含在rebase中。这在执行非交互式rebase时最常见,当git没有意识到它从“其他”分支中提取的提交是它在“我的”分支上与之冲突的提交的更新版本。

救命!我打破了它!

毫无疑问 - 有时候变基很难。如果你犯了一个错误并且这样做了你需要的遗失,那么git reflog可以挽救你。运行此命令将显示更改ref或引用的每个操作- 即分支和标记。每行显示旧参考的指向,你可以使用git cherry-pickgit checkoutgit show,或使用任何其他操作。

原文链接:git rebase in depth