Git-秘籍-三-

134 阅读49分钟

Git 秘籍(三)

原文:Git Recipes

协议:CC BY-NC-SA 4.0

六、合并分支

创建和切换分支的命令允许您将项目分成独立的开发线。例如,您可以创建分支来启动应用中的新功能或实现修复。迟早你会完成一个特性或修复,然后很可能,你会想把你的努力融入到开发的主线中。连接不同分支的过程可以使用两种不同的技术来完成。第一个是通过 gitmerge命令实现的。这是本章的主题。第二个是用 git merge** 命令实现的。这是本章的主题。第二个是用 ** git rebase 命令完成的。我们将推迟这个问题,因为它是下一章的主题。

也许你已经注意到术语合并并不是全新的。我们已经在配方 5-7 中使用它来表示将工作目录中的变更与我们切换到的分支连接起来的过程。在那里,合并是在结帐时完成的。结果被留在工作目录中等待提交。在本章中,我们考虑将不同的分支合并在一起。合并分支的结果将被存储和提交,也就是说,在一个分支中。我们将在不包含未提交或未跟踪文件的干净配方中执行合并。在配方之后,存储库将再次处于干净的状态。

在这里,你会充分体会到simple-commitclone-with-branches的别名。他们会带你到更高的抽象层次。我们将使用$ git simple-commit创建一系列提交,而不是使用$ git add$ git commit以及$ echo foo > bar。因此,你将能够专注于合并。别名的工作方式不会发生冲突。当然,这是一个简化的设置,而不是真实的场景。但是我更喜欢把合并和解决冲突分成两个独立的步骤。冲突将在第九章中深入讨论。

这一章反映了我通常教授分支和合并的方式。我坚信学习 git 分支模型的一个关键点是掌握这个操作转换修订图的方式。一旦您理解了如何创建一个具有预定义结构的图,合并就变得简单明了。

本章涵盖了三种合并情况:

  • 快进
  • 两个分叉分支的合并
  • 多个分叉分支的合并

为了实现这些合并,我们需要创建:

  • 有两个分支的存储库,其中一个必须合并到另一个中
  • 具有两个分叉分支的存储库
  • 和具有多个分叉分支的储存库

所有这些存储库都将被创建在单独的配方中,如果有必要的话,这样可以更容易地多次重复每个案例。每当您想要再次执行合并时,只需克隆一个初始存储库并执行合并分支的命令。以这种方式工作,你可以,例如,测试和分析$ git merge命令的不同选项如何影响它的行为。

此外,本章还解释了:

  • 如何撤消合并操作
  • 如何使用--no-ff开关强制将快进实现为典型的合并
  • 如何使用--ff-only开关确保合并是快进
  • 如何使用第 n 个父引用

6-1.在分支机构中实现新功能

问题

您的存储库是干净的,只包含一个名为master的分支。该分支由三个修订组成。该配方的起点如图图 6-1 所示。您希望实现应用的一个新特性。因为您不确定这个解决方案是否是好的,所以您决定使用一个名为feature的新分支。你想要达到的结果如图图 6-2 所示。

9781430261032_Fig06-01.jpg

图 6-1 。制作方法 6-1 的起点

9781430261032_Fig06-02.jpg

图 6-2 。完成配方 6-1

解决办法

启动命令行并创建新的存储库:

$ cd git-recipes

$ git init 06-01

$ cd 06-01

接下来,在master分支中创建三个提交:

$ git simple-commit m1 m2 m3

你的存储库现在看起来像图 6-1 。创建一个名为feature的新分支,包含三个新提交:

$ git checkout -b feature
$ git simple-commit f1 f2 f3

$ git checkout master命令完成检查master分支的配方。现在,存储库看起来像图 6-2 。

它是如何工作的

该配方使用已知的命令。它提供了在应用中实现新功能的最佳方法。每当你开始研究一个新的课题时,就在一个专门的分支中进行。请注意,图 6-2 中显示的库也可以绘制成图 6-3 中的库。修改不必形成一条直线。该配方对分支中的提交数量不敏感。例如,master分支可以包括 100 次提交,而feature分支只能包括一次提交。这个存储库唯一重要的方面是master分支完全合并到了feature分支中。

9781430261032_Fig06-03.jpg

图 6-3 。图 6-2 的替代图形表示

6-2.快进分支

问题

你已经完成了配方 6-1 的工作,并决定将你在feature分支的工作合并到master分支中。你想要创建如图图 6-4 所示的存储库。

9781430261032_Fig06-04.jpg

图 6-4 。将特征分支合并到主分支的结果

解决办法

用分支克隆配方 6-1 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-01 06-02

$ cd 06-02

现在你在master分支。用$ git merge feature命令将feature分支合并到master分支。该命令将打印此操作以快进方式进行的信息。当你完成时,master分支包含了在master分支中创建的所有文件以及来自feature分支的三个文件。$ ls命令将打印六个文件名:f1.txtf2.txtf3.txtm1.txtm2.txtm3.txt

它是如何工作的

这个秘籍展示了合并分支的最简单的例子。有两个分支masterfeature,当前分支为master。一个很重要的事实是,master分支中的所有修改都包含在feature分支中。我们说master分支合并到了feature分支。该关系在配方 5-9 中定义。

在图 6-2 等设置中,master分支发出的命令$ git merge feature只是将master指针移动到feature分支引用的地方。这个操作叫做快进。这是连接两个发展历史的最简单的例子。最终的存储库包含两个指向完全相同的提交的分支。请注意,在快进期间,不会创建新的提交。

请记住,历史将不包含我们使用$ git merge 命令的任何信息。特征分支的名称可以从历史中消失。这可以算是一个缺点。我们将在配方 6-6 中避开这个缺点。

您也可以尝试将master分支合并到feature分支中。那种情况下会发生什么?因为master分支已经合并到feature分支了,所以命令:

$ git checkout feature
$ git merge master

只会打印一条短信:Already up-to-date。存储库不会改变。

6-3.撤消快进

问题

你已经完成了配方 6-2 中呈现的快进操作,你对它不满意。你想撤销合并。换句话说,你想要将图 6-4 中所示的存储库转换回图 6-2 中所示的形式。

解决办法

cp命令克隆存储库06-02:

$ cd git-recipes

$ cp -R 06-02 06-03

$ cd 06-03

撤消合并最简单的解决方案是使用 reflog。由于使用了cp命令来克隆 reflog,所以它不是空的,如果使用了$ git clone命令,它就会是空的。命令$ git reflog打印的结果类似于:

0deae94 HEAD@{0}: merge feature: Fast-forward
757d501 HEAD@{1}: checkout: moving from feature to master
0deae94 HEAD@{2}: checkout: moving from feature to feature
0deae94 HEAD@{3}: clone: from c:/git-recipes/06-01

您可以使用$ git reset --hard HEAD@{1}撤销合并。

它是如何工作的

在合并的情况下,$ git merge命令之后的 reflog 条目HEAD@{1}指向您当前的分支,就像它在合并之前一样。将此作为参数传递给$ git reset --hard,您将撤销合并。即使您的 reflog 为空,您仍然可以使用 SHA-1 名称撤销合并,如在$ git reset --hard 757d501中所示。

6-4.平行发展 分叉分支

问题

您刚刚创建了一个包含应用新特性的分支。存储库看起来像图 6-2 中的所示。这是配方 6-4 的起点。但是,您不确定您在feature分支的工作是否已经完成。您不确定它是否已准备好进行合并。同时,你想继续在master分公司工作。你想切换到master分支并创建一些新的版本。这个配方之后的库应该看起来像图 6-5 。

9781430261032_Fig06-05.jpg

图 6-5 。配方 6-4 后获得的储存库

解决办法

用分支克隆配方 6-1 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-01 06-04

$ cd 06-04

并创建两个新的修订版本:

$ git simple-commit m4 m5

它是如何工作的

这个配方强调了这样一个事实,即不同分支中的工作可以并行地继续**。在这种情况下,我们说两个分支masterfeature 分叉。提交m4f1被创建的确切时间并不重要,插图通常会在同一级别显示它们,如图 6-5 中的所示。实际上,它们总是在不同的时刻创建的。因此,可视化修订图的命令,如$ git --oneline --graph --decorate --all$ gitk --all&,总是在不同的级别显示它们,如图图 6-6 。修订版m4m5是后来创建的,因此它们将出现在修订版f1f2f3之上。**

9781430261032_Fig06-06.jpg

图 6-6 。提交 m4 和 m5 是在提交 f1、f2、f3 之后创建的,因此它们被绘制在上面

$ gitk --all &命令从图 6-6 中的图 6-7 中呈现仓库。

9781430261032_Fig06-07.jpg

图 6-7 。使用 gitk 应用可视化配方 6-4 的储存库

请记住,在图 6-5 所示的存储库中进行快速合并是不可能的。您可以使用$ git merge命令的附加参数--ff-only进行检查。这个参数在 git 上设置了一个附加条件:执行合并,但是只有在快速前进的情况下。命令:

$ git merge --ff-only feature

master分支中执行的可以解释为下面用伪代码写的条件语句:

if (the merge of feature into master is a fast-forward) {
    $ git merge feature
}

图 6-5 中所示的库中执行的命令$ git merge --ff-only feature将无法产生输出:

fatal: Not possible to fast-forward, aborting.

使用--ff-only开关,您可以确保您的合并操作总是以快进方式进行。如果合并 不能快进完成,则中止。为了快速执行分叉分支的合并,我们将使用$ git rebase命令。这将是下一章的主题。

6-5.合并 分叉的分支

问题

你想要合并如图 6-5 所示的分支。f eature分支将并入master分支。您想要获取的存储库在图 6-8 中显示。图 6-8 强调了m4m5f1f2f3修订的创建顺序。在这个配方中,这个顺序并不重要,因此图 6-8 也可以像图 6-9 那样绘制。

9781430261032_Fig06-08.jpg

图 6-8 。将分叉的分支合并后得到的知识库

9781430261032_Fig06-09.jpg

图 6-9 。在图 6-8 中显示了储存库的可选视觉表示

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-04 06-05

$ cd 06-05

并用$ git merge feature命令合并分支。

它是如何工作的

在不可能快进的情况下,$ git merge命令创建一个额外的修订,称为合并提交 。此提交与您到目前为止创建的提交不同,因为它包含多个父提交。它连接两个或多个不同的版本。这使我们有机会将每个提交分类为非合并提交合并提交。一个合并提交是一个有两个或更多父提交的提交。非合并提交是只有一个父提交的提交。显然,在这个配方中创建的提交有两个父提交,因此它是一个合并提交。

当使用$ git log$ gitk检查历史时,您可以过滤掉这两种类型的提交。命令:

$ git log --oneline --merges

输出只合并提交,而

$ git log --oneline --no-merges

仅打印非合并提交。您还可以通过以下方式设置预期的最小和最大父节点数:

$ git log --oneline --max-parents=X --min-parents=Y

其中XY是任意正整数。

Git 支持引用,这些引用允许它使用脱字符号(^)选取合并提交的任何父元素。引用[REVISION]^[n]指向用[REVISION]标识的提交的第 n 个父提交。对于图 6-9 中的库,master¹指向版本m5master²指向修订f3,如图图 6-10 所示。我将它们称为第 n 个父引用。

9781430261032_Fig06-10.jpg

图 6-10 。第 n 个父引用 master¹ 和 master²

记住参考号[REVISION]∼[REVISION]∼1[REVISION]^[REVISION]¹是等价的。这是因为1是默认值,并且总是引用合并提交中的第一个父节点。

如果你想放心的使用$ git merge命令,你要记住当前分支是你合并到的分支,传递给$ git merge 命令的分支是要合并的分支。您合并到的分支(master分支)的末端成为合并提交的第一个父级,您合并到的分支(feature分支)的末端成为第二个父级。您合并的分支不会改变,它仍然指向与命令之前相同的修订。您合并到的分支会收到一个新的 commit,其注释类似于:

Merge branch 'X'

其中X是您合并的分支的名称(在我们的菜谱中是feature)。您可以记住上面的规则,记住,当在master分支上时,$ git log --oneline -1命令会打印:

6fb2 Merge branch 'feature'

正如您所猜测的,工作目录现在包含了来自两个分支的所有文件。命令$ ls输出文件:f1.txtf2.txtf3.txtm1.txtm2.txtm3.txtm4.txtm5.txt

可以完全按照配方 6-3 中的方法撤消合并。只是这次你不仅可以使用 reflog 和 SHA-1,还可以使用祖先和第 n 个父引用。假设你在master分支,以下两个命令将撤销本配方中讨论的合并:

$ git reset --hard master^
$ git reset --hard master∼

但是,如果你在 Windows 命令行工作,事情就复杂了。因为插入符号是一个特殊的字符,你必须以特殊的方式使用它。插入符号被 Windows shell 分析器用作转义字符。在 Linux shells 中,这个角色通常分配给反斜杠()字符。如果你想在 Windows 命令行中使用插入符号,你必须键入两次(^^).)此外,因为在 Windows 上 git 子命令是通过间接 shell 调用触发的,所以转义被执行两次。因此,如果您想在 Windows 命令行中使用引用master²,您必须键入四个 caretmaster^^^²。当然,如果您在 bash shell 中工作,这并不适用。

最有趣的情况是,当您想在嵌入字符串的 SQL 语句的正则表达式中使用反斜杠(\))时,比如:

$query = "SELECT * FROM paradox WHEARE content REGEXP '\\\\\\\\'";

所有三种语言—RegExp、SQL 和 PHP—都使用相同的转义字符,即反斜杠(\))。因此,一个反斜杠被编码为八个反斜杠!

6-6.避免了 的快进合并

问题

你的存储库看起来像图 6-2 。你想将分支feature合并到master中,这样历史看起来就像图 6-11 。您希望来自feature分支的所有修订被可视化地分组在一个灯泡中。

9781430261032_Fig06-11.jpg

图 6-11 。使用- no-ff 选项将特征分支合并到主分支的结果

解决办法

用分支克隆配方 6-1 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-01 06-06

$ cd 06-06

然后将feature分支与:

$ git merge --no-ff feature

它是如何工作的

$ git merge命令的选项--no-ff 改变了强制创建合并提交的默认行为,即使合并可以作为快进来执行。通过这种方式,您保留了提交f1f2f3相互关联的信息。它们都处理开发的同一个方面,共同构成一个完整的作品。如果出于某种原因,您需要将整个分支恢复或复制到历史中的其他地方,像这样组织提交将使操作更容易。

6-7.发散 多个分支

问题

您想要创建如图 6-12 所示的存储库。它包含五个分叉的分支。请注意,创建修订的实际顺序并不重要。该图显示了在完全相同的时刻创建的来自不同分支的修订,我们知道这是不正确的。您是单独工作的,并且您的所有修订都是按顺序创建的,一次一个,而不是并行的。但是这个图像的作用是强调,不管你提交的顺序是什么,配方 6-7 中描述的过程都工作得很好。

9781430261032_Fig06-12.jpg

图 6-12 。具有五个不同分支的存储库

解决办法

用分支克隆配方 6-1 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-01 06-07

$ cd 06-07

并创建四个新分支:

$ git branch a
$ git branch b
$ git branch c
$ git branch d

然后一个接一个地切换到每个分支并创建两个新的提交。这些命令如清单 6-1 中的所示。

清单 6-1。 创建分叉分支的命令如图图 6-9

$ git checkout master
$ git simple-commit m4 m5

$ git checkout a
$ git simple-commit a1 a2

$ git checkout b
$ git simple-commit b1 b2

$ git checkout c
$ git simple-commit c1 c2

$ git checkout d
$ git simple-commit d1 d2

$ git checkout master

它是如何工作的

例如,图 6-12 所示的库结构可以在你的产品发布新版本后获得。我使用像这样构造的存储库来给框架添加新的独立特性。一旦一个框架的稳定特性发布了,我就实现多样化的特性,比如授权、数据库连接、开发环境设置,以及不同分支中的各种扩展。通过这种方式,我可以创建满足特定需求的框架发行版。发行版可以包含在分支中实现的任何特性。每个功能都可以根据您的需要打开或关闭。这个发行版是用 merge 命令创建的:您合并的分支将出现在最终的发行版中。

用清单 6-1 中的命令创建的储存库表单图 6-12 将由$ git log --oneline --graph --all命令显示,如图 6-13 中的所示。

9781430261032_Fig06-13.jpg

图 6-13 。由$ git log - oneline - graph - all 命令绘制的来自图 6-12 的存储库

6-8.合并 多个分支

问题

在图 6-12 中所示的存储库中工作,您想要将四个分支abcd合并成master分支。您的目标是将图 6-12 所示的存储库转换成图 6-14 所示的存储库。

9781430261032_Fig06-14.jpg

图 6-14 。将分支 a、b、c 和 d 合并到主分支的结果

解决办法

用分支克隆配方 6-7 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-07 06-08

$ cd 06-08

并将四个分支合并成master分支执行:

$ git merge a b c d

它是如何工作的

命令$ git merge a b c dmaster中创建新的提交,标记为Merge branches 'a''b''c''d'。这将是在当前分支中创建的合并提交。它的五个父级将可以通过以下第 n 个父级引用来访问:

master¹
master²
master³
master⁴
master⁵

第一个父进程master¹指向与master分支在图 6-14 中指向的提交相同的提交。第二个父节点master²指向由a分支在图 6-14 中指向的提交。第三个父节点master³,通过b分支指向图 6-14 中表示的提交。诸如此类。正如您所猜测的,父节点的顺序取决于$ git merge a b c d命令传递给分支的顺序。

许多分支的合并可以用与方案 6-3 或方案 6-5 中相同的方式撤销。您可以使用 reflog、SHA-1 名称或第 n 个祖先引用:

$ git reset --hard master^

由带--graph开关的$ git log命令打印的图 6-14 中所示的库的可视化表示在图 6-15 中呈现。

9781430261032_Fig06-15.jpg

图 6-15 。由$ git log with - graph switch 命令绘制的配方 6-8 中的存储库

很少需要合并两个以上的分支。事实上,在很多流行的项目中,比如 jQuery、Twitter Bootstrap、Ruby on Rails、Symfony,都没有超过两个父级的提交。我所知道的同时合并两个以上分支的项目只有 git 和 Linux。令人惊讶的是,Linux 包含有 32 个父级的提交!您可以使用$ git log命令的--min-parents=n选项进行验证,例如:

$ git log --oneline --min-parents=32

另一个补充参数--max-parent=n设置由$ git log打印的提交的最大父项数量的要求。

当然,合并众多分支的能力并不重要。操作:

$ git merge a b c d

可以作为四个不同的合并来执行,每个合并只涉及两个分支:

$ git merge a
$ git merge b
$ git merge c
$ git merge d

上面唯一的缺点是历史将包含四个合并提交,而不是一个。

摘要

现在,你已经知道了$ git merge,你可以开始充分欣赏 git 分支模型。使用$ git merge处理合并到的分支和合并到分支。您合并到的分支是您当前的分支。您合并的分支是传递给$ git merge命令的分支。如果命令$ git branch打印:


  bar
* foo

那么对于$ git merge bar,我们有:

  • 是您合并到的分支
  • 是你合并的分支

一般来说,$ git merge命令执行两种操作之一:要么是快进,要么是合并

快进,也用 FF 表示,是通过在图中向前移动分支来更新分支的过程。当您要合并的分支合并到已经合并的分支中时,就会发生这种情况。在这种情况下,不会创建新的提交。该命令的唯一结果是您合并的分支(您的当前分支)的更新的 SHA-1 散列。如果你的分支出现分歧,快进是不可能的。

在另一种情况下,当 FF 不可能时,$ git merge命令通过创建新的提交来执行merge。这个新提交非常特殊:它至少有两个父提交。毫不奇怪,它被称为合并提交

本章之后,你应该能够合并任意数量的分支,如果你对结果不满意,可以撤销操作。您知道如何强制执行非快进合并,即使在默认情况下,操作将作为快进执行。最后,您知道如何使用--ff-only开关来避免执行非快进合并。

当我们深入工作流时,合并的所有这些方面都很重要。

七、分支变基

你可以用$ git merge$ git rebase命令连接两个不同的开发历史。在这一章中,我们将讨论重定基。合并在前一章已经讨论过了。

一般来说,重置基础是一种将分歧分支转换成线性历史的方法。您可以将它看作是一个自动的挑选操作,将一系列提交从图中的一个位置移动到另一个位置。当您开始与同一个存储库中的其他开发人员合作时,使用 rebasing 的优势将变得显而易见。通过使用 rebasing 命令,你将能够产生一个清晰的项目线性历史。

我们将从对重定基础的分叉分支的深入解释开始。我们将使用三种不同的方法来执行此操作:

  • 使用$ git rebase命令
  • 使用$ git format-patch$ git am命令
  • 使用$ git cherry-pick命令

这将为你提供一个坚实的背景和对重定基础工作方式的深刻理解。

然后,我们将继续单独使用$ git rebase来连接分离的分支(即,没有$ git merge命令)。在那里,您将学习如何通过重定基础来快进分叉的分支。

接下来,我们将讨论仅移动新分支的一部分的问题。这可以通过$ git rebase命令的--onto参数来实现,它适用于您的存储库包含三个或更多分支的场景。这个设置也将作为讨论提交范围的起点。在继续进行$ git rebase --onto操作之前,我们将讨论两点和三点操作符。

本章的最后一个秘籍将涉及灯泡。我们在配方 6-6 中学习了如何创建它们。如果你想保留合并的话,改变基础要困难得多。

7-1.改变分叉分支的基础

问题

您在一个拥有两个名为masterfeature的分支的存储库中工作。分支分叉了,你的存储库现在看起来像图 7-1(a) 。你想以这样的方式改造 ?? 分公司:

  • 历史是线性的(这意味着分支不再分叉)。
  • master分支合并为feature分支。
  • feature分支中进行的所有提交都在master分支的最顶端。

你想要实现的储存库在图 7-1(b) 中给出。

9781430261032_Fig07-01.jpg

图 7-1 。在重设基础之前(a)和重设基础之后 (b)来自配方 7-1 的储存库

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-04 07-01

$ cd 07-01

然后按照以下步骤操作:

  1. $ git checkout feature命令检查feature分支。
  2. $ git rebase master命令将feature分支复位到master分支上。
  3. $ git checkout master命令检查master分支。

它是如何工作的

$ git rebase master执行的转换可以被描述为在另一个分支的顶部应用由来自当前分支的提交引入的改变。这在图 7-2 中进行了描述。

9781430261032_Fig07-02.jpg

图 7-2 。将提交从当前分支“移动”到另一个分支之上

正如你从配方 5-8 中已经知道的,没有办法将版本从一个地方移动到另一个地方。你所能做的就是创建一个新的修订版,它将有相同的注释,并且将相同的变化引入到你的文件中。这就是为什么图 7-1(b) 和图 7-2 中的修改包含了素数。它强调了这样一个事实,即这些是用不同的 SHA-1 名称进行的新修订。

最初的修订版f1f2f3会发生什么?没什么?它们完好无损。更准确地说,可以如图 7-3 中的所示描述重置基础。最初的修订f1f2f3不再被任何分支引用——它们变成了悬空的修订。但是它们在数据库中保持不变。至少只要不使 reflog 过期并清理数据库。

9781430261032_Fig07-03.jpg

图 7-3 。将修订 f1、f2 和 f3 的副本更精确地重新基础化到另一个分支中—原始修订保持不变

图 7-3 中的包含了如何从重置基础中恢复的提示。要撤销重置基础,你所要做的就是改变feature分支所指向的版本。你应该记得上一章的内容,分支只是指针。你可以把它们当成贴纸:任何东西都可以从一个地方撕下来,然后再贴到另一个地方。此操作不会修改数据库,所有修订保持不变。如果你修改了feature分支,使其再次指向f3修订版,那么重定基础将被撤销。

让我们找到最初的版本f3。和往常一样,您可以使用$ git reflog命令。但这一次,它的输出可能会产生误导。用$ git log命令来探索 reflog 可能会更容易。我们希望获得所有提交的列表,这些提交:

  • 包含在 reflog 中
  • 有包含f3字符串的注释

清单 7-1 中给出了适当的命令。用--pretty参数定义的格式打印缩短的散列(%h占位符)、注释(%s占位符)和提交日期(%cd占位符)。每个提交都可以在 reflog 中出现多次。实际上,每次签出都会在 reflog 中存储一个对提交的新引用。多亏了管道化的sortuniq命令,清单 7-1 中的命令产生的输出将包含每个提交一次。

清单 7-1。 该命令列出所有引用日志提交,注释中包含 f3

$ git log --walk-reflogs --grep=f3 --pretty="%h %s %cd" | sort | uniq

当您找到正确的提交时,您可以使用$ git reset --hard [SHA-1]命令更改feature分支。如果您的存储库是干净的,那么这个命令可以被看作是将您当前的分支移动到任意版本的一种方式。使用两个单独的命令也可以达到相同的效果:

$ git branch -D feature
$ git checkout -b feature [SHA-1]

第一条命令删除feature分支;第二个创建一个新的feature分支,指向期望的修订。您可以将上述两个命令合并为一个:

$ git checkout -B feature [SHA-1]

开关-b是一个安全的开关:只有当存储库还没有包含这样的分支时,它才会创建一个分支。如果分支存在,$ git checkout -b失败。开关-B强制$ git checkout命令覆盖现有分支。

好的,我们知道重定基础是如何转换修订图的结构的。但是文件会怎么样呢?从这个意义上说,重定基础的结果与合并产生的结果完全相同。两个命令:

# current branch is feature
$ git rebase master
$ git merge master

导致工作目录的内容完全相同。工作目录包含两个分支的文件:featuremaster

一般来说,图 7-2 和 7-3 中描述的换基在两个分支上操作;因此,该命令需要两个参数:

$ git rebase a b

如果跳过第二个参数,将使用HEAD。因此,命令:

$ git rebase a
$ git rebase a HEAD

是等价的。要将feature分支重定位到master分支上,就像在这个配方中一样,您可以:

  • 将当前分支更改为feature,并使用一个参数进行重置,如下所示:

    $ git checkout feature
    $ git rebase master
    
  • 使用两个参数来重新设定基数——那么你当前的分支就不重要了:

    $ git rebase master feature
    

无论哪种情况,feature都是成功重置基础后的当前分支。

7-2.手动重置分叉分支

问题

为了更深入地了解重设基础,你想执行与配方 7-1 相同的转换,而不使用$ git rebase命令。在这个秘籍中,你想把重建基础分成两个操作:创建补丁和应用补丁。通过这种方式,补丁可以由一个开发人员创建,通过电子邮件发送,然后由另一个开发人员应用。

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-04 07-02

$ cd 07-02

然后按照以下步骤操作:

  1. $ git checkout feature 结账feature分支

  2. $ git format-patch --ignore-if-in-upstream master
    

    f1f2f3版本生成补丁

  3. 进入分离头状态,其中HEAD指向与master分支相同的版本。您将通过执行$ git checkout git rev-parse master`` 来实现

  4. 使用$ git am *.patch 应用补丁

  5. 使用$ git checkout -B featurefeature分支移动到当前版本

  6. $ rm *.patch删除补丁

它是如何工作的

feature分支包含三个提交f1f2f3,它们不包含在master分支中。您可以通过以下方式进行检查:

$ git log --oneline master..feature

输出将显示三个提交:

0deae94 f3
c1cab03 f2
3df8f34 f1

参数master..feature 指定一组提交。您可以将其视为减法运算符:

feature - master

或者更准确地说:

revisions included in feature - revisions included in master

这就是您如何发现在 rebase 期间哪些提交已经或将要被转移到其他位置。命令:

$ git log --oneline master..feature

将打印将被移动的提交。当在重置基础后发出时,它将打印被移动的提交。

现在我们想为这三次提交生成补丁。修补程序是一个文本文件,它精确地描述了要在项目文件中引入的变更集。当在feature分支发出时,命令:

$ git format-patch --ignore-if-in-upstream master

生成三个名为0001-f1.patch0002-f2.patch0003-f3.patch的文本文件。第一个文件是修订版f1的补丁。第二个文件是修订版f2的补丁。第三个文件是修订版f3的补丁。参数- ignore-if-in-upstream保证只生成还没有在master分支中合并的提交补丁。当您想要为许多分支多次生成补丁时,此选项变得很有必要。

所有生成的文件都是新的和未被跟踪的,正如$ git status -s命令所证明的:

?? 0001-f1.patch
?? 0002-f2.patch
?? 0003-f3.patch

在配方 5-6 中,你学到了新的未跟踪文件不会影响签出命令。您知道当前分支可以切换,未跟踪的文件将保持不变。这正是我们想要做的,因为我们想要将补丁应用到master分支。然而,因为master分支应该在结果存储库中保持不变,所以我们将使用分离的 HEAD 状态。

这个秘籍给了你更多关于分离头部状态的详细实用的知识。我警告过你要避免它,所以你可能会问为什么要在 git 中引入它。分离的 HEAD 状态背后的原因是一些命令,比如 rebase,改变了修订的图形。为了保留原始分支,需要在分离的头状态下执行这些操作。如果出现问题,您可以很容易地返回到操作之前的状态,因为原来的分支没有改变。

好的,现在我们需要进入一个分离的 HEAD 状态,其中HEAD指向与master分支相同的修订。如何产生master分公司所指修订的 SHA-1 名称?您可以使用$ git rev-parse命令来完成:

$ git rev-parse master

上面的命令将一个符号引用,比如HEADHEAD∼featureinfo²,转换成阿沙-1 名称。使用反勾运算符将$ git rev-parse的结果作为参数传递给 checkout,您将进入所需的分离头部状态。完整的命令如清单 7-2 中的所示。您的存储库现在看起来如图 7-4 所示。

清单 7-2。

$ git checkout `git rev-parse master`

9781430261032_Fig07-04.jpg

图 7-4 。该库来自图 7-1(a)后的命令来自清单 7-2

当存储库看起来像图 7-4 时,我们应用存储在带有.patch后缀的文件中的补丁:

$ git am *.patch

上面的命令使用HEAD指针作为父指针来再现提交f1f2f3。存储库现在看起来像图 7-5 。

9781430261032_Fig07-05.jpg

图 7-5 。从图 7-4 中的存储库在应用了$ git am *补丁后。补丁命令

最后一步是现在改变feature分支。我们希望它指向我们当前的修订。为此我们可以使用$ git checkout命令。然而,命令$ git checkout -b feature将不起作用。原因相当明显:feature分支已经存在。不过,我们可以使用-B开关强制结帐:

$ git checkout -B feature

更新后的知识库如图 7-6 所示。提交f1f2f3在数据库中仍然作为悬空修订可用,但是它们没有显示在图中。

9781430261032_Fig07-06.jpg

图 7-6 。来自的储存库图 7-5 后的$ git checkout -B 特性

我们不再需要补丁了。用$ rm *.patch 命令删除它们。

使用樱桃采摘 rebase

您可以使用$ git cherry-pick命令获得类似的结果

  1. 进入分离头状态:$ git checkout git rev-parse master``
  2. $ git cherry-pick feature∼2重新应用HEAD中的修订f1
  3. $ git cherry-pick feature∼1重新应用HEAD中的修订f2
  4. $ git cherry-pick feature重新应用HEAD中的修订f3
  5. 使用$ git checkout -B featurefeature分支移动到当前版本

上述解决方案的主要缺点是,这里您必须知道您想要重新应用哪些修订。命令$ git format-patch将卸下你肩上的重担!另外$ git cherry-pick不会创建补丁。要通过电子邮件发送补丁,你必须用$ git format-patch命令生成补丁。

image 提示配方 7-2 背后的主要原因是为了让你更深入地了解$ git rebase是如何工作的。不要使用$ git format-patch$ git am$ git cherry-pick来重置你的分支,除非你想通过电子邮件把补丁发给其他人。请使用$ git rebase命令。配方 7-2 中的分析将有助于你理解重置基础的内部原理。根据您的工作流程,每次您想要发布您的作品时,都可能需要重新设定基准。

7-3.将分歧的分支连接成线性历史

问题

你在一个拥有两个分支masterfeature的存储库中工作,如图图 7-7(a) 所示。您希望将feature分支合并到master分支中,这样得到的历史是线性的,也就是说,它不包含合并提交。该配方的起点如图 7-7(a) 中的所示。您想要获取的存储库在图 7-7(b) 中显示。

9781430261032_Fig07-07.jpg

图 7-7 。起点和结果将分歧分支加入线性历史

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-04 07-03

$ cd 07-03

然后按照以下步骤操作:

  1. $ git rebase master featurefeature分支重置到master分支上
  2. $ git rebase feature master?? 将master分支重置到feature分支上

它是如何工作的

我们从图 7-7(a) 所示的储存库开始。该配方的第一步执行配方 7-1 中描述的操作。在$ git rebase master feature之后,存储库将看起来像图 7-1(b) 。我们需要将master分支快进到feature分支。这正是第二个命令$ git rebase feature master的目的。

快进可以用$ git merge或者$ git rebase来完成。下面是用$ git merge命令快进master分支的命令:

$ git checkout master
$ git merge feature

这是与$ git rebase命令相同的命令:

$ git rebase feature master

在配方 6-2 中讨论了使用$ git merge快进。

7-4.分叉的三个分支

问题

你的存储库包含两个分叉的分支masterfeature,如图图 7-8(a) 所示。首先,你想在你的feature分部的最新修订版的基础上,研究一些新的想法。您需要创建一个名为brave-idea的新分支,并将您的更改提交为修订版b1b2。接下来您想要切换到feature分支并创建三个新的修订f4f5f6。你想要实现的库如图图 7-8(b) 所示。

9781430261032_Fig07-08.jpg

图 7-8 。通过对存储库(a)应用配方 7-4,您将得到存储库(b)

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-04 07-04

$ cd 07-04

然后按照以下步骤操作:

  1. $ git checkout -b brave-idea feature创建并检查brave-idea分支
  2. $ git simple-commit b1 b2brave-idea分支中创建两个版本
  3. $ git checkout feature检查feature分支
  4. $ git simple-commit f4 f5 f6feature分支中创建三个版本
  5. $ git checkout master将当前分支更改为master

它是如何工作的

这个秘籍解释了如何创建许多不同的分支。我们只使用众所周知的命令来实现这一点:clonecheckoutsimple-commit。这就是如何轻松生成具有给定结构的存储库。如果您想分析 git 命令及其对图形结构的影响,这个功能非常有用。

请记住,当您使用带有--graph选项的$ git log命令时,您得到的图形可能会稍有不同。从图 7-8(b) 中得到的一个库的$ git log --oneline --graph --decorate --all 结果如图图 7-9 所示。

9781430261032_Fig07-09.jpg

图 7-9 。该库来自图 7-8(b) 由$ git 绘制的 log-one line-graph-decoration-all

一旦你创建了如图 7-8 所示的存储库,许多关于修订图的问题可能会出现。例如以下内容:

  • 如何找到分支ab的共同祖先?
  • 如何寻找任意支数的共同祖先?
  • 如何找到两个分支的差异a - b,即包含在分支a中但不包含在分支b中的修订?
  • 如何找到两个分支的对称差异a ∆ b,即包含在ab中但不同时包含在两者中的修订?
  • 如何查找包含在abc分支中,不包含在def分支中的修订?

两个分支的共同祖先是包含在两个分支中的最新修订。对于分支featurebrave-idea,它是f3。对于masterfeaturem3。您可以使用以下方法找到共同祖先:

$ git merge-base feature brave-idea

如果你想得到两个以上分支的共同祖先,使用--octopus参数。命令:

$ git merge-base --octopus feature brave-idea master

打印m3提交的 SHA-1。

提交的范围已经在配方 7-2 中讨论过了。特殊运算符..被解释为分支的差异。命令:

$ git log --oneline master..brave-idea

打印提交的b2b1f3f2f1,同时:

$ git log feature..master

输出版本m4m5

由两个分支引入的新提交集由...操作符解析。这是分支的对称差异。的输出:

$ git log feature...brave-idea

f6f5f4b2b1组成。

指定包含和排除修订的更详细的方法是使用--not操作符。命令:

$ git log a b c --not d --not e --not f

打印包含在abc中,不包含在def中的修订。这也可以写成:

$ git log a b c ^d ^e ^f

使用上面的语法,你可以用清单 7-3 中的命令列出masterfeaturebrave-idea分支中引入的新修订。该命令输出修订:

  • f6f5f4——在feature分支引入的提交
  • b2b1——在brave-idea分行引入的提交
  • m5m4——在master分行引入的提交

清单 7-3。

$ git log --oneline
    master feature brave-idea
    ^`git merge-base master feature`
    ^`git merge-base feature brave-idea`

我们如何达到上述结果?我们包括所有三个分支机构:

master feature brave-idea

然后排除通过masterfeature分支的共同祖先可用的提交(它是修订版 m3):

^`git merge-base master feature`

并且排除通过featurebrave-idea分支的共同祖先可用的提交(它是修订版f3):

^`git merge-base feature brave-idea`

使用用反勾运算符定义的 shell 子命令,我们不必复制/粘贴嵌入的m3f3的 SHA-1 名称。

在研究修订图时,您可能还会发现生成带有给定注释的提交的 SHA-1 名称的命令很有用:

$ git log --format="%h" --grep=XXX --all

上述命令考虑了所有分支(--all选项),并搜索包含XXX字符串的修订。多亏了--format参数,输出只包含缩短的 SHA-1 名字。

7-5.部分重置和

问题

您决定在图 7-10(a) 所示的库中的brave-idea分支中引入的代码现在可以与他人共享了。因此,您想要将修订版b1b2移动到master分支。你想要实现的库如图图 7-10(b) 所示。如果在你制作brave-idea的过程中,你最初的想法不断发展,变得足够大,可以作为独立的特征来对待,这种情况就会发生。

9781430261032_Fig07-10.jpg

图 7-10 。通过对存储库(a)应用配方 7-5,您将得到存储库(b)

解决办法

用分支克隆配方 7-4 中的存储库:

$ cd git-recipes

$ git clone-with-branches 07-04 07-05

$ cd 07-05

然后用清单 7-4 中所示的命令将brave-idea分支重置到master分支上。

清单 7-4。 将图 7-10(a) 中所示的库转换成图 7-10(b) 中所示状态的命令

$ git rebase --onto master feature brave-idea

它是如何工作的

命令$ git rebase --onto作用于三个分支:

$ git rebase --onto a b c

第一个分支是我们将重新应用补丁的分支。另外两个分支定义了要重新应用的补丁集。它将是由双点运算符b..c定义的集合。换句话说,该命令获取包含在c中但不包含在b中的修订,并在a上重新应用它们。如果操作成功,则c被移动并指向结果提交。

清单 7-4 中的命令可以在任何分支中执行。结果总是一样的:提交b1b2将作为b1'b2'被重新应用到master分支之上。操作后的分支c将是你当前的分支。

如果您忽略了最后一个参数,那么您当前的分支将被重置。以下命令是等效的:

$ git rebase --onto foo bar
$ git rebase --onto foo bar HEAD

我们可以说清单 7-4 中的命令相当于两个命令:

$ git checkout brave-idea
$ git rebase --onto master feature

7-6.为分叉分支创建灯泡

问题

你的知识库看起来像图 7-11 中的(a)。您想要将feature分支中引入的变更合并回master分支,以这样的方式,重新应用的修订f1'f2'f3'在来自master分支的修订之上形成一个灯泡。您希望实现的存储库如图 7-11(b) 所示。

9781430261032_Fig07-11.jpg

图 7-11 。将配方 7-6 应用于图 6-5 中的存储库后,您将获得该存储库

解决办法

用分支克隆配方 6-4 中的存储库:

$ cd git-recipes

$ git clone-with-branches 06-04 07-06

$ cd 07-06

然后按照以下步骤操作:

  1. $ git rebase master featurefeature分支重置到master分支上
  2. $ git checkout master切换到master分支
  3. $ git merge --no-ff featurefeature分支合并成master分支

它是如何工作的

配方 7-6 包括两个步骤:

  • 首先,我们使用配方 7-1 来转换存储库,如图 7-1 所示。
  • 接下来,我们使用配方 6-6 进行合并,形成一个灯泡。

7-7.在子分支 中创建灯泡

问题

你的知识库看起来像图 7-12(a) 。你想将brave-idea分支合并回feature分支作为灯泡。您希望实现的知识库如图图 7-12(b) 所示。

9781430261032_Fig07-12.jpg

图 7-12 。配方 7-7 中考虑的储存库

解决办法

用分支克隆配方 7-4 中的存储库:

$ cd git-recipes

$ git clone-with-branches 07-04 07-07

$ cd 07-07

然后按照以下步骤操作:

  1. $ git rebase feature brave-ideabrave-idea分支重置到feature分支上
  2. $ git checkout feature切换到feature分支
  3. $ git merge --no-ff brave-ideabrave-idea分支合并成feature分支
  4. $ git branch -d brave-idea删除brave-idea分支
  5. $ git checkout master检查master分支

它是如何工作的

配方 7-7 显示了如何将配方 7-6 应用于featurebrave-idea分支。您可能认为这是多余的,但最终的存储库对于下一个秘籍是必需的。

7-8.给树枝换上灯泡

问题

你的存储库现在看起来像图 7-12(b) 。您想要将feature分支重新定位到master分支上。

解决办法

用分支克隆配方 7-7 中的存储库:

$ cd git-recipes

$ git clone-with-branches 07-07 07-08

$ cd 07-08

然后用$ git rebase master featurefeature分支复位到master分支上。您将获得如图图 7-13 所示的存储库。注意feature分支不再包含merge branch 'brave-idea'修订。

9781430261032_Fig07-13.jpg

图 7-13 。该库来自图 7-12 后的$ git rebase master 特性

它是如何工作的

正如你在图 7-13 中所看到的,重置基础只在非合并提交时起作用。所有合并提交都将丢失。重定基础总是产生一条直线的提交,没有灯泡或合并。在有球茎的树枝的情况下,它不一定是你所期望的。如果你想保留合并和灯泡,你不能使用简单的 rebase 命令。您必须移动整个分支,然后重新生成合并提交。

好好看看图 7-13 。提交b1b2用两个素数b1''b2''表示。双撇号强调了这些是新提交的事实。它们引入了与图 7-12 中的b1'b2'以及图 7-8 中的b1b2相同的变化,但它们的 SHA-1 名称不同。

7-9.重设基础期间保留合并

问题

你的知识库看起来像图 7-14(a) 。你要将feature树枝重新放在master树枝上,保留灯泡。你想要实现的库如图图 7-14(b) 所示。

9781430261032_Fig07-14.jpg

图 7-14 。通过将配方 7-9 应用于存储库(a ),您将得到存储库(b)

解决办法

用分支克隆配方 7-7 中的存储库:

$ cd git-recipes

$ git clone-with-branches 07-07 07-09

$ cd 07-09

然后用--preserve-merges参数进行重置:

$ git rebase --preserve-merges master feature

它是如何工作的

参数--preserve-merges强制 git 在重置基础期间保留合并。

摘要

本章介绍了重定基准的概念——从一个分支的顶部复制另一个分支的提交序列的操作。你可以把它当成一个工具,把分叉的分支转化成线性的历史。重设基础的语法允许您在一个完整的分支之上重设基础。您可以通过以下方式实现这一点:

$ git rebase dest src

其中,src是您要从中获取提交的分支,而dest是提交将被重新应用的分支。此次操作后,src将成为当前分支机构。

您还可以使用以下命令执行部分重置:

$ git rebase --onto dest part src

这里的dest也是提交将被重新应用的分支。要移动的提交集由partsrc分支定义。该操作移动包含在src中并从part分支中排除的提交。你可以把它作为dest重新应用的(src - part)差值记忆下来。

在这两种情况下,最终的src分支都可以省略。如果是这种情况,那么将使用当前分支。这些命令:

$ git rebase dest
$ git rebase dest HEAD

与以下命令完全等效:

$ git rebase --onto dest part
$ git rebase --onto dest part HEAD

在这一章中,我们第一次有意地在超脱的头部状态下工作。分支状态可以被视为以原子方式执行操作(如重置基础)的手段。该操作在分离的头部状态下执行。成功完成后,我们根据需要调整分支。否则不会修改分支,并且可以取消操作。在第九章关于冲突的讨论中会有更多关于这方面的内容。

请记住,默认情况下,重置基础会跳过合并提交。如果需要,您可以使用--preserve-merges选项保存它们。

在本章中,顺便说一下,你还学习了如何用$ git merge-base命令找到两个分支的共同祖先,以及如何指定范围提交。提交范围可以用两个特殊的操作符.....来定义,或者用更详细的方式来定义。

当您键入a..b时,它是一组包含在b中但不包含在a中的提交。你可以把它想成一个区别(b - a)

三个点c...d指定对称差,即两组提交的集合:

  • c中可用而d中不可用的
  • 以及那些在d中可用而在c中不可用的

这可以算是(c - d) + (d - c)

更详细的语法使用--not操作符,缩写为^来排除分支。范围:

a b c ^d ^e ^f
a b c --not d --not e --not f

包括abc中可用的提交,排除def中可用的提交。

八、修改历史记录

本章介绍修改修订图结构的各种命令。有时你需要将三个不同的版本合并成一个版本。在其他时候,您可能需要相反的操作:将一个单独的提交分成许多单独的提交。无论是哪种情况,请记住 git 版本是永久的。他们永远不会改变。一旦创建了修订,就无法对其进行修改。你所能做的就是用秘籍 3-12 和 5-4 中介绍的方法把它扔掉。因此,每当我说“让我们修改一个修订”这样的话,我脑海中的操作是创建一个类似于原始版本的新修订。原始版本在 git 数据库中保持不变。它可以通过符号引用而不是 reflog 来访问,但它仍然存在。直到下一次数据库清除。

如果任何操作产生的结果令您不满意,您可以随时返回到以前的状态。您所需要的只是包含正确快照的修订名称。可以使用 reflog 来查找该名称,但也可以创建一个临时分支来保存对所需修订的引用。因为修订不会改变,所以您不需要担心修改修订的后果。无论您做什么,都不会改变已经存储在数据库中的修订。提交、重置基础和合并只会产生新的修订,这些操作不会修改现有的修订。修改修订是不可能的。

这对于我研究 git 来说是一个真正的突破。一旦我学会了如何重构修订图以及如何撤销各种操作,我就获得了自由使用该工具的信心。

8-1.修订最新版本

问题

您刚刚提交了一组变更到存储库中,一分钟后您意识到有一些额外的修改应该被合并到先前的版本中。您不想再创建另一个提交;您可能希望通过添加一些额外的更改来修改现有的修订。

解决办法

创建新的存储库:

$ cd git-recipes

$ git init 08-01

$ cd 08-01

然后按照以下步骤操作:

  1. 用$ echo lorem > lorem.txt 创建文件 lorem.txt
  2. 使用$ git add lorem.txt 暂存文件
  3. 用$ git commit -m "lorem "提交文件

存储库包含一个标记为lorem的修订。清单 8-1 显示了$ git log --pretty=fuller命令的输出。

清单 8-1。 我们希望修改的原始版本

commit 5a786865f21b5c1725e56c2bf60f6516ce736b9b
Author:     Włodzimierz Gajda <gajdaw@gajdaw.pl>
AuthorDate: Thu Aug 22 07:02:00 2013 +0200
Commit:     Włodzimierz Gajda <gajdaw@gajdaw.pl>
CommitDate: Thu Aug 22 07:02:00 2013 +0200

    Lorem

现在您意识到存储在lorem.txt文件中的文本应该大写并扩展。为了更深入地了解 git 的内部,我们将在不同的用户名下修改版本。

修改本地存储在这个特定存储库中的user.nameuser.email配置设置。您可以通过以下方式实现这一目标:

$ git config --local user.name "John Doe"

$ git config --local user.email john@example.net

最后,按照以下步骤修改版本:

  1. 用$ echo Lorem Ipsum Dolor > lorem.txt 修改 lorem.txt 的内容
  2. 使用$ git add lorem.txt 暂存文件
  3. 使用$ git commit-amend-m " Lorem Ipsum Dolor "提交文件

历史仍然只包含一个版本。命令$ git log --pretty=fuller打印清单 8-2 中所示的输出。

清单 8-2。$ git commit-amend 命令创建的提交

commit f63bce5e17a3ba02b0dbee13bb56ceabfd622ce7
Author:     Włodzimierz Gajda <gajdaw@gajdaw.pl>
AuthorDate: Thu Aug 22 07:02:00 2013 +0200
Commit:     John Doe <john@example.net>
CommitDate: Thu Aug 22 07:07:45 2013 +0200

    Lorem Ipsum Dolor

两个版本,原始版本和修正版本,都可以通过 reflog 获得。命令$ git reflog输出:

f63bce5 HEAD@{0}: commit (amend): Lorem Ipsum Dolor
5a78686 HEAD@{1}: commit (initial): lorem

因此,您可以随时撤销用$ git reset --hard HEAD@{1}修改的内容。

它是如何工作的

$ git commit命令的参数--amend允许您修改历史中的最新版本。命令$ git commit --amend:

  • 从历史中获取最近的提交(在配方 8-1 中,它是名为5a78的提交;文件lorem.txt包含lorem;提交如清单 8-1 所示
  • 获取暂存区的当前状态(在配方 8-1 中,它是带有Lorem Ipsum Dolor的暂存文件lorem.txt
  • 并将它们组合成一个新的修订(在配方 8-1 中,它是名为f63b的提交;提交如清单 8-2 所示

这个新版本(f63b)取代了历史中的原始版本(5a78)。

从技术上讲,该命令不会修改版本。它创建一个新的提交。您可以使用$ git reflog命令找到两个提交的名称。原始提交在 git 数据库中一直悬而未决,直到最终被垃圾收集操作删除。

image 提示记住——git 版本是永久的!不可能更改提交中存储的任何信息并保留相同的 SHA-1。$ git commit --amend命令创建一个全新的版本,然后更新主分支以指向新的版本。

为什么我们在这个配方中改变了user.nameuser.email的配置?它将帮助您理解 git 处理日期和分配作者身份的方式。每个提交包含四个属性:AuthorAuthorDateCommitCommitDate。他们储存:

  • Author—作者的名字
  • AuthorDate—最初提交的日期
  • Commit—委托人的姓名
  • CommitDate—历史中引入提交的日期

当您第一次创建提交时,AuthorCommit都将被设置为您的名字。存储在AuthorDateCommitDate中的日期将是相同的。这种情况如清单 8-1 所示。

如果用--amend选项修改提交会发生什么?Git 保留原来的AuthorAuthorDate字段,并为CommitCommitDate字段设置新值。这显示在清单 8-2 中。同样的规则也适用于你的提交。

image 提示 Git 不关心你的文件的最后修改日期。Git 跟踪内容——文件的最后修改日期不会以任何方式影响您的修订。存储在数据库中的每个提交都包含AuthorDateCommitDate。这些日期是在提交、rebase 或 cherrypick 时设置的。文件的最后修改日期是在您签出文件时设置的。例如,这是在切换分支时完成的。

细心的读者会注意到,$ git commit --amend引入的修改的作者被错误地归于原作者。在配方 8-1 中,两个词IpsumDolor是由用户 John Doe 创作的,但被认为是 odzimierz Gajda。然而,实际上这种情况从来不会发生,因为不允许修改其他开发人员提交的内容。你可以对它们进行精选或重新排序,但这样作者才能被正确地归属。

8-2.删除最近的版本

问题

您希望从当前分支中删除两个最新的修订。图 8-1 中描述了您想要实现的转变。

9781430261032_Fig08-01.jpg

图 8-1 。删除两个最新修订

解决办法

创建一个包含修订版本abcd的新存储库:

$ cd git-recipes

$ git init 08-02

$ cd 08-02

$ git simple-commit a b c d

您的存储库现在包含四个修订版。全部包含在master分支中。使用命令$ git reset --hard HEAD∼2删除两个最新版本。现在$ git log --oneline命令只返回两个版本:a 和 b。版本 c 和 d 从历史中删除。

image 提示你可以用这个方法删除任意数量的最近提交。命令$ git reset --hard HEAD∼13将删除最后 13 个提交。

它是如何工作的

参考号HEAD∼2指向图 8-1 中的版本b。你也可以使用修订版的 SHA-1b来达到同样的效果。假设修订版b的名称是a1b2c3d4,以下命令是等效的:

$ git reset --hard HEAD∼2
$ git reset --hard a1b2c3d4  # SHA-1 of revision b

正如您已经知道的,git 命令通常不会从数据库中删除对象。因此,在图 8-2 中可以更准确地描述配方 8-2 中执行的操作。修订版cd在数据库中保持可用,直到它们在 reflog 中的所有符号引用都被清除并且数据库被清理。要撤消操作,您可以随时使用 reflog。

9781430261032_Fig08-02.jpg

图 8-2 。修订版 c 和 d 一直悬而未决,直到它们被 git gc 清除

image 提示配方 8-2 中讨论的命令在配方 3-5 中使用,以检验所需的版本。

8-3.将许多修订压缩成一个修订

问题

您的存储库包含相当多的修订。你想把最后三个版本压缩成一个版本。图 8-3 中的描述了您想要执行的操作。

9781430261032_Fig08-03.jpg

图 8-3 。压缩最后三次修订

解决办法

创建一个包含修订版本abcd的新存储库:

$ cd git-recipes

$ git init 08-03

$ cd 08-03

$ git simple-commit a b c d

您的存储库现在包含四个修订,工作目录包含四个文件。当然,您可以使用$ git log$ ls命令来检查它。如果您想验证哪些文件包含在最新版本中,请使用以下命令:

$ git show --name-only HEAD

它将打印上一次修订带有注释d的信息,其中包括一个名为d.txt的文件。同样,您可以列出任何以前版本中包含的文件:

$ git show --name-only HEAD∼
$ git show --name-only HEAD∼2

让我们将最后三个提交压缩到一个版本中。为此,请运行以下命令:

$ git rebase -i HEAD∼3

重置命令的开关-i打开交互模式。该命令将启动 vim 编辑器,内容如清单 8-3 中的所示。更改编辑器的内容——键入清单 8-4 中的命令。您应该将出现在第二次和第三次提交之前的单词 pick 改为 fixup

清单 8-3。$ git rebase-I HEAD 后 vim 的内容∨3

pick f2136a0 b
pick a36ee90 c
pick 46c002f d

# Rebase d344a8a..46c002f onto d344a8a
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell

清单 8-4。 您必须在$ git rebase -i HEAD 之后键入的 vim 内容 3

pick f2136a0 b
fixup a36ee90 c
fixup 46c002f d

在你输入了清单 8-4 中的内容后,关闭编辑器。然后 git 会执行操作。在此之后,使用以下内容检查存储库的历史记录:

$ git log --oneline

该命令将只打印两个提交ab。根据$ ls命令,工作目录仍然包含四个文件。命令:

$ git show --name-only HEAD

证明最后一次提交现在包含三个文件b.txtc.txtd.txt

它是如何工作的

在交互式重定基础期间,git 将您在编辑器中键入的内容视为子命令列表。例如,清单 8-4 包含三个子命令。其中之一是:

fixup a36ee90 c

其含义是:“对修订版 a36ee90c 执行修正操作。”以下是交互式重置基础可用子命令的完整列表:

  • pick—提交将出现在结果历史记录中
  • reword—将使用提交,但 git 将允许修改其注释
  • edit—将使用提交,但 git 将允许修改(添加和删除文件)
  • squash—提交将被压缩到前一个提交中,git 将允许修改结果提交的注释
  • fixup—与 squash 相同,但这次 git 不允许修改结果修订的注释(将使用第一个修订的注释)
  • exec—该命令允许你执行任意的外壳命令

每个子命令都可以用它的首字母缩写。您键入的命令由 git 按照它们在编辑器中出现的顺序逐一执行。

那么清单 8-4 中显示的命令是什么意思呢?其中有三个:第一个是 pick 命令,第二个是 fixup 命令,第三个是另一个 fixup 命令。第一个命令

pick f2136a0 b

选择提交b。因此,它将出现在结果历史中。您可以将 pick 命令视为一个精选命令。应用由版本f2136a0定义的补丁。下一个命令:

fixup a36ee90 c

将提交c压缩为之前的提交b。fixup 命令不允许您修改结果提交的注释。您将获得包含来自提交bc的变更集的提交,并且用提交b的原始注释来表示。

最后一个命令:

fixup 46c002f d

执行一次挤压。这一次,git 将 commit d压缩为将c压缩为b的结果。因此,您将以注释b表示的单个提交结束,并合并来自提交bcd的变更集。

交互式重置基础执行的所有操作都存储在 reflog 中。现在,命令$ git reflog打印以下结果:

e3fc0e0 HEAD@{0}: rebase -i (finish): returning to refs/heads/master
e3fc0e0 HEAD@{1}: rebase -i (squash): b
5eb1d5a HEAD@{2}: rebase -i (squash): # This is a combination of 2 commits.
f2136a0 HEAD@{3}: checkout: moving from master to f2136a0
46c002f HEAD@{4}: commit: d
a36ee90 HEAD@{5}: commit: c
f2136a0 HEAD@{6}: commit: b
d344a8a HEAD@{7}: commit (initial): a

正如您可能猜到的那样,原始修订会保留在数据库中,直到它们被清除。如果您想撤销重设基础,您需要以下命令:$ git reset --hard HEAD@{4}

给定版本中修改的文件列表由$ git show --name-only [REVISION]命令打印。您也可以使用$ git log --name-only REVISION∼..REVISION$ git diff --name-only REVISION∼..REVISION来获得类似的结果。范围REVISION∼..REVISION$ git log$ git diff命令的输出限制为仅一个版本。

8-4.将一个修订拆分成多个修订

问题

存储库中的最新版本在工作目录中引入了三个新文件。您无意中提交了所有三个文件,一分钟后才意识到每个文件都应该存储在单独的修订中。您希望将最新修订拆分为三个不同的修订,每个修订都与一个文件相关。配方 8-4 与配方 8-3 相反。

9781430261032_Fig08-04.jpg

图 8-4。将最近的修订分成许多不同的修订

解决办法

创建一个新的存储库,它将包含两个修订版本:ab。第一次修订应包括一个文件a.txt,第二次修订应包括三个文件:b.txtc.txtd.txt:

$ cd git-recipes

$ git init 08-04

$ cd 08-04

$ echo a > a.txt

$ git add -A

$ git commit -m a

$ echo b > b.txt

$ echo c > c.txt

$ echo d > d.txt

$ git add -A

$ git commit -m b

使用$ git log --oneline命令验证存储库包含两个修订版,并且最后一个修订版确实包含三个文件。$ git show --name-only命令的输出将打印三个文件名:b.txtc.txtd.txt

现在,将您的历史重置为修订版a,将修订版b引入的修改作为新的未提交的变更保留在工作目录中。你可以用$ git reset HEAD∼命令来做这件事。在这个命令之后,由$ git status -sb返回的库的状态将如下:

?? b.txt
?? c.txt
?? d.txt

如您所见,文件现在未被跟踪。您可以创建三个不同的修订版本:

$ git add b.txt
$ git commit -m b

$ git add c.txt
$ git commit -m c

$ git add d.txt
$ git commit -m d

秘籍完成了。您可以使用$ git log命令检查历史包含四个版本abcd。每个修订版都包含一个文件。要验证它,请使用以下命令:

$ git show --name-only HEAD
$ git show --name-only HEAD∼
$ git show --name-only HEAD∼2

它是如何工作的

为了更深入地了解$ git reset命令 转换您的存储库的方式,您需要彻底理解回购的结构。存储库包括:

  • 工作目录
  • 集结地
  • 数据库

存储在.git/HEAD中的头指向存储在数据库中的一个提交。

工作目录可以解释为项目的一个快照。那很清楚。但是同样的,你也可以把 staging 区域和你的 HEAD 指针当作两个不同的快照。因此,我们可以说,在任何给定的时间点,您的存储库在三个不同的快照上运行:

  • 第一个快照—工作目录
  • 第二个快照—暂存区
  • 第三个快照—存储在 HEAD 指向的修订中的快照

让我们假设您刚刚用$ git simple-commit lorem创建了一个修订。存储库是干净的,工作目录包含一个名为lorem.txt的文件,存储一个字符串lorem

当存储库干净时,所有三个快照—工作目录、暂存区和磁头—都是相同的。让我们用$ echo foo > lorem.txt来修改lorem.txt 文件。这次操作后$ git status -sb命令打印出来:

_M lorem.txt

存储在三个快照中的lorem.txt文件包含:

  • 第一个快照(工作目录):该文件包含foo
  • 第二个快照(暂存区):该文件包含lorem
  • 第三张快照(头):该文件包含lorem

因此_M意味着工作目录中的文件不同于暂存区中存储的文件,同时暂存区中存储的文件与HEAD指向的修订中的文件相同。

$ git status -sb 打印的两个字母代码,如_M,让你知道三个快照的区别。如果两个字母的代码是XY,那么:

  • X让您了解第三个快照(HEAD)和第二个快照(暂存区)之间的差异
  • Y让您了解第二个快照(暂存区)和第一个快照(工作目录)之间的区别

让我们用$ git add lorem.txt暂存lorem.txt文件。$ git status -s 的输出如下:

M_ lorem.txt

代码是M_,这次存储在三个快照中的lorem.txt文件包含:

  • 第一个快照(工作目录):该文件包含foo
  • 第二个快照(暂存区):该文件包含foo
  • 第三张快照(HEAD):该文件包含lorem

因此M_意味着工作目录中的文件与暂存区中的文件相同;存储在暂存区中的文件不同于存储在HEAD中的文件。

如果您使用$ git commit提交此修改,那么所有三个快照将再次同步。它们都包含带有foo字符串的文件lorem.txt

一旦你掌握了三个快照背后的思想,就很容易理解$ git reset命令的工作方式。该命令更改三个快照:头、登台区和工作目录。它有三个影响其行为的重要选项--soft--mixed--hard、。它们的含义总结在表 8-1 中。

表 8-1 。$ git reset 命令的选项- soft、- mixed、- hard

image

表 8-1 告诉我们--soft选项只影响HEAD--mixed选项影响HEAD和中转区。第三个选项(--hard)影响所有三个快照:HEAD、暂存区和工作目录。

我们已经非常熟悉的操作是:

$ git reset --hard [REVISION]

该操作的内部可以描述为:

  • HEAD的修改:更新HEAD使其指向[REVISION]
  • 暂存区的修改:获取快照,使其现在由HEAD指向,并将其存储在暂存区中
  • 工作目录的修改:取HEAD现在指向的快照,签入工作目录

在该命令之后,所有三个快照完全相同。您当前的版本是[REVISION]。注意:这个命令会修改工作目录。正如您已经知道的,您将丢失未提交的更改!

第二个选项,--mixed是默认的。因此,以下两个命令是相同的:

$ git reset --mixed [REVISION]
$ git reset [REVISION]

您可以将$ git reset --mixed操作视为暂存和提交的逆操作。以下是根据三个快照描述的内部情况:

  • HEAD的修改:更新HEAD使其指向[REVISION]
  • staging area 的修改:取HEAD现在指向的快照,存储在 staging area 中
  • 不要触摸工作区域

现在很容易分析这个菜谱中的命令:$ git reset HEAD∼。该命令相当于$ git reset --mixed HEAD∼。它执行两种操作:

  • 设置HEAD,使其指向父版本
  • 它获取快照,使其现在由HEAD指向,并将其存储在暂存区中。

请注意,工作目录没有改变。您的所有修改(它们已经被提交)都将保留在那里。结果与您暂存和提交变更之前完全一样。三个文件b.txtc.txtd.txt现在显示为未暂存。

第三个选项--soft,只移动存储在HEAD中的指针。它不会修改临时区域或工作目录。如果您试图在这个菜谱中使用$ git reset --soft HEAD∼,那么$ git status -sb返回的状态将是:

A_ b.txt
A_ c.txt
A_ d.txt

文件是暂存的。如果您想创建一个只存储一个文件的修订版,您必须卸载一些文件。要将c.txt文件从A_更改为_A,您可以使用$ git rm --cached c.txt命令。执行$ git reset HEAD∼的另一种方法是使用两个命令:

$ git reset --soft HEAD∼
$ git rm --cached [b-d].txt

尽管这个解决方案更糟糕,我还是鼓励你去尝试一下。使用$ git add$ git commit你可以在两个不同的步骤中存放和提交文件。命令$ git reset --soft$ git rm --cached执行相反的操作:取消提交和取消登台。

在这个配方中,我们在一次提交中执行了一个撤销操作。记住,用同样的方式,你可以用$ git reset HEAD∼5撤销任意数量的提交。

8-5.重新排序修订

问题

您的存储库包含许多修订。最后三个版本被标记为bcd。它们在历史中按以下顺序出现:d是最近的版本,cd之前创建,bc之前。您需要对修订版bcd重新排序,以符合图 8-5 。

9781430261032_Fig08-05.jpg

图 8-5 。重新排序修订

解决办法

创建一个包含修订版本abcd的新存储库:

$ cd git-recipes

$ git init 08-05

$ cd 08-05

$ git simple-commit a b c d

命令$ git log --oneline现在返回以下输出:

cc595c7    d
7bb0fe3    c
b040c68    b
9dfe77d    a

修订顺序为abcd。最老的是a,最新的是d

现在用$ git rebase -i HEAD∼3进行交互式重置基础。在这个命令之后,git 将用清单 8-5 中显示的内容启动 vim。将清单 8-5 中显示的内容替换为清单 8-6 中显示的代码。变化非常小:提交被重新排序。然后保存文件并关闭编辑器。

清单 8-5。$ git rebase 后 vim 的原始内容-I HEAD∽3(提交顺序为 b,c,d)

pick b040c68    b
pick 7bb0fe3    c
pick cc595c7    d

清单 8-6。 交互式改基时你应该在编辑器中输入的内容(提交顺序为 d、b、c)

p   cc595c7    d
p   b040c68    b
p   7bb0fe3    c

当您保存并关闭编辑器时,git 将执行重置。该操作完成后,使用$ git log命令检查您的修订顺序。$ git log -oneline的输出应该如下:

7bb0fe3    c
b040c68    b
cc595c7    d
9dfe77d    a

这些提交现在按cbda排序(从最新的到最早的)。

它是如何工作的

交互式重置基础的子命令pick可以缩写为p。通过更改编辑器中子命令的顺序,可以修改应用修补程序的顺序。重置基础根据补丁在编辑器窗口中的顺序应用补丁。清单 8-6 中的第一个子命令,即p-cc595c7-d,定义了要应用的第一个补丁。因此,在生成的历史中,修订版d将出现在a之后。

原始修订保留在数据库中,可以通过引用日志引用进行访问。

8-6.删除几个版本

问题

您的存储库包含几个修订版。最后五次修订被标记为bf。您想要删除修订版bdf。图 8-6 中描述了您想要实现的转变。

9781430261032_Fig08-06.jpg

图 8-6 。删除修订

解决办法

创建一个包含修订版af的新存储库:

$ cd git-recipes

$ git init 08-06

$ cd 08-06

$ git simple-commit a b c d e f

$ git log --oneline命令现在按以下顺序打印修订:

35cba5a     f
4932572     e
bb7f037     d
93b7397     c
9219566     b
17e5231     a

您希望从历史记录中删除一些提交。您要删除的最早提交是b。这是历史上第五次犯罪(f第一、e第二、d第三、c第四、b第五)。你需要的命令是$ git rebase -i HEAD∼5。这是一个交互式的重置基础,所以 git 将启动编辑器。交互式重设基础的原始子命令如清单 8-7 所示。用清单 8-8 中所示的子命令替换它们。最后保存文件并关闭编辑器。

清单 8-7。 配方 8-6 中交互重置的原始子命令

pick 9219566 b
pick 93b7397 c
pick bb7f037 d
pick 4932572 e
pick 35cba5a f

清单 8-8。 执行图 8-6 中所示转换的命令

pick 93b7397 c
pick 4932572 e

当您完成重置$ git log --oneline时,应打印:

4932572     e
93b7397     c
17e5231     a

它是如何工作的

如果从编辑器中删除一个子命令选择,那么相应的修订将不会出现在历史中。

8-7.编辑旧版本

问题

您的存储库包含许多修订。历史上的第三次修订被标记为x,它引入了一个新文件x.txt。现在你想重新编辑这个版本:它应该引入两个新文件x.txty.txt。你还想在提交x之后引入一个新的版本z。其他修订应保持不变。图 8-7 中显示了您想要实现的转换。

9781430261032_Fig08-07.jpg

图 8-7 。编辑旧版本

解决办法

创建一个包含修订版本abxcd的新存储库:

$ cd git-recipes

$ git init 08-07

$ cd 08-07

$ git simple-commit a b x c d

并与$ git rebase -i HEAD∼3进行交互式重置基础。交互式重设基础的原始子命令如清单 8-9 所示。用清单 8-10 中的子命令替换它们。你要把修改x的命令从pick改成edit。然后保存文件并关闭编辑器。

清单 8-9。 配方 8-7 中交互重置的原始子命令

pick 9aa7b18 x
pick 2455e82 c
pick f8bf7b5 d

清单 8-10。 执行图 8-7 中所示转换的命令

edit 9aa7b18 x
pick 2455e82 c
pick f8bf7b5 d

重置基础过程在提交x时停止。您现在可以使用以下命令调整x提交:

$ echo y > y.txt

$ git add y.txt

$ git commit --amend --no-edit

x提交被调整时,用$ git simple-commit z创建一个新的修订z。最后,用$ git rebase --continue完成重置。

它是如何工作的

交互式重置基础是作为一个迭代来实现的,该迭代循环通过清单 8-10 中所示的命令。这种迭代是在分离的磁头状态下进行的。当你关闭包含清单 8-10 所示子命令的编辑器时,git 进入分离状态并执行迭代。

清单 8-10 中的第一个子命令是edit 9aa7b18 x。该命令首先应用由9aa7b18标识的x版本定义的补丁,然后暂停重设基础。补丁x之后,你就处于分离的头部状态。如果你想验证,这运行命令$ git status -sb。您将看到以下输出:

## HEAD (no branch)

证明你现在是在超脱的头部状态下工作。bash 命令提示符:

gajdaw@GAJDAW /c/git-recipes/08-07 (master|REBASE-i 1/3)

打印您正在使用三个修补程序执行重置基础操作以及应用了第一个修补程序的信息。

如你所知,git 允许你用$ git add$ git commit这样的命令在分离的头状态下工作。因此,您可以用$ echo y > y.txt创建一个新文件,用$ git add y.txt暂存它,最后用$ git commit --amend --no-edit修改当前提交。这就是x提交被修改的方式。如果您跳过--no-edit选项,那么 git 将启动编辑器,您将有机会修改修订版x的注释。

一旦您完成了x提交,您就可以继续创建版本z。完成后,你用$ git rebase --continue命令完成秘籍。

值得注意的是,当交互式重置基础暂停时,您可以用其他方法修改历史。您可以使用$ git commit插入额外的提交,或者使用$ git reset删除一些提交。但是,在完成第一个基础之前,您不能执行另一个交互式基础重建。

您可以使用$ git rebase --abort中止暂停的重置基础。若要撤消该操作,请使用 reflog。

8-8.恢复版本

问题

您的存储库包含任意数量的修订。其中一个提交在您的项目中引入了一个 bug。您希望以这样一种方式撤销由该提交引入的更改,即项目的历史直到当前头都保持不变。

你想要实现的转变在图 8-8 中给出。应恢复标有b的版本。修订版c之前的历史必须保持不变。该操作将通过创建一个标记为Revert "b"的附加版本来实现。这个新的提交恢复了由b引入的更改。

9781430261032_Fig08-08.jpg

图 8-8 。恢复修订

解决办法

创建一个包含修订版本abc的新存储库:

$ cd git-recipes

$ git init 08-08

$ cd 08-08

$ git simple-commit a b c

然后执行$ git revert --no-edit HEAD∼ 命令。

它是如何工作的

命令$ git revert [REVISION]创建一个新的修订版本,恢复由[REVISION]引入的更改。附加参数--no-edit将新版本的注释设置为Revert "..."。这是撤销已包含在项目公共历史中的修订的唯一方法。

8-9.正在恢复合并提交修订

问题

您在项目中使用两个分支:masterfeature。分支出现分歧,你决定将feature分支合并为master分支。当你完成合并后,feature分支被删除。

过了一段时间,你意识到feature branch 引入了许多 bug 和严重的问题。因此,您希望将分支feature的合并恢复为master分支。

您想要实现的转换如图 8-9 所示。

9781430261032_Fig08-09.jpg

图 8-9 。恢复合并提交

解决办法

使用以下命令创建如图 8-9(a) 中所示的存储库:

$ cd git-recipes

$ git init 08-09

$ cd 08-09

$ git simple-commit m1 m2 m3

$ git checkout -b feature

$ git simple-commit f1 f2 f3

$ git checkout master

$ git simple-commit m4 m5

$ git merge feature

$ git branch -d feature

$ git simple-commit m6

$ git log --oneline --graph命令验证你的库的历史看起来像图 8-9(a) 中的。也可以用$ ls列出工作目录的内容。工作目录现在包含九个文件:f1.txtf3.txtm1.txtm6.txt

当存储库准备就绪时,您可以使用以下命令恢复合并提交:

$ git revert --no-edit -m 1 HEAD∼

该命令将历史向前移动。存储库现在将包含一个带有注释Revert "Merge branch 'feature'"的新版本。这个修订删除了您在feature分支中创建的提交中引入的所有变更。工作目录现在只包含六个文件m1.txtm6.txt。文件f1.txtf2.txtf3.txt都没了。您可以使用$ ls命令来验证它。

它是如何工作的

合并提交有两个或多个父提交。如果您恢复合并提交,您必须指出应该恢复历史的哪一部分。在图 8-9 中标为Merge branchfeature的提交有两个父级:

  • 第一个是提交m5
  • 第二个父节点是提交节点f3

恢复Merge branch ' feature'提交 可以产生由修订组成的快照:

m1, m2, m3, m4, m5, m6

或由修订组成的快照:

m1, m2, m3, f1, f2, f3, m6

决定权在你。您通过传递给$ git revert命令的附加参数-m做出决定。如果你想把历史保存在合并提交的第一个父项下,那么使用-m 1参数,比如:

$ git revert --no-edit -m 1 HEAD∼

上述命令将产生由m1m2m3m4m5m6修订组成的快照。这种情况如图 8-10 中的所示。

9781430261032_Fig08-10.jpg

图 8-10 。用$ git revert - no-edit -m 1 HEAD 获得的快照

如果你想把历史保存在合并提交的第二个父项下,那么使用-m 2参数,比如:

$ git revert --no-edit -m 2 HEAD∼

该命令产生由修订版 m1m2m3f1f2f3m6组成的快照。这种情况如图 8-11 所示。

9781430261032_Fig08-11.jpg

图 8-11 。用$ git revert - no-edit -m 2 HEAD 获得的快照

如果您忘记指出想要保留的分支,git 将拒绝恢复合并提交。该命令将无法生成以下消息:

error: Commit XXXXXX is a merge but no -m option was given.
fatal: revert failed

8-10.摘樱桃修订版

问题

您想要将修订从一个分支复制到另一个分支。图 8-12 中的展示了您心目中的转变。您的master分支包含一个标记为m4的修订。你要把它复制到feature分公司。

9781430261032_Fig08-12.jpg

图 8-12 。精心挑选的修订

解决办法

从配方 6-4 克隆存储库:

$ cd git-recipes

$ git clone-with-branches 06-04 08-10

$ cd 08-10

然后用$ git checkout feature转到特征分支,用$ git cherry-pick master∼复制修订m4

它是如何工作的

$ git cherry-pick命令将修订版定义的补丁作为参数应用到当前分支。

8-11.挤压一根树枝

问题

您刚刚完成了一项新功能的工作。您的工作由存储在专用分支中的三个提交组成。您想要压缩这些提交,并将它们作为一个新的提交添加到您的master分支之上。

该任务在图 8-13 中给出。feature分支包含三个版本f1f2f3。你想把它们压缩成一个单独的版本,出现在master分支中。

9781430261032_Fig08-13.jpg

图 8-13 。压断树枝

解决办法

从配方 6-4 克隆存储库:

$ cd git-recipes

$ git clone-with-branches 06-04 08-11

$ cd 08-11

接下来用$ git merge --squash feature挤压特征分支。最后,用$ git commit -m "The feature branch was squashed"提交变更。

它是如何工作的

操作$ git merge --squash feature修改工作目录和储存库的暂存区,复制在feature分支中引入的变更。就在这个命令之后$ git status -sb打印出来:

A  f1.txt
A  f2.txt
A  f3.txt

这意味着:

  • 工作目录包含来自特征分支的所有变更
  • 所有的变化都已经上演了

如果你真的对这个修改满意,你可以用$ git commit命令提交它们。

8-12.重新使用恢复的分支

问题

处理项目时,您将创建一个名为 feature 的分支,其中包含许多修订。feature 分支中的所有代码看起来都是正确的,您将它合并到主分支中,形成一个灯泡。碰巧这个分支机构引起了许多问题。因此,您决定使用方法 8-9 中解释的过程来恢复合并提交。

您的项目的工作继续进行,主分支继续前进。一段时间后,您希望再次合并已恢复的特征分支。您想要实现的操作如图 8-14 所示。

9781430261032_Fig08-14.jpg

图 8-14 。配方 8-12 将储存库(a)转换成储存库(b)

解决办法

创建新的存储库:

$ cd git-recipes

$ git init 08-12

$ cd 08-12

并创建图 8-14(a) 所示的存储库。使用以下命令:

$ git simple-commit a b
$ git checkout -b foo-bar
$ git simple-commit x y
$ git checkout master
$ git merge --no-ff foo-bar
$ git simple-commit c
$ git revert -m 1 --no-edit HEAD$ git simple-commit d

使用$ ls命令你可以验证工作目录不包含文件x.txty.txt。至此foo-bar分支成功还原。

你的项目的历史向前推进了。revert 命令后创建了版本d。现在您想将foo-bar分支重新合并到主分支中。为此,请运行以下命令:

$ git format-patch foo-bar∼2..foo-bar

$ git checkout -b foo-bar-tmp

$ git am *.patch

$ rm *.patch

$ git branch -M foo-bar-tmp foo-bar

$ git checkout master

$ git merge --no-ff -m "2nd merge of 'foo-bar'" foo-bar

存储库应该看起来像图 8-14(b) 。您可以使用$ git log --graph --oneline --all --decorate命令来验证这一点。工作目录包含文件x.txty.txt

它是如何工作的

当你的库看起来像图 8-14(a) 中的库时,命令:

$ git rebase master foo-bar

将不执行重置基础操作。它只会将 foo-bar 分支快进到 master 分支指向的修订。如果你想强制重置基准,你必须使用$ git format-patch$ git am命令手动重置。

foo-bar 分支中的修订可通过范围说明符foo-bar∼2..foo-bar获得。命令:

$ git log --oneline foo-bar∼2..foo-bar

列出了两个版本xy。为了创建这些修订版的补丁,我们使用:

$ git format-patch foo-bar∼2..foo-bar

接下来,我们创建一个新的临时分支,名为 foo-bar-tmp:

$ git checkout -b foo-bar-tmp

并在其中应用补丁:

$ git am *.patch

不再需要修补程序,因此您可以使用以下命令删除它们:

$ rm *.patch

然后,将临时分支 foo-bar-tmp 重命名为原来的名称 foo-bar:

$ git branch -M foo-bar-tmp foo-bar

当分支 foo-bar 准备就绪时,您可以进入主分支:

$ git checkout master

并再次合并 foo-bar 分支:

$ git merge --no-ff -m "2nd merge of 'foo-bar'" foo-bar

摘要

前两章介绍了合并和重定基——这两种操作使修订图的结构变得复杂。在第八章中,我们更加关注修订图,考虑了多种方法来转换其结构。我确信你会发现这里讨论的许多秘籍在你的日常工作中很有用。

这里介绍的所有配方都强调了修订的性质。让我再次提醒你:修订不会改变。一旦创建,就不能修改。你所能做的就是创建新的修订版,在某些方面与原版相似。这条规则构成了各种撤消操作的基础。如果你想在 git 中撤销某些东西,你必须在开始操作之前寻找 HEAD 指向的修订。如果你知道它的名字,那么$ git reset --hard [REVISION]会撤销操作。

本章要记住的第二件重要的事情涉及到存储库的三个方面:

  • 工作目录
  • 集结地
  • 和您当前的分支(即由 HEAD 指向的修订)

它们中的每一个都定义了项目中文件的快照。您可以使用文件系统命令修改工作目录中存储的快照,例如$ rm$ cp$ echo foo > bar等等。使用 git 命令修改暂存区中存储的快照,如$ git add$ git rm$ gim mv等。最后,可以用$ git commit命令修改 HEAD 指向的版本中存储的快照。

使用这三个快照,您可以将由$ git status -sb返回的两个字母状态代码解释为:

  • 代码的第一个字母比较了磁头快照和转移区:

  • 空格表示存储在磁头快照和转移区中的文件是相同的

  • 任何其他字符都表示存储在磁头快照中的文件不同于暂存区中的文件

  • 代码的第二个字母比较了临时区域和工作目录

  • 空格表示存储在临时区域中的文件和存储在工作目录中的文件是相同的

  • 任何其他字符都表示存储在登台区的文件不同于存储在工作目录中的文件

本章还阐明了作者身份的概念以及 git 处理日期的方式。每个提交存储四个不同的属性:AuthorCommitAuthorDateCommitDate. AuthorCommit保存在提交中引入的代码作者的身份(Author属性),以及在项目历史中引入提交的人的身份(Commit属性)。如你所知,当你执行$ git commit命令时Author被设置(没有--amend参数)。当您使用$ git commit --amend$ git cherry-pick$ git rebase修改提交时,git 只改变提交者的用户名——作者身份保持不变。请注意,当您压缩一些提交时,没有办法保留原始作者。被挤压的提交将归因于第一个提交的作者。

其他属性AuthorDateCommitDate是时间戳。第一个存储创作提交时的信息,第二个存储历史中引入提交时的信息。Git 不关心或存储任何其他日期。特别是,git 操作不受文件系统中存储的修改日期的影响。如果您:

  • 下午 5 点提交
  • 在下午 5:10 创建、编辑并保存文件a.txt
  • 在下午 5:20 创建、编辑并保存文件b.txt
  • 下午 5:30 提交

then your repository will contain two commits. The first will contain the timestamp 5:00 p.m. The second revision will be denoted as created at 5:30. There is nothing in between. The information that your files were modified at 5:10 and at 5:20 is lost—git doesn’t track it.

介绍的秘籍中有四个使用了交互式的重新基础。你应该非常仔细地分析它们,尤其是配方 8-7。即使你不打算用它的方式提出。在冲突的情况下,重定基础将被暂停——这就是配方 8-7 特别重要的原因。一旦你知道如何用配方 8-7 编辑旧版本,你将更容易解决冲突。

image 注意请注意,本章中的配方在用于修改已与他人共享的修订时会导致严重的问题。您只能使用配方 8-8 和 8-9 中的$ git revert命令来修改已发布的修订。所有其他配方只能用于没有发送到共享存储库的修订。记住:你的存储库的公共历史只能向前发展。否则,在团队内部同步工作将会非常困难和麻烦。