Git 多个合并分支 - 如何在合并到 master 分支时避免多次提交
Git multiple merge branches- how to avoid multiple commit while merging to master branch
我的 Git 存储库如下所示:
我创建了 2 个分支 - Branch_1 和 Branch_2。现在我终于准备好将这个 Branch_2 合并到 Master 分支中了。但是当我合并时,它显示了 Branch_1 和 Branch_2 的所有提交,因为它们之间有多个合并。在将我的代码合并到 master 分支之前,任何人都可以建议如何在这种情况下进行一次提交吗?
git log --oneline --graph --color --all --decorate
* 36dbb26 (origin/Branch_2) changed abc
* 1a7bf25 changed T
* 110095a changed Z
* 1087d5d Merge remote-tracking branch 'origin/Branch_1' into Branch_2
|\
| * 8c9d02a (origin/Branch_1) sleep added between each processing to discover partitions
| * ca401cb changed S
| * 20a4edd changed R
* 3f472ef install package
* 1087d5d Merge remote-tracking branch 'origin/Branch_1' into Branch_2
|\
| * 8c9d02a (origin/Branch_1) adding y
| * ca401cb changed g
| * 97c326d changed f
* | fd543bf changed c
* | 7b24330 (HEAD -> master, origin/master, origin/HEAD) fix D
* | 53aecb4 adding x
|/
* 49d3bda changed e
| * 213ea18 (origin/Feature_branch) changed d
| * 0b3b675 changed c
|/
* df6ac90 Adding c
* 96699ff Adding b
* 99f165f Adding a
我想要如下所示的最终结果:(将来自 fd543bf 的所有提交合并为 1 个提交)
* 36dbb26 (HEAD -> master, origin/Branch_2) changed R-All consolidated
* 7b24330 (origin/master) Fix D
TL;DR
您可能只想要 git log --first-parent
。
长
... But when I did merge it showed all the commits for Branch_1 & Branch_2 because of multiple merge in between.
不,这不是原因。您看到所有这些提交的原因是因为您实际上拥有所有这些提交。
这里要理解的是,最终,Git 都是关于提交的。 Commit是Git.1中的存储单位 Commit是你有的,你想要的。如果你不想要 这些 提交,你想要的必须是一些 other 提交。提交就是您所得到的,所以您 更好 想要提交。 (如果你想要别的东西,不要使用 Git。但许多其他版本控制系统也是 commit-oriented,所以你可能会发现你仍然会得到提交,所以你不妨坚持Git,除非……嗯,读下一段。)
B运行ch 名称,在 Git 中存在一个主要原因:find 提交。这是 Git 与其他版本控制系统不同的地方。在许多版本控制系统中,b运行ch 是提交的容器,您可以通过检查 b运行ches 来检查提交:b运行ch 中包含的提交集合是如果这是你的要求,你会看到一组提交。但这不是 b运行ch 名称在 Git.
中的工作方式
在 Git 中,一个提交可以——而且经常是——在很多,甚至 all,b运行ches 同时。那是因为 Git 的 b运行ch 名称不是容器。他们不 hold 提交。他们只是让你找到提交。每个名称找到 一个 提交。 提交本身 找到提交的 rest。
每个 Git 提交都由两部分组成,稍后我们将对此进行描述。每个提交都通过其唯一的哈希 ID 找到。每个提交都有这些哈希 ID 之一;该哈希 ID 可以说是提交的“真名”。没有哈希 ID,Git 根本找不到提交。2 所以一个 b运行ch 名称包含一个哈希 ID,根据定义, last 包含在 b运行ch 中的提交。该提交又持有一组哈希 ID——通常只是一个——较早的提交,它们是也是 b运行ch[=490= 的一部分].
当我们有一个 b运行ch 名称,如 main
或 feature
,它包含一些哈希 ID,我们说 b运行ch 名称 指向最后一个,或提示,提交b运行ch:
<-H <--feature
但是提交 H
——这里的 H
代表真正的哈希 ID,不管它是什么——具有某个早期提交 G
的哈希 ID。所以我们说 H
指向 G
:
<-G <-H <--feature
但是提交 G
也向后指向 still-earlier 提交:
... <-F <-G <-H <-- feature
等等,一直回到有史以来的第一次提交。从字面上看,这个 不能 向后指向较早的提交,所以它不会,这就是 Git 停止向后工作的地方。
所以,这就是提交 在 b运行ch 上的意思: 我们从 b运行ch 名称开始,它自动确定那个 b运行ch 上的 last 提交,然后向后工作。但如果是这样的话……好吧,假设我们有这样的事情,其中提交 I
指向回 H
,并且提交 K
也指向 H
:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
哪个 b运行ch 持有提交 H
?
Git 的回答是提交 H
现在在 both b运行ches 同时time. 所有较早的提交也是如此。此外,即使 H
是某些 third b运行ch:
上的 last 提交
I--J <-- br1
/
...--G--H <-- main
\
K--L <-- br2
依旧如此。提交 H
现在在 所有三个 b运行ches.
因此,在 Git 中,包含一些提交 的 组 b运行 是动态和流动的。重要的不是 b运行ch 名称,而是从提交到提交的连接。 b运行ch 名称很有用,但只是为了让您入门。其他一切都是关于提交。
1因为提交是由更小的部分组成的,所以可以在较低的级别上工作。但这大致类似于将盐等分子分解成原子——金属钠和氯——甚至是质子、中子和电子等亚原子粒子。一旦你像这样分解它们,它们就不再 有用 了,不像盐那样有用。您不能用金属钠或氯调味食物,尤其不能用中子调味。
2有一些维护命令——特别是git fsck
和git gc
——简单地查看每个 在存储库中提交并找出哪些连接到其他连接等等。这是非常slw,所以这不是你在day-to-day操作中使用Git的方式。在像 Linux 内核这样更大的存储库中,git checkout
或 git log
有时会花费几秒钟,但 git fsck
或 git gc
可能需要很多时间分钟。其中一些取决于您的计算机及其文件系统等的速度,但对比非常明显:通过哈希 ID 查找提交是 fast,但通过任何其他方式查找它通常非常慢。
提交的两个部分是快照和元数据
我们上面提到每个提交都有两个部分。它们是:
主要数据,一张快照。在这里,Git 永久保存3 每个文件的名称和内容的 read-only 快照,截至您或任何人提交时。这允许您——或其他任何人——取回该快照的所有这些文件。
元数据。这里,Git 保存提交人的姓名和电子邮件地址。 Git 为 当 他们提交时保存 date-and-time-stamp。 (Git 实际上每个提交有两个 name-and-address-and-time 字段,在这里,尽管大多数人通常只看一个。)Git 允许您添加描述 - 日志消息—解释 为什么 你做出这个提交,如果你愿意的话。而且,Git 本身的键,这也是 Git 存储那些 earlier-commit 哈希 ID 的地方。 Git 保留此类哈希 ID 的列表。大多数提交只有一个条目,它告诉 Git 提交的 parent 是什么。
元数据中的 parent 让 Git 向您显示提交——这是一个快照,而不是一组更改——作为一组变化。如果我们连续两次提交:
... <-F <-G ...
然后我们从F
(parent)和G
(child)中取出快照和比较它们,相同的不改变,不相同的...好吧,比较它们会告诉你什么改变了。这就是 Git 显示的内容: 更改 。但是要获得这些更改,Git 需要 两次 提交,以获得两个快照。
3虽然任何提交的任何部分都不能更改,但并非所有提交都必须永远持续,所以说for all time 是言过其实了。给定提交的哈希 ID,如果 Git 可以找到该提交,则该提交就是 那个提交。这不是任何其他提交。它必须是您上次查看时具有该哈希 ID 的提交。换句话说,提交仍然存在,所以它没有改变,它的文件仍然是原来的样子。
但是,您可以Git 删除 提交。这并不容易:Git 被构建为 添加新提交 同时保留现有提交,并且您使用的大多数日常命令都是这样工作的。但是你可以通过一些努力,使一些提交很难找到。一旦你这样做,并让它们 un-find-able(维护命令除外)足够长的时间,Git 最终会确定它们一定是不需要的垃圾,并将它们真正扔掉。 git gc
维护命令专门执行此操作。一旦发生这种情况,如果您已将哈希 ID 保存在其他地方(例如将其写在白板上)并正确输入,Git 会说 我没有该 ID 的任何内容.
因为 Git 是为 add 提交而构建的,当两个 Git 连接并拥有 Git-sex 时,接收 Git 通常非常愿意添加 all 发送 Git 的新提交给自己,新提交像病毒一样传播。因此,仅仅因为您添加了但随后撤回了提交,并不意味着它没有传给其他人 Git。稍后可能会回复您:
不要害怕临时提交,但是做记住,如果你让其他Git与你的[=691交谈=],他们可能会 复制 你的临时提交,并稍后将它们呈现给你——所以要么小心你让你的仓库有哪些仓库 Git-sex,要么小心关于让敏感数据进入您的临时提交,或两者兼而有之。
还请注意,当您使用 git push
时,您 选择您的 Git 发送给其他 Git,所以 git push
对你来说 更安全 ——你可以选择发送哪些提交,包括临时提交——而不是让所有用户都可以读取你的存储库(并且因此阅读你所有的临时提交)。
接收Gits,当然要小心了。这就是为什么像 GitHub 这样的托管网站提供访问控制(这不是直接内置到 Git 本身,而是 add-on)。
合并是一个以上的提交[=756=
当我们有分歧的工作时,例如:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
我们可能想要合并这两个不同的工作线。这样,我们就可以获得一个提交,其中添加了某人在 br1
中添加的功能和 某人在 br2
中添加的功能。这就是 git merge
的意思。
现在,git merge
,作为一个命令,并不总是进行 合并提交。我们需要仔细区分动词形式 to merge,意思是 to combine work,以及名词或形容词形式 合并或合并提交,意思是由于执行work-combining:
而产生的提交
合并的动词形式是git merge
通常(或至少经常)使用的形式。
名词形式 a merge 或其形容词等价物 a merge commit 是什么 Git 通常(或至少经常)在完成 合并 工作后生成。
所以你可以看出这些是密切相关的,但不是一回事。一个是过程;另一个是结果。
我们不会详细介绍该过程的工作原理,但是当合并的结果是 合并提交 时,合并提交就像任何其他提交一样,除了没有 单个 parent,它有 两个或更多 。 (大多数合并提交恰好有两个 parent;我将在后面的部分中介绍 或更多 部分。)请记住,所有提交都有两个部分:快照,和 list-of-parents。 合并提交的特别之处在于它的列表有两个或更多parents。
现在,任何新提交的 第一个 parent 就是您开始的提交。你运行:
git checkout br1
然后你做一些事情来进行新的提交,最终,你 运行 git commit
。 Git 构建一个 new 提交,具有新的唯一哈希 ID,作者:
- 保存每个文件的当前形式的快照;4
- 收集元数据:您的姓名、电子邮件地址、当前 date-and-time、您的日志消息等;
- 将这些全部写出来,使用当前提交的哈希ID作为新提交的parent;最后
- 将新提交的哈希 ID 写入 当前 b运行ch 名称。
这可能就是您获得提交 J
的方式,例如:您 运行 git checkout br1
,提取了提交 I
。然后您使用 git commit
进行了新的提交。新提交的 parent 是提交 I
,因此 J
指向 I
,现在 name br1
选择提交 J
而不是选择提交 I
.
然而,当您使用 git merge
进行新提交时,5 Git 不会写出 single-parent 提交并推进 b运行ch 名称。这次,Git 写出 multi-parent 提交。新提交的 parent 列表中的 first parent 与往常相同,但至少有一个额外的 parent 进入列表。
额外的 parent,在这种情况下,是您在 运行 git merge
:
时选择的提交
git checkout br1
git merge br2
这导致 Git 使用提交 L
作为另一个提交。所以,在合并两个 b运行ches 的工作并得到一个合适的快照之后,Git 现在使新的 merge commit M
像这个:
I--J
/ \₁
...--G--H M <-- br1 (HEAD)
\ /²
K--L <-- br2
这里的(HEAD)
表示我们“在”b运行chbr1
,所以新提交M
就是b[=的新提示782=]ch br1
。 Commit M
有 two parents 而不是通常的:第一个 parent 是 commit J
,其中 b运行ch br1
刚才点过。 第二个 parent 是提交 L
。 b运行ch name br2
没有变,所以还是指向commit L
.
因为 M
指向 L
以及 J
,提交 K-L
现在在 b运行ch br1
. 这就是您的 git log
显示它们的原因:它们存在并且在 b运行ch 上。 Git 通过提交 M
找到 它们,然后返回到 both 提交 J
andL
,从这两个,到两个提交I
andK
,从这两个,提交 H
。 (当然,Git 必须小心访问提交 H
一次,即使现在有两种方法可以到达那里。但这对 Git 来说很容易做到。)
4快照是根据 Git 的 index 中的文件副本制作的,而不是来自您可以查看和使用的文件。这就是为什么 Git 让你 运行 git add
如此频繁。
5如果合并有合并冲突,to-merge进程会中途停止让你修复te 冲突。最终的 git commit
或 git merge --continue
将完成合并并进行合并提交。为了实现这一点,在中间停止之前,git merge
在冲突的合并 状态中写出这个特殊的 。 git commit
命令检查此状态并完成合并,而不是进行普通的 single-parent 提交。
章鱼合并
由于您在某种程度上抱怨必须进行多次合并提交才能合并多个 b运行ch,因此是时候提及 Git 的 章鱼合并。假设我们有一个“主线 b运行ch”和两个或多个 spring 来自它的特性,可能来自单个起点提交,也可能来自多个起点:
o--o--o <-- feature1
/
...--o--o--o <-- main (HEAD)
\
o--o <-- feature2
我们可以一次合并两个特征 b运行ches:
o--o--o <-- feature1
/ \
...--o--o---o--M <-- main (HEAD)
\
o--o <-- feature2
然后:
o--o--o <-- feature1
/ \
...--o--o---o--M--N <-- main (HEAD)
\ /
o-----o <-- feature2
这个方法没有问题。它工作正常。 主线 b运行ch,main
现在有两个 two-parent 合并提交 M
和 N
。 N
的第一个parent是M
; M
的第一个 parent 是直接在主线上左侧的提交。 N
的 second parent 显示了 feature2
是如何合并的,second parent M
显示了 feature1
是如何合并的。
Git 提供了能力——在某些情况下,因为在进行这种合并时,没有很好的方法来解决合并冲突,所以章鱼合并必须是 conflict-free——使用单个合并提交以获得此结果:
o--o--o <-- feature1
/ \
...--o--o--o---M <-- main (HEAD)
\ /
o--o <-- feature2
Commit M
这里有 三个 parent 而不是只有两个。第一个 parent 像往常一样在它的正后方左侧。第二个和第三个 parent 是来自 feature1
和 feature2
.
的剩余两个 branch-tip 提交
我们通过 运行ning 得到这个:
git checkout main
git merge feature1 feature2
我们命名两个提交的事实使得 git merge
使用 -s octopus
合并策略 ,它试图合并所有这些提交(使用章鱼样式merge base algorithm)并且只有在没有冲突的情况下才进行合并。这意味着有些合并你可以用两个常规two-parent合并你不能用three-parent章鱼做;但有些人喜欢章鱼合并,因为它们一次将所有特征结合在一起,和表明没有冲突(嗯,可能)。6
请注意,章鱼合并 still 导致将 all 提交放在 merged-into b运行 ch(在本例中为 main
)。 Git 简单地跟随 all parents 的合并,当你 运行 git log
时,你会看到 所有属于 b运行ch.
的提交
6因为Git是一套工具,而不是一个完整的解决方案,所以可以构建一个实际上不使用[=57的章鱼合并=] ,或者经历了两次常规合并。但是不要那样做。我们甚至不会看如何你可以做到这一点。
查看更少的提交
git log
遍历提交,一次一个,从提交向后移动到他们的 parent。每当遇到合并提交时,它都可以选择向后移动到哪个提交。但它不会 坚持 向您显示每个提交,甚至 移动到 以这种方式可以到达的每个提交。它只是 默认 显示每个提交。
您可以限制您看到哪些提交,并且您可以限制哪些提交git log
将首先访问 .如果你限制访问的提交集,你会自动限制看到的提交,所以这是非常强大的。我们不会在这里查看所有血淋淋的细节,而只会查看一个非常有用且重要的选项:--first-parent
.
当我们使用--first-parent
时,我们告诉Git:每当你到达合并提交时,假装这个合并提交只有一个parent,即,它的第一个 parent. 换句话说, 完全忽略 merged-in 提交,甚至不走那些路。7 如果我们有:
I--J
/ \₁
...--G--H M--N--O--P <-- main (HEAD)
\ /²
K--L
在点 M
处发生了一些合并,我们 运行 git log
,我们将看到提交 P
、O
、N
、M
、J
、L
、K
、I
、H
等(M
和 H
按某种顺序发生)。8 但是如果我们 运行:
git log --first-parent
walk 会假装提交 M
只有一个 parent、J
,我们将访问提交 P
、O
、N
、M
、J
、I
、H
,依此类推。我们甚至从未 看 提交 K-L
,所以我们从未看到它们。
7请注意,就像岔路口以后会重新汇合一样,如果你改变方向——沿着这条路从你原来的目的地回到你原来的站要点——是,join现在是fork,以前的fork现在是join。因此,由于 Git 向后工作,merges 实际上是 b运行ch 和 b运行ch points 是事情走到一起的地方。这真的完全取决于你如何看待它。
8当合并在图形遍历中提供 git log
分叉时,提交的实际顺序来自您提供的排序选项。 默认 排序是首先显示最高提交日期。如果在进行所有提交时所有计算机时钟都是准确的,这将以正确的顺序显示提交,但有时一台计算机的时钟已关闭,并且提交可能会奇怪地混合在一起。在困难的情况下,考虑使用 git log --graph
来帮助查看实际的提交图结构。
其他选项
正如我在这个答案的顶部提到的,如果你不想要 这些 提交,你必须想要 一些其他 提交.当我说 这些提交 时,我说的都是一般性的——Git 存储提交,所以这就是你得到的全部——但也是具体的。如果您不想 merge 提交,请不要首先 make 合并提交。 (“不开始 none,不会 none”,正如他们所说。)
现在,这有一些巨大的缺点。如果您不进行 merge 提交,您将无法保留您所做的实际原始工作。不过你确实有这个选择。例如,当您 运行 git merge
时,您可以使用 git merge --squash
。这告诉 Git 通过合并 过程 ,但是要进行普通的 non-merge 单个 parent 提交 最后。 (它也无缘无故地打开 --no-commit
。9)
如果您确实使用此方法,请记住删除在合并操作之前找到提交的b运行ch名称 因为这些提交现在与执行它们的(单个)squash-merge 是多余的。如果您允许这些提交稍后重新出现在视图中,它们很可能会造成麻烦。在许多方面,这与让临时或不正确的提交逃逸到其他一些 Git 存储库的那种病毒效应是相同的问题:Git 被构建为 add提交,而不是丢弃它们。但是通过做一个不留合并痕迹的squash-merge,你在以后给自己设了一个陷阱,除非那些now-unwanted提交真的永远消失了。
如果您有多个合并要执行,并且每个合并都有一些冲突需要解决,您可以像正常 (non-squash) 合并或挤压合并那样执行它们。结果将是多次提交:多次合并提交,或多次普通 single-parent 提交。您可以在执行其中任一操作后,然后使用 git reset --soft
使新的 merge-or-not-merge 提交 难以找到 ,然后使用普通的 git commit
制作一个新的、单一的、普通的提交,它与最终合并具有相同的 snapshot。与 git merge --squash
一样,您现在通常应该认为合并的 b运行ches“已死”,您应该摆脱这些提交并假装它们从未存在过,希望它们永远不会回来困扰您。
这样做并没有错,但需要了解自己在做什么。明白后果才去做。
9隐含的 -n
几乎可以肯定只是原始 shell 脚本实现的遗留物,在 [=691= 中一直小心保存]的行为。这很烦人,因为如果您想要 这种行为,您可以 使用git merge -n --squash
。不过现在这是多余的。
在一次提交中压缩所有内容:调用 git reset --soft
然后调用 git commit
:
# from Branch_2 :
git reset --soft master
git commit
我的 Git 存储库如下所示:
我创建了 2 个分支 - Branch_1 和 Branch_2。现在我终于准备好将这个 Branch_2 合并到 Master 分支中了。但是当我合并时,它显示了 Branch_1 和 Branch_2 的所有提交,因为它们之间有多个合并。在将我的代码合并到 master 分支之前,任何人都可以建议如何在这种情况下进行一次提交吗?
git log --oneline --graph --color --all --decorate
* 36dbb26 (origin/Branch_2) changed abc
* 1a7bf25 changed T
* 110095a changed Z
* 1087d5d Merge remote-tracking branch 'origin/Branch_1' into Branch_2
|\
| * 8c9d02a (origin/Branch_1) sleep added between each processing to discover partitions
| * ca401cb changed S
| * 20a4edd changed R
* 3f472ef install package
* 1087d5d Merge remote-tracking branch 'origin/Branch_1' into Branch_2
|\
| * 8c9d02a (origin/Branch_1) adding y
| * ca401cb changed g
| * 97c326d changed f
* | fd543bf changed c
* | 7b24330 (HEAD -> master, origin/master, origin/HEAD) fix D
* | 53aecb4 adding x
|/
* 49d3bda changed e
| * 213ea18 (origin/Feature_branch) changed d
| * 0b3b675 changed c
|/
* df6ac90 Adding c
* 96699ff Adding b
* 99f165f Adding a
我想要如下所示的最终结果:(将来自 fd543bf 的所有提交合并为 1 个提交)
* 36dbb26 (HEAD -> master, origin/Branch_2) changed R-All consolidated
* 7b24330 (origin/master) Fix D
TL;DR
您可能只想要 git log --first-parent
。
长
... But when I did merge it showed all the commits for Branch_1 & Branch_2 because of multiple merge in between.
不,这不是原因。您看到所有这些提交的原因是因为您实际上拥有所有这些提交。
这里要理解的是,最终,Git 都是关于提交的。 Commit是Git.1中的存储单位 Commit是你有的,你想要的。如果你不想要 这些 提交,你想要的必须是一些 other 提交。提交就是您所得到的,所以您 更好 想要提交。 (如果你想要别的东西,不要使用 Git。但许多其他版本控制系统也是 commit-oriented,所以你可能会发现你仍然会得到提交,所以你不妨坚持Git,除非……嗯,读下一段。)
B运行ch 名称,在 Git 中存在一个主要原因:find 提交。这是 Git 与其他版本控制系统不同的地方。在许多版本控制系统中,b运行ch 是提交的容器,您可以通过检查 b运行ches 来检查提交:b运行ch 中包含的提交集合是如果这是你的要求,你会看到一组提交。但这不是 b运行ch 名称在 Git.
中的工作方式在 Git 中,一个提交可以——而且经常是——在很多,甚至 all,b运行ches 同时。那是因为 Git 的 b运行ch 名称不是容器。他们不 hold 提交。他们只是让你找到提交。每个名称找到 一个 提交。 提交本身 找到提交的 rest。
每个 Git 提交都由两部分组成,稍后我们将对此进行描述。每个提交都通过其唯一的哈希 ID 找到。每个提交都有这些哈希 ID 之一;该哈希 ID 可以说是提交的“真名”。没有哈希 ID,Git 根本找不到提交。2 所以一个 b运行ch 名称包含一个哈希 ID,根据定义, last 包含在 b运行ch 中的提交。该提交又持有一组哈希 ID——通常只是一个——较早的提交,它们是也是 b运行ch[=490= 的一部分].
当我们有一个 b运行ch 名称,如 main
或 feature
,它包含一些哈希 ID,我们说 b运行ch 名称 指向最后一个,或提示,提交b运行ch:
<-H <--feature
但是提交 H
——这里的 H
代表真正的哈希 ID,不管它是什么——具有某个早期提交 G
的哈希 ID。所以我们说 H
指向 G
:
<-G <-H <--feature
但是提交 G
也向后指向 still-earlier 提交:
... <-F <-G <-H <-- feature
等等,一直回到有史以来的第一次提交。从字面上看,这个 不能 向后指向较早的提交,所以它不会,这就是 Git 停止向后工作的地方。
所以,这就是提交 在 b运行ch 上的意思: 我们从 b运行ch 名称开始,它自动确定那个 b运行ch 上的 last 提交,然后向后工作。但如果是这样的话……好吧,假设我们有这样的事情,其中提交 I
指向回 H
,并且提交 K
也指向 H
:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
哪个 b运行ch 持有提交 H
?
Git 的回答是提交 H
现在在 both b运行ches 同时time. 所有较早的提交也是如此。此外,即使 H
是某些 third b运行ch:
I--J <-- br1
/
...--G--H <-- main
\
K--L <-- br2
依旧如此。提交 H
现在在 所有三个 b运行ches.
因此,在 Git 中,包含一些提交 的 组 b运行 是动态和流动的。重要的不是 b运行ch 名称,而是从提交到提交的连接。 b运行ch 名称很有用,但只是为了让您入门。其他一切都是关于提交。
1因为提交是由更小的部分组成的,所以可以在较低的级别上工作。但这大致类似于将盐等分子分解成原子——金属钠和氯——甚至是质子、中子和电子等亚原子粒子。一旦你像这样分解它们,它们就不再 有用 了,不像盐那样有用。您不能用金属钠或氯调味食物,尤其不能用中子调味。
2有一些维护命令——特别是git fsck
和git gc
——简单地查看每个 在存储库中提交并找出哪些连接到其他连接等等。这是非常slw,所以这不是你在day-to-day操作中使用Git的方式。在像 Linux 内核这样更大的存储库中,git checkout
或 git log
有时会花费几秒钟,但 git fsck
或 git gc
可能需要很多时间分钟。其中一些取决于您的计算机及其文件系统等的速度,但对比非常明显:通过哈希 ID 查找提交是 fast,但通过任何其他方式查找它通常非常慢。
提交的两个部分是快照和元数据
我们上面提到每个提交都有两个部分。它们是:
主要数据,一张快照。在这里,Git 永久保存3 每个文件的名称和内容的 read-only 快照,截至您或任何人提交时。这允许您——或其他任何人——取回该快照的所有这些文件。
元数据。这里,Git 保存提交人的姓名和电子邮件地址。 Git 为 当 他们提交时保存 date-and-time-stamp。 (Git 实际上每个提交有两个 name-and-address-and-time 字段,在这里,尽管大多数人通常只看一个。)Git 允许您添加描述 - 日志消息—解释 为什么 你做出这个提交,如果你愿意的话。而且,Git 本身的键,这也是 Git 存储那些 earlier-commit 哈希 ID 的地方。 Git 保留此类哈希 ID 的列表。大多数提交只有一个条目,它告诉 Git 提交的 parent 是什么。
元数据中的 parent 让 Git 向您显示提交——这是一个快照,而不是一组更改——作为一组变化。如果我们连续两次提交:
... <-F <-G ...
然后我们从F
(parent)和G
(child)中取出快照和比较它们,相同的不改变,不相同的...好吧,比较它们会告诉你什么改变了。这就是 Git 显示的内容: 更改 。但是要获得这些更改,Git 需要 两次 提交,以获得两个快照。
3虽然任何提交的任何部分都不能更改,但并非所有提交都必须永远持续,所以说for all time 是言过其实了。给定提交的哈希 ID,如果 Git 可以找到该提交,则该提交就是 那个提交。这不是任何其他提交。它必须是您上次查看时具有该哈希 ID 的提交。换句话说,提交仍然存在,所以它没有改变,它的文件仍然是原来的样子。
但是,您可以Git 删除 提交。这并不容易:Git 被构建为 添加新提交 同时保留现有提交,并且您使用的大多数日常命令都是这样工作的。但是你可以通过一些努力,使一些提交很难找到。一旦你这样做,并让它们 un-find-able(维护命令除外)足够长的时间,Git 最终会确定它们一定是不需要的垃圾,并将它们真正扔掉。 git gc
维护命令专门执行此操作。一旦发生这种情况,如果您已将哈希 ID 保存在其他地方(例如将其写在白板上)并正确输入,Git 会说 我没有该 ID 的任何内容.
因为 Git 是为 add 提交而构建的,当两个 Git 连接并拥有 Git-sex 时,接收 Git 通常非常愿意添加 all 发送 Git 的新提交给自己,新提交像病毒一样传播。因此,仅仅因为您添加了但随后撤回了提交,并不意味着它没有传给其他人 Git。稍后可能会回复您:
不要害怕临时提交,但是做记住,如果你让其他Git与你的[=691交谈=],他们可能会 复制 你的临时提交,并稍后将它们呈现给你——所以要么小心你让你的仓库有哪些仓库 Git-sex,要么小心关于让敏感数据进入您的临时提交,或两者兼而有之。
还请注意,当您使用
git push
时,您 选择您的 Git 发送给其他 Git,所以git push
对你来说 更安全 ——你可以选择发送哪些提交,包括临时提交——而不是让所有用户都可以读取你的存储库(并且因此阅读你所有的临时提交)。
接收Gits,当然要小心了。这就是为什么像 GitHub 这样的托管网站提供访问控制(这不是直接内置到 Git 本身,而是 add-on)。
合并是一个以上的提交[=756=
当我们有分歧的工作时,例如:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
我们可能想要合并这两个不同的工作线。这样,我们就可以获得一个提交,其中添加了某人在 br1
中添加的功能和 某人在 br2
中添加的功能。这就是 git merge
的意思。
现在,git merge
,作为一个命令,并不总是进行 合并提交。我们需要仔细区分动词形式 to merge,意思是 to combine work,以及名词或形容词形式 合并或合并提交,意思是由于执行work-combining:
合并的动词形式是
git merge
通常(或至少经常)使用的形式。名词形式 a merge 或其形容词等价物 a merge commit 是什么 Git 通常(或至少经常)在完成 合并 工作后生成。
所以你可以看出这些是密切相关的,但不是一回事。一个是过程;另一个是结果。
我们不会详细介绍该过程的工作原理,但是当合并的结果是 合并提交 时,合并提交就像任何其他提交一样,除了没有 单个 parent,它有 两个或更多 。 (大多数合并提交恰好有两个 parent;我将在后面的部分中介绍 或更多 部分。)请记住,所有提交都有两个部分:快照,和 list-of-parents。 合并提交的特别之处在于它的列表有两个或更多parents。
现在,任何新提交的 第一个 parent 就是您开始的提交。你运行:
git checkout br1
然后你做一些事情来进行新的提交,最终,你 运行 git commit
。 Git 构建一个 new 提交,具有新的唯一哈希 ID,作者:
- 保存每个文件的当前形式的快照;4
- 收集元数据:您的姓名、电子邮件地址、当前 date-and-time、您的日志消息等;
- 将这些全部写出来,使用当前提交的哈希ID作为新提交的parent;最后
- 将新提交的哈希 ID 写入 当前 b运行ch 名称。
这可能就是您获得提交 J
的方式,例如:您 运行 git checkout br1
,提取了提交 I
。然后您使用 git commit
进行了新的提交。新提交的 parent 是提交 I
,因此 J
指向 I
,现在 name br1
选择提交 J
而不是选择提交 I
.
然而,当您使用 git merge
进行新提交时,5 Git 不会写出 single-parent 提交并推进 b运行ch 名称。这次,Git 写出 multi-parent 提交。新提交的 parent 列表中的 first parent 与往常相同,但至少有一个额外的 parent 进入列表。
额外的 parent,在这种情况下,是您在 运行 git merge
:
git checkout br1
git merge br2
这导致 Git 使用提交 L
作为另一个提交。所以,在合并两个 b运行ches 的工作并得到一个合适的快照之后,Git 现在使新的 merge commit M
像这个:
I--J
/ \₁
...--G--H M <-- br1 (HEAD)
\ /²
K--L <-- br2
这里的(HEAD)
表示我们“在”b运行chbr1
,所以新提交M
就是b[=的新提示782=]ch br1
。 Commit M
有 two parents 而不是通常的:第一个 parent 是 commit J
,其中 b运行ch br1
刚才点过。 第二个 parent 是提交 L
。 b运行ch name br2
没有变,所以还是指向commit L
.
因为 M
指向 L
以及 J
,提交 K-L
现在在 b运行ch br1
. 这就是您的 git log
显示它们的原因:它们存在并且在 b运行ch 上。 Git 通过提交 M
找到 它们,然后返回到 both 提交 J
andL
,从这两个,到两个提交I
andK
,从这两个,提交 H
。 (当然,Git 必须小心访问提交 H
一次,即使现在有两种方法可以到达那里。但这对 Git 来说很容易做到。)
4快照是根据 Git 的 index 中的文件副本制作的,而不是来自您可以查看和使用的文件。这就是为什么 Git 让你 运行 git add
如此频繁。
5如果合并有合并冲突,to-merge进程会中途停止让你修复te 冲突。最终的 git commit
或 git merge --continue
将完成合并并进行合并提交。为了实现这一点,在中间停止之前,git merge
在冲突的合并 状态中写出这个特殊的 。 git commit
命令检查此状态并完成合并,而不是进行普通的 single-parent 提交。
章鱼合并
由于您在某种程度上抱怨必须进行多次合并提交才能合并多个 b运行ch,因此是时候提及 Git 的 章鱼合并。假设我们有一个“主线 b运行ch”和两个或多个 spring 来自它的特性,可能来自单个起点提交,也可能来自多个起点:
o--o--o <-- feature1
/
...--o--o--o <-- main (HEAD)
\
o--o <-- feature2
我们可以一次合并两个特征 b运行ches:
o--o--o <-- feature1
/ \
...--o--o---o--M <-- main (HEAD)
\
o--o <-- feature2
然后:
o--o--o <-- feature1
/ \
...--o--o---o--M--N <-- main (HEAD)
\ /
o-----o <-- feature2
这个方法没有问题。它工作正常。 主线 b运行ch,main
现在有两个 two-parent 合并提交 M
和 N
。 N
的第一个parent是M
; M
的第一个 parent 是直接在主线上左侧的提交。 N
的 second parent 显示了 feature2
是如何合并的,second parent M
显示了 feature1
是如何合并的。
Git 提供了能力——在某些情况下,因为在进行这种合并时,没有很好的方法来解决合并冲突,所以章鱼合并必须是 conflict-free——使用单个合并提交以获得此结果:
o--o--o <-- feature1
/ \
...--o--o--o---M <-- main (HEAD)
\ /
o--o <-- feature2
Commit M
这里有 三个 parent 而不是只有两个。第一个 parent 像往常一样在它的正后方左侧。第二个和第三个 parent 是来自 feature1
和 feature2
.
我们通过 运行ning 得到这个:
git checkout main
git merge feature1 feature2
我们命名两个提交的事实使得 git merge
使用 -s octopus
合并策略 ,它试图合并所有这些提交(使用章鱼样式merge base algorithm)并且只有在没有冲突的情况下才进行合并。这意味着有些合并你可以用两个常规two-parent合并你不能用three-parent章鱼做;但有些人喜欢章鱼合并,因为它们一次将所有特征结合在一起,和表明没有冲突(嗯,可能)。6
请注意,章鱼合并 still 导致将 all 提交放在 merged-into b运行 ch(在本例中为 main
)。 Git 简单地跟随 all parents 的合并,当你 运行 git log
时,你会看到 所有属于 b运行ch.
6因为Git是一套工具,而不是一个完整的解决方案,所以可以构建一个实际上不使用[=57的章鱼合并=] ,或者经历了两次常规合并。但是不要那样做。我们甚至不会看如何你可以做到这一点。
查看更少的提交
git log
遍历提交,一次一个,从提交向后移动到他们的 parent。每当遇到合并提交时,它都可以选择向后移动到哪个提交。但它不会 坚持 向您显示每个提交,甚至 移动到 以这种方式可以到达的每个提交。它只是 默认 显示每个提交。
您可以限制您看到哪些提交,并且您可以限制哪些提交git log
将首先访问 .如果你限制访问的提交集,你会自动限制看到的提交,所以这是非常强大的。我们不会在这里查看所有血淋淋的细节,而只会查看一个非常有用且重要的选项:--first-parent
.
当我们使用--first-parent
时,我们告诉Git:每当你到达合并提交时,假装这个合并提交只有一个parent,即,它的第一个 parent. 换句话说, 完全忽略 merged-in 提交,甚至不走那些路。7 如果我们有:
I--J
/ \₁
...--G--H M--N--O--P <-- main (HEAD)
\ /²
K--L
在点 M
处发生了一些合并,我们 运行 git log
,我们将看到提交 P
、O
、N
、M
、J
、L
、K
、I
、H
等(M
和 H
按某种顺序发生)。8 但是如果我们 运行:
git log --first-parent
walk 会假装提交 M
只有一个 parent、J
,我们将访问提交 P
、O
、N
、M
、J
、I
、H
,依此类推。我们甚至从未 看 提交 K-L
,所以我们从未看到它们。
7请注意,就像岔路口以后会重新汇合一样,如果你改变方向——沿着这条路从你原来的目的地回到你原来的站要点——是,join现在是fork,以前的fork现在是join。因此,由于 Git 向后工作,merges 实际上是 b运行ch 和 b运行ch points 是事情走到一起的地方。这真的完全取决于你如何看待它。
8当合并在图形遍历中提供 git log
分叉时,提交的实际顺序来自您提供的排序选项。 默认 排序是首先显示最高提交日期。如果在进行所有提交时所有计算机时钟都是准确的,这将以正确的顺序显示提交,但有时一台计算机的时钟已关闭,并且提交可能会奇怪地混合在一起。在困难的情况下,考虑使用 git log --graph
来帮助查看实际的提交图结构。
其他选项
正如我在这个答案的顶部提到的,如果你不想要 这些 提交,你必须想要 一些其他 提交.当我说 这些提交 时,我说的都是一般性的——Git 存储提交,所以这就是你得到的全部——但也是具体的。如果您不想 merge 提交,请不要首先 make 合并提交。 (“不开始 none,不会 none”,正如他们所说。)
现在,这有一些巨大的缺点。如果您不进行 merge 提交,您将无法保留您所做的实际原始工作。不过你确实有这个选择。例如,当您 运行 git merge
时,您可以使用 git merge --squash
。这告诉 Git 通过合并 过程 ,但是要进行普通的 non-merge 单个 parent 提交 最后。 (它也无缘无故地打开 --no-commit
。9)
如果您确实使用此方法,请记住删除在合并操作之前找到提交的b运行ch名称 因为这些提交现在与执行它们的(单个)squash-merge 是多余的。如果您允许这些提交稍后重新出现在视图中,它们很可能会造成麻烦。在许多方面,这与让临时或不正确的提交逃逸到其他一些 Git 存储库的那种病毒效应是相同的问题:Git 被构建为 add提交,而不是丢弃它们。但是通过做一个不留合并痕迹的squash-merge,你在以后给自己设了一个陷阱,除非那些now-unwanted提交真的永远消失了。
如果您有多个合并要执行,并且每个合并都有一些冲突需要解决,您可以像正常 (non-squash) 合并或挤压合并那样执行它们。结果将是多次提交:多次合并提交,或多次普通 single-parent 提交。您可以在执行其中任一操作后,然后使用 git reset --soft
使新的 merge-or-not-merge 提交 难以找到 ,然后使用普通的 git commit
制作一个新的、单一的、普通的提交,它与最终合并具有相同的 snapshot。与 git merge --squash
一样,您现在通常应该认为合并的 b运行ches“已死”,您应该摆脱这些提交并假装它们从未存在过,希望它们永远不会回来困扰您。
这样做并没有错,但需要了解自己在做什么。明白后果才去做。
9隐含的 -n
几乎可以肯定只是原始 shell 脚本实现的遗留物,在 [=691= 中一直小心保存]的行为。这很烦人,因为如果您想要 这种行为,您可以 使用git merge -n --squash
。不过现在这是多余的。
在一次提交中压缩所有内容:调用 git reset --soft
然后调用 git commit
:
# from Branch_2 :
git reset --soft master
git commit