学习用 Git 变基来改变历史!

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 &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,?]?
复制代码

提示:若是你对其余选项感到好奇,请按 ? 汇总显示。

这个大块看起来更好:单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 将为你完成其他工做。请注意,在实践中这样作可能会致使冲突,参看下面章节以获取解决冲突的帮助。

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[5] ,它只是简单地将不在 feature-1 中的 feature-2 中提交重放到 master 上。你的历史记录如今看起来像这样:

A--B--C--D--> master
   |     \--G--> feature-2
   \--E--F--> feature-1
复制代码

解决冲突

解决合并冲突的详细信息不在本指南的范围内,未来请你注意另外一篇指南。假设你熟悉一般的解决冲突的方法,那么这里是专门适用于变基的部分。

有时,在进行变基时会遇到合并冲突,你能够像处理其余任何合并冲突同样处理该冲突。Git 将在受影响的文件中设置冲突标记,git status 将显示你须要解决的问题,而且你可使用 git addgit rm 将文件标记为已解决。可是,在 git rebase 的上下文中,你应该注意两个选项。

首先是如何完成冲突解决。解决因为 git merge 引发的冲突时,与其使用 git commit 那样的命令,更适当的变基命令是 git rebase --continue。可是,还有一个可用的选项:git rebase --skip。 这将跳过你正在处理的提交,它不会包含在变基中。这在执行非交互性变基时最多见,这时 Git 不会意识到它从“其余”分支中提取的提交是与“咱们”分支上冲突的提交的更新版本。

帮帮我! 我把它弄坏了!

毫无疑问,变基有时会很难。若是你犯了一个错误,并所以而丢失了所需的提交,那么可使用 git reflog 来节省下一天的时间。运行此命令将向你显示更改一个引用(即分支和标记)的每一个操做。每行显示你的旧引用所指向的内容,你可对你认为丢失的 Git 提交执行 git cherry-pickgit checkoutgit show 或任何其余操做。


via: git-rebase.io/

做者:git-rebase 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出


  1. 咱们添加了一个空的初始提交以简化本教程的其他部分,由于要对版本库的初始提交进行变基须要特殊的命令(即git rebase --root)。 ↩︎

  2. 若是要编译此程序,请运行 cc -o main main.c,而后运行 ./main 查看结果。 ↩︎

  3. 实际上,这是“混合重置”。“软重置”(使用 git reset --soft 完成)将暂存更改,所以你无需再次 git add 添加它们,而且能够一次性提交全部更改。这不是咱们想要的。咱们但愿选择性地暂存部分更改,以拆分提交。 ↩︎

  4. 实际上,这取决于上游分支自己是否已变基或删除/压扁了某些提交。git pull --rebase 尝试经过在 git rebasegit merge-base 中使用 “复刻点fork-point” 机制来从这种状况中恢复,以免变基非本地提交。 ↩︎

  5. 实际上,这取决于 Git 的版本。直到 2.26.0 版,默认的非交互行为之前与交互行为稍有不一样,这种方式一般并不重要。 ↩︎

相关文章
相关标签/搜索