标签 变基 下的文章

学习如何使用 Git 来压扁、变基和精选。

当我与别人谈到 Git 时,几乎每个人都对 git rebase 命令 有强烈的印象,这个命令让许多人遇到了问题,而不得不更改目录、删除仓库、然后再重新克隆一个仓库。我认为这是因为他们误解了分支是如何工作,遇到了一个非常糟糕的默认界面,还有一些合并冲突把事情搞得一团糟。

怎么找不到 git squash 命令?

如果你曾在本地的仓库提交过很多次,并希望能把这些提交都合并为一个提交,接下来,我们就来介绍能用什么 Git 命令达到这个目的。Git 称这个概念为 “ 压扁提交 squash commits ”。我在编写文档时发现了这个概念:我花了十几个提交才修改好我的 Markdown 文档,但是仓库的维护者不想看到我的所有尝试,以免扰乱了该项目的历史,所以我被告知“需要压扁你的提交”。

压扁提交听起来是一个很有用的方法。但是只有一个问题:我不知道该怎么做。作为 Git 的新手,我做了任何人会做的事情:我去查阅 git-squash 的手册,但我立即遇到了阻碍:

$ man git-squash
> No manual entry for git-squash

我发现没有一个名为 squash 的 Git 命令,而是被要求 运行一个完全独立的命令:git rebase 命令,该命令能将我的所有提交最终合并为一个提交。

我知道我碰到一个常见的情形:已经使用工具一段时间的人使用了行话或引用了一个概念,这个概念对他们来说是非常清楚的,但对新手来说就不能明白了。从概念上讲,这个情况看起来是这样的:

Image of 6 bowls of different colored spices, and an arrow pointing to the second image of all the spices blended into one bowl.

我这样说是为了鼓励你,你绝对不是第一个或最后一个 被 Git 或谈论 Git 的人 弄糊涂的人。你可以要求对方说明白他的意见,并帮助你应该使用的正确命令。仓库的维护者实际上的意思是,“使用 git rebase 命令**,将很多提交压扁成一个提交”。

现在就来学习 git rebase 命令吧

git rebase 命令会将一个提交链从其第一个父级中删除,并将其放置在另一个提交链的末尾,将两个提交链组合成一个长链,而不是两个并行链。我意识到这是一个很复杂的定义。

回想一下 Git 的提交是如何链接在一起的,你可以看到,除了初始的 main(或 master)分支外,任何分支都有一个 父提交 parent commit 作为该链的 “ 基础 base ”。“ 变基 rebase ” 能使另一个链中的最后一个提交成为指定分支的新 “ 基础提交 base commit ”。

在 Git 中整合来自不同分支的修改主要有两种方法: 合并 merge 以及 变基 rebase ,你可能更熟悉 git merge 命令。接下来,就来看看 [git-scm.com] 是如何解释 git mergegit rebase 的差异:

Image of Git merge versus git rebase shown as numbered bubbles.

在合并示例中,它会把两个分支的最新快照(C3C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(C5)。experiment 的分支指针仍然存在,仍然指向 C4

在变基示例中,它提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次,使 C3 成为 C4 的新父级,并产生了一个名为 C4' 的新提交。

(LCTT 译注:具体的命令如下:

$ git checkout experiment
$ git rebase main
First, rewinding head to replay your work on top of it...
Applying: added staged command

它的原理是首先找到这两个分支 —— 即当前分支 experiment、变基操作的目标基底分支 main —— 的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3,最后以此将之前另存为临时文件的修改依序应用。)

值得注意的是,分支指针 main 没有移动。要让 Git 将指针移动到链的末尾(由experiment 指向),你还需要执行合并。

(LCTT 译注:具体的命令如下:

$ git checkout main
$ git merge experiment

master 分支的快进合并

此时,C4' 指向的快照就和上面使用 merge 命令的例子中 C5 指向的快照一模一样了。)

git rebase 并不能替代 git mergegit rebase 是一种用于制作更清晰的历史记录,以与 git merge 结合使用的工具。

(LCTT 译注:使用 git rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。)

交互式变基能给你一个更友好的界面!

从命令行执行 git rebase 命令,最可怕的地方在于它糟糕的默认界面。运行命令 git rebase <target-refr> 要么有效,要么会变得一团糟,因为它没有太多的反馈或方法来确保它做你想做的事情。幸运的是,git rebase 命令和许多其他 Git 命令一样,具有 交互模式 interactive mode ,你可以使用参数 -i 或者 -interactive 来使用交互模式。

Image of the Git lens interactive Rebase tool in VS Code.

在使用交互式模式时,git rebase 会从一个糟糕的黑框界面转换为一个选项菜单,允许你选择对正在变基的提交链所做的事。对于每个提交,你可以选择

  • 选用 pick :按原样包含
  • 重写 reword :重写提交消息
  • 编写 edit :在变基完成之前对提交中的文件进行进一步更改
  • 压扁 squash :将多个提交压缩成一个提交,保留所有提交消息
  • 修理 fixup :将多个提交压缩成一个提交,但只保留最后一个提交消息
  • 丢弃 drop :丢弃此提交

就我个人而言,我更喜欢 VS Code 的开源 GitLens 扩展 使用下拉选择列表布局选项的方式,但 Git 允许你使用任何编辑器选择这些选项。对于 Emacs 或 Vim 等纯文本工具,你需要键入选择,而不是从菜单中选择,但最终结果仍然是相同的。

何时做变基

知道 何时 做变基与知道 如何 做变基同样重要。事实上,如果你不在乎你的仓库历史提交消息有点混乱的话,那么你可以永远都不使用 git rebase 命令。但是,如果你想要更干净的历史提交消息,并且想要更少扰乱你的图形视图的提交,那么当你使用 git rebase 命令时,有一个重要的经验法则需要时刻记住:

“不要变基你存储库以外的的提交,那些提交可能是别人工作的基础。”

如果你遵循该准则,不会发生什么大问题的。

简而言之,如果你让一个本地分支来完成你的工作,变基是没有问题的。但一旦该分支被 推送 push 了,就不要再变基该分支了。当然,你想要怎么做完全取决于你自己。

希望你会认为上述内容有助于你理解 git rebase 命令的工作原理,并能让你更有信心地使用它。与任何 Git 命令一样,练习是学习和理解怎么做的唯一方法。我鼓励你勇敢地尝试 交互式变基 interactive rebase git rebase -i <branch name>

接下来学习 Git cherry-pick 命令吧

大多数开发人员将修改提交到某一分支上,但是之后发现他们一直提交到了错误的分支上。理想情况下,他们可以拿走那个提交,然后把它移到正确的分支,这正是 git cherry-pick 命令的作用。

git cherry-pick 命令利用了变基单个提交的方法。这一用法非常常见,以至于有了它自己的命令。

Image of a woman picking a cherry from one tree and putting on another tree.

要使用 git cherry-pick,你只需告诉 Git 你要移动到“那个分支”的提交 ID(由 HEAD 指向):

$ git cherry-pick <target-ref>

如果出现问题,你可以根据 Git 提供的错误消息,来进行恢复:

$ git cherry-pick -i 2bc01cd
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
error: could not apply 2bc01cd… added EOF lines
hint: After resolving the conflicts, mark them with
hint: "git add/rm ", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".
$ git cherry-pick --abort

让 Git 更强大

git rebase 命令是 Git 实用程序强大的地方之一。你最好在测试仓库中先练习一下怎么使用,一旦你熟悉了它的概念和工作流程,你就可以给仓库一个清晰历史消息记录了。


via: https://opensource.com/article/22/11/advanced-git-commands

作者:Dwayne McDaniel 选题:lkxed 译者:chai001125 校对:wxy

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

Git 核心的附加价值之一就是编辑历史记录的能力。与将历史记录视为神圣的记录的版本控制系统不同,在 Git 中,我们可以修改历史记录以适应我们的需要。这为我们提供了很多强大的工具,让我们可以像使用重构来维护良好的软件设计实践一样,编织良好的提交历史。这些工具对于新手甚至是有经验的 Git 用户来说可能会有些令人生畏,但本指南将帮助我们揭开强大的 git-rebase 的神秘面纱。

值得注意的是:一般建议不要修改公共分支、共享分支或稳定分支的历史记录。编辑特性分支和个人分支的历史记录是可以的,编辑还没有推送的提交也是可以的。在编辑完提交后,可以使用 git push -f 来强制推送你的修改到个人分支或特性分支。

尽管有这么可怕的警告,但值得一提的是,本指南中提到的一切都是非破坏性操作。实际上,在 Git 中永久丢失数据是相当困难的。本指南结尾介绍了在犯错误时进行纠正的方法。

设置沙盒

我们不想破坏你的任何实际的版本库,所以在整个指南中,我们将使用一个沙盒版本库。运行这些命令来开始工作。 1

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 程序 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,?]?
提示:如果你对其他选项感到好奇,请按 ? 汇总显示。

这个大块看起来更好:单一、独立的更改。让我们按 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 或任何其他操作。


  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 版,默认的非交互行为以前与交互行为稍有不同,这种方式通常并不重要。

via: https://git-rebase.io/

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

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

让大家觉得你一次就能写出完美的代码,并让你的补丁更容易审核和合并。

软件开发是混乱的。有很多错误的转折、有需要修复的错别字、有需要修正的错误、有需要稍后纠正的临时和粗陋的代码,还有在以后的开发过程中发现一次又一次的问题。有了版本控制,在创建“完美”的最终产品(即准备提交给上游的补丁)的过程中,你会有一个记录着每一个错误转折和修正的原始记录。就像电影中的花絮一样,它们会让人有点尴尬,有时也会让人觉得好笑。

如果你使用版本控制来定期保存你的工作线索,然后当你准备提交审核的东西时,又可以隐藏所有这些私人草稿工作,并只提交一份单一的、完美的补丁,那不是很好吗?git rebase -i,是重写历史记录的完美方法,可以让大家觉得你一次就写出了完美的代码!

git rebase 的作用是什么?

如果你不熟悉 Git 的复杂性,这里简单介绍一下。在幕后,Git 将项目的不同版本与唯一标识符关联起来,这个标识符由父节点的唯一标识符的哈希以及新版本与其父节点的差异组成。这样就形成了一棵修订树,每个签出项目的人都会得到自己的副本。不同的人可以把项目往不同的方向发展,每个方向都可能从不同的分支点开始。

 title=

左边是 origin 版本库中的主分支,右边是你个人副本中的私有分支。

有两种方法可以将你的工作与原始版本库中的主分支整合起来:一种是使用合并:git merge,另一种是使用变基:git rebase。它们的工作方式非常不同。

当你使用 git merge 时,会在主分支(master)上创建一个新的提交,其中包括所有来自原始位置(origin)的修改和所有本地的修改。如果有任何冲突(例如,如果别人修改了你也在修改的文件),则将这些冲突标记出来,并且你有机会在将这个“合并提交”提交到本地版本库之前解决这些冲突。当你将更改推送回父版本库时,所有的本地工作都会以分支的形式出现在 Git 版本库的其他用户面前。

但是 git rebase 的工作方式不同。它会回滚你的提交,并从主分支(master)的顶端再次重放这些提交。这导致了两个主要的变化。首先,由于你的提交现在从一个不同的父节点分支出来,它们的哈希值会被重新计算,并且任何克隆了你的版本库的人都可能得到该版本库的一个残破副本。第二,你没有“合并提交”,所以在将更改重放到主分支上时会识别出任何合并冲突,因此,你需要在进行 变基 rebase 之前先修复它们。现在,当你现在推送你的修改时,你的工作不会出现在分支上,并且看起来像是你是在主分支的最新的提交上写入了所有的修改。

 title=

合并提交(左)保留了历史,而变基(右)重写历史。

然而,这两种方式都有一个缺点:在你准备好分享代码之前,每个人都可以看到你在本地处理问题时的所有涂鸦和编辑。这就是 git rebase--interactive(或简写 -i)标志发挥作用的地方。

git rebase -i 登场

git rebase 的最大优点是它可以重写历史。但是,为什么仅止于假装你从后面的点分支出来呢?有一种更进一步方法可以重写你是如何准备就绪这些代码的:git rebase -i,即交互式的 git rebase

这个功能就是 Git 中的 “魔术时光机” 功能。这个标志允许你在做变基时对修订历史记录进行复杂的修改。你可以隐藏你的错误! 将许多小的修改合并到一个崭新的功能补丁中! 重新排列修改历史记录中的显示顺序!

 title=

当你运行 git rebase -i 时,你会进入一个编辑器会话,其中列出了所有正在被变基的提交,以及可以对其执行的操作的多个选项。默认的选择是选择(Pick)。

  • Pick:会在你的历史记录中保留该提交。
  • Reword:允许你修改提交信息,可能是修复一个错别字或添加其它注释。
  • Edit:允许你在重放分支的过程中对提交进行修改。
  • Squash:可以将多个提交合并为一个。
  • 你可以通过在文件中移动来重新排序提交。

当你完成后,只需保存最终结果,变基操作就会执行。在你选择修改提交的每个阶段(无论是用 rewordeditsquash 还是发生冲突时),变基都会停止,并允许你在继续提交之前进行适当的修改。

上面这个例子的结果是 “One-liner bug fix” 和 “Integate new header everywhere” 被合并到一个提交中,而 “New header for docs website” 和 “D'oh - typo. Fixed” 合并到另一个提交中。就像变魔术一样,其他提交的工作还在你的分支中,但相关的提交已经从你的历史记录中消失了!

这使得使用 git send-email 或者用你新整理好的补丁集在父版本库中创建一个拉取请求,然后来提交一个干净的补丁给上游项目变得很容易。这有很多好处,包括让你的代码更容易审核,更容易接受,也更容易合并。


via: https://opensource.com/article/20/4/git-rebase-i

作者:Dave Neary 选题:lujun9972 译者:wxy 校对:wxy

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

用简洁而优雅的 Git 命令撤销仓库中的改变。

使用 Git 工作时其中一个鲜为人知(和没有意识到)的方面就是,如何轻松地返回到你以前的位置 —— 也就是说,在仓库中如何很容易地去撤销那怕是重大的变更。在本文中,我们将带你了解如何去重置、恢复和完全回到以前的状态,做到这些只需要几个简单而优雅的 Git 命令。

重置

我们从 Git 的 reset 命令开始。确实,你应该能够认为它就是一个 “回滚” —— 它将你本地环境返回到之前的提交。这里的 “本地环境” 一词,我们指的是你的本地仓库、暂存区以及工作目录。

先看一下图 1。在这里我们有一个在 Git 中表示一系列提交的示意图。在 Git 中一个分支简单来说就是一个命名的、指向一个特定的提交的可移动指针。在这里,我们的 master 分支是指向链中最新提交的一个指针。

 title=

图 1:有仓库、暂存区、和工作目录的本地环境

如果看一下我们的 master 分支是什么,可以看一下到目前为止我们产生的提交链。

$ git log --oneline
b764644 File with three lines
7c709f0 File with two lines
9ef9173 File with one line

如果我们想回滚到前一个提交会发生什么呢?很简单 —— 我们只需要移动分支指针即可。Git 提供了为我们做这个动作的 reset 命令。例如,如果我们重置 master 为当前提交回退两个提交的位置,我们可以使用如下之一的方法:

$ git reset 9ef9173

(使用一个绝对的提交 SHA1 值 9ef9173

或:

$ git reset current~2

(在 “current” 标签之前,使用一个相对值 -2)

图 2 展示了操作的结果。在这之后,如果我们在当前分支(master)上运行一个 git log 命令,我们将看到只有一个提交。

$ git log --oneline

9ef9173 File with one line

 title=

图 2:在 reset 之后

git reset 命令也包含使用一些选项,可以让你最终满意的提交内容去更新本地环境的其它部分。这些选项包括:hard 在仓库中去重置指向的提交,用提交的内容去填充工作目录,并重置暂存区;soft 仅重置仓库中的指针;而 mixed(默认值)将重置指针和暂存区。

这些选项在特定情况下非常有用,比如,git reset --hard <commit sha1 | reference> 这个命令将覆盖本地任何未提交的更改。实际上,它重置了(清除掉)暂存区,并用你重置的提交内容去覆盖了工作区中的内容。在你使用 hard 选项之前,一定要确保这是你真正地想要做的操作,因为这个命令会覆盖掉任何未提交的更改。

恢复

git revert 命令的实际结果类似于 reset,但它的方法不同。reset 命令(默认)是在链中向后移动分支的指针去“撤销”更改,revert 命令是在链中添加一个新的提交去“取消”更改。再次查看图 1 可以非常轻松地看到这种影响。如果我们在链中的每个提交中向文件添加一行,一种方法是使用 reset 使那个提交返回到仅有两行的那个版本,如:git reset HEAD~1

另一个方法是添加一个新的提交去删除第三行,以使最终结束变成两行的版本 —— 实际效果也是取消了那个更改。使用一个 git revert 命令可以实现上述目的,比如:

$ git revert HEAD

因为它添加了一个新的提交,Git 将提示如下的提交信息:

Revert "File with three lines"

This reverts commit b764644bad524b804577684bf74e7bca3117f554.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
#       modified:   file1.txt
#

图 3(在下面)展示了 revert 操作完成后的结果。

如果我们现在运行一个 git log 命令,我们将看到前面的提交之前的一个新提交。

$ git log --oneline
11b7712 Revert "File with three lines"
b764644 File with three lines
7c709f0 File with two lines
9ef9173 File with one line

这里是工作目录中这个文件当前的内容:

$ cat <filename>
Line 1
Line 2

img

图 3 revert 操作之后

恢复或重置如何选择?

为什么要优先选择 revert 而不是 reset 操作?如果你已经将你的提交链推送到远程仓库(其它人可以已经拉取了你的代码并开始工作),一个 revert 操作是让他们去获得更改的非常友好的方式。这是因为 Git 工作流可以非常好地在分支的末端添加提交,但是当有人 reset 分支指针之后,一组提交将再也看不见了,这可能会是一个挑战。

当我们以这种方式使用 Git 工作时,我们的基本规则之一是:在你的本地仓库中使用这种方式去更改还没有推送的代码是可以的。如果提交已经推送到了远程仓库,并且可能其它人已经使用它来工作了,那么应该避免这些重写提交历史的更改。

总之,如果你想回滚、撤销或者重写其它人已经在使用的一个提交链的历史,当你的同事试图将他们的更改合并到他们拉取的原始链上时,他们可能需要做更多的工作。如果你必须对已经推送并被其他人正在使用的代码做更改,在你做更改之前必须要与他们沟通,让他们先合并他们的更改。然后在这个侵入操作没有需要合并的内容之后,他们再拉取最新的副本。

你可能注意到了,在我们做了 reset 操作之后,原始的提交链仍然在那个位置。我们移动了指针,然后 reset 代码回到前一个提交,但它并没有删除任何提交。换句话说就是,只要我们知道我们所指向的原始提交,我们能够通过简单的返回到分支的原始链的头部来“恢复”指针到前面的位置:

git reset <sha1 of commit>

当提交被替换之后,我们在 Git 中做的大量其它操作也会发生类似的事情。新提交被创建,有关的指针被移动到一个新的链,但是老的提交链仍然存在。

变基

现在我们来看一个分支变基。假设我们有两个分支:master 和 feature,提交链如下图 4 所示。master 的提交链是 C4->C2->C1->C0 和 feature 的提交链是 C5->C3->C2->C1->C0

 title=

图 4:master 和 feature 分支的提交链

如果我们在分支中看它的提交记录,它们看起来应该像下面的这样。(为了易于理解,C 表示提交信息)

$ git log --oneline master
6a92e7a C4
259bf36 C2
f33ae68 C1
5043e79 C0

$ git log --oneline feature
79768b8 C5
000f9ae C3
259bf36 C2
f33ae68 C1
5043e79 C0

我告诉人们在 Git 中,可以将 rebase 认为是 “将历史合并”。从本质上来说,Git 将一个分支中的每个不同提交尝试“重放”到另一个分支中。

因此,我们使用基本的 Git 命令,可以变基一个 feature 分支进入到 master 中,并将它拼入到 C4 中(比如,将它插入到 feature 的链中)。操作命令如下:

$ git checkout feature
$ git rebase master

First, rewinding head to replay your work on top of it...
Applying: C3
Applying: C5

完成以后,我们的提交链将变成如下图 5 的样子。

 title=

图 5:rebase 命令完成后的提交链

接着,我们看一下提交历史,它应该变成如下的样子。

$ git log --oneline master
6a92e7a C4
259bf36 C2
f33ae68 C1
5043e79 C0

$ git log --oneline feature
c4533a5 C5
64f2047 C3
6a92e7a C4
259bf36 C2
f33ae68 C1
5043e79 C0

注意那个 C3'C5'— 在 master 分支上已处于提交链的“顶部”,由于产生了更改而创建了新提交。但是也要注意的是,rebase 后“原始的” C3C5 仍然在那里 — 只是再没有一个分支指向它们而已。

如果我们做了这个变基,然后确定这不是我们想要的结果,希望去撤销它,我们可以做下面示例所做的操作:

$ git reset 79768b8

由于这个简单的变更,现在我们的分支将重新指向到做 rebase 操作之前一模一样的位置 —— 完全等效于撤销操作(图 6)。

 title=

图 6:撤销 rebase 操作之后

如果你想不起来之前一个操作指向的一个分支上提交了什么内容怎么办?幸运的是,Git 命令依然可以帮助你。用这种方式可以修改大多数操作的指针,Git 会记住你的原始提交。事实上,它是在 .git 仓库目录下,将它保存为一个特定的名为 ORIG_HEAD 的文件中。在它被修改之前,那个路径是一个包含了大多数最新引用的文件。如果我们 cat 这个文件,我们可以看到它的内容。

$ cat .git/ORIG_HEAD
79768b891f47ce06f13456a7e222536ee47ad2fe

我们可以使用 reset 命令,正如前面所述,它返回指向到原始的链。然后它的历史将是如下的这样:

$ git log --oneline feature
79768b8 C5
000f9ae C3
259bf36 C2
f33ae68 C1
5043e79 C0

在 reflog 中是获取这些信息的另外一个地方。reflog 是你本地仓库中相关切换或更改的详细描述清单。你可以使用 git reflog 命令去查看它的内容:

$ git reflog
79768b8 HEAD@{0}: reset: moving to 79768b
c4533a5 HEAD@{1}: rebase finished: returning to refs/heads/feature
c4533a5 HEAD@{2}: rebase: C5
64f2047 HEAD@{3}: rebase: C3
6a92e7a HEAD@{4}: rebase: checkout master
79768b8 HEAD@{5}: checkout: moving from feature to feature
79768b8 HEAD@{6}: commit: C5
000f9ae HEAD@{7}: checkout: moving from master to feature
6a92e7a HEAD@{8}: commit: C4
259bf36 HEAD@{9}: checkout: moving from feature to master
000f9ae HEAD@{10}: commit: C3
259bf36 HEAD@{11}: checkout: moving from master to feature
259bf36 HEAD@{12}: commit: C2
f33ae68 HEAD@{13}: commit: C1
5043e79 HEAD@{14}: commit (initial): C0

你可以使用日志中列出的、你看到的相关命名格式,去重置任何一个东西:

$ git reset HEAD@{1}

一旦你理解了当“修改”链的操作发生后,Git 是如何跟踪原始提交链的基本原理,那么在 Git 中做一些更改将不再是那么可怕的事。这就是强大的 Git 的核心能力之一:能够很快速、很容易地尝试任何事情,并且如果不成功就撤销它们。


via: https://opensource.com/article/18/6/git-reset-revert-rebase-commands

作者:Brent Laster 选题:lujun9972 译者:qhwdw 校对:wxy

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