Git 合并只是覆盖一个分支?
Git merge just overwrites a branch?
我是 git 的新手,我无法让它按照我想要的方式合并更改。所以这就是我想要完成的事情:
我需要更新我们的学习管理系统。
我正在处理 2 个分支:mergebranch 和 update。在 mergebranch 上,我有旧版本的系统,其中有一些自定义更改和文件不在更新中,但我也需要它们在更新版本中。然后在更新分支上,我有新版本的新文件。
我尝试了 git merge update
,这导致 git 删除了 mergebranch 中的自定义文件,但不在更新中。此外,没有合并冲突,我觉得这很奇怪。我查看了第一行只有版本号的版本文本文件。我预计会发生冲突,但 git 只是用新版本号覆盖了旧版本号。似乎 git 只是将新版本复制到旧版本上,并没有合并发生。我还收到这些我不理解的“删除模式”和“创建模式”消息。
这就是我在 git 中所做的:
- 正在将文件从服务器克隆到本地目录
git checkout -b update
- 删除旧文件并用更新中的新文件替换它们
git add .
git commit -m "Update vanilla version"
git checkout master
git checkout -b mergebranch
git merge update
谁能帮帮我?我迷路了。
On mergebranch
, I have the old version of the system that has some custom changes and files that are not in the update
, but I need them to be in the updated
version as well.
在 Git 中,您总是将一个分支合并到您当前签出的分支中。因此,如果您想将 mergedbranch
中的更改引入 updated
,您必须将 mergebranch
合并到 update
,方法是:
git checkout update
git merge mergebranch
从分支 branch1 获取一些代码到另一个 branch2 :
结账到 branch2 : git checkout branch2
通过运行拉命令从远程分支branch1获取代码:git pull origin branch1
更新
git fetch origin
git checkout origin/mergebranch
5-2) 合并这个分支:
git merge --no-ff update
解决冲突并纠正错误(如果存在)然后将此版本(如果完美)推送到您的分支 ex 中:mergebranch
这个新版本你可以把它推送到 master 或 mergebranch
git push origin mergebranch
如果您与显示的头部消息分离,则改为执行此操作:
git push origin HEAD:mergebranch
根据你的描述,我认为你有这段历史(时间从左到右流动):
D--E--F <-- update
/
--A--B--C <-- mergebranch
当您发出合并命令时,mergebranch
指向提交 C
。这是 so-called fast-forward 的情况。不需要实际合并。 Git 只是将 mergebranch
重新指向提交 F
,因此它等于分支 update
。
D--E--F <-- update, mergebranch
/
--A--B--C
如果两个分支真的像这样分开,则需要进行实际的合并操作:
D--E--F <-- update
/
--A--B--C
\
X--Y <-- mergebranch
在这种情况下,将创建一个合并提交:
D--E--F <-- update
/ \
--A--B--C M <-- mergebranch
\ /
X-----Y
当提交 D
、E
和 F
删除一些文件并添加其他文件时,这些删除和添加将反映在分支 [=15= 的最终状态中],无论操作是 fast-forward 还是真正的合并,因为使用命令
git merge update
你说:“我想要分支 updated
从当前分支 (C
) 分叉到分支 updated
的尖端 (F
) 集成到当前分支中。"
在您理解 git merge
之前,您需要了解 Git 并不是关于 b运行ches 或文件的。 Git 是关于 提交 的。这意味着您需要准确地知道提交是什么以及做什么。我们不会在这里涵盖所有内容,但这些是现在的相关要点:
每个提交都有编号。这些数字不是简单的计数——它们不是从提交 1 到提交 2 再到提交 3——而是丑陋的大哈希 ID。这些看起来 运行dom,但实际上不是:每个实际上都是提交内容的加密校验和。您将在 git log
输出中看到这些哈希 ID(或 运行 git log
现在可以看到它们)。
由于这种加密校验和编号,任何提交的任何部分都不能更改。提交一旦完成,就完全 read-only,而且大部分是永久性的。 (可以摆脱提交,但我们不会在这里讨论。)
一个提交有两部分,它的数据和它的元数据。数据部分包含 Git 在您(或任何人)提交时知道的每个文件的完整快照。元数据包含有关提交本身的信息,例如提交人、时间和原因:他们的姓名、电子邮件地址和日志消息。
在元数据中,Git 添加了一些用于 Git 自身的信息:每个提交都包含其前一个提交的提交编号(哈希 ID)。 Git 将其称为提交的 parent。 (大多数提交恰好具有这些父 ID 之一,但正如我们将看到的,合并提交具有更多。)
Git 可以通过 hash-ID 查找任何提交——或任何内部 Git 对象。因此,如果我们手头有一个哈希 ID,我们就说这个 指向 提交,Git 现在可以找到它。而且,因为每个提交都存储其父项的哈希 ID,所以这些提交指向向后。这意味着我们可以像这样绘制一个简单的普通提交线性链:
... <-F <-G <-H
其中每个大写字母代表一个实际的哈希 ID。如果我们知道散列 ID H
,我们可以 Git 找到实际提交,其中包含文件快照和较早提交的散列 ID G
。这让 Git 找到 G
,其中包含快照和哈希 ID F
,让 Git 找到 F
,等等。
这就是 Git 的工作方式,一般来说:向后,从 last 提交。请注意,例如,提交 G
无法更改:它可以指向 F
,因为 F
在我们(或任何人)创建 G
时存在,但是 H
还不存在,在我们创建它之前我们不会知道 H
的 ID。所以 G
可以指向 F
,但不能指向 H
。 H
可以指向 G
,但不能指向以后的内容。
但是我们还有一个问题。 Git 从哪里获取哈希 ID H
?这就是 b运行ch names 进来的地方。
一个 b运行ch 名称持有一个提交哈希 ID
鉴于提交链:
...--F--G--H
Git只是简单地将hash ID H
放入一些b运行ch name,比如master
。这个名字然后指向提交 H
,这样很容易找到:
...--G--H <-- master
如果我们现在想添加一个 second b运行ch 名称,Git 需要我们选择一些 existing commit,并将新名称指向此提交。很多时候,我们选择我们现在使用的提交——例如,提交 H
:
...--G--H <-- develop, master
现在我们有两个 names——对于同一个提交,此刻——我们需要一种方法来知道我们实际上使用哪个名字。为了解决这个问题,Git 将特殊名称 HEAD
附加到一个 b运行ch 名称,因此我们应该更新绘图:
...--G--H <-- develop, master (HEAD)
这表明虽然两个名称都选择提交 H
,但我们使用的名称是 master
。请注意,每个提交都在 b运行ches.
假设我们现在在 master
上进行了两次 new 提交(没有明显的原因,但也许我们忘记先切换到 develop
)。当我们进行第一次新提交时,Git 将:
- 保存Git知道的每个文件的快照;
- 将我们的姓名和电子邮件地址添加为作者和提交者,并将时间戳设置为“现在”;
- 添加我们的日志消息;
- 使用 当前提交的哈希 ID
H
作为我们新提交的父哈希;
- 写出新的提交,从而获得自己的唯一编号,但我们将其称为
I
;和
- 将新提交的哈希 ID 写入当前 b运行ch 名称.
结果是:
I <-- master (HEAD)
/
...--G--H <-- develop
如果我们在此状态下进行第二次新提交,我们将得到:
I--J <-- master (HEAD)
/
...--G--H <-- develop
此时,通过 H
的提交仍在两者上运行ches,但提交 I
和 J
仅在 master
.
现在让我们 运行 git checkout develop
(或者在 Git 2.23 和更高版本中,git switch develop
会做同样的事情)。这使得我们当前的 b运行ch name 变为 develop
并且我们当前的 commit 返回到 commit H
:
I--J <-- master
/
...--G--H <-- develop (HEAD)
Git 将更新其内部 next-commit 文件(在 Git 的 index 又名 暂存区,我们这里没有涉及)和我们的工作树文件来匹配提交 H
,这样我们就可以从永久保存在 H
中的相同文件开始。如果我们现在进行新的提交,我们会得到:
I--J <-- master
/
...--G--H
\
K <-- develop (HEAD)
请注意,每个 b运行ch 名称仅标识一个提交:J
代表 master
,K
代表开发。 Git 将这些称为 b运行ches 的 tip commits。
进行第二次新提交给我们:
I--J <-- master
/
...--G--H
\
K--L <-- develop (HEAD)
提交 K
和 L
现在仅在 develop
上。我们现在处于 git merge
有意义的情况。
真正的合并
git merge
的作用一句话就可以说清楚,但细节就比较复杂了。 合并就是合并更改。但是我们刚刚看到 Git 实际上并没有 store 更改。每个提交都有每个文件的完整快照。那么Git如何做到这一点呢?
Git对此的回答是回到我们正在制作的图纸上。这些绘图生成了一个 提交图 。通过从任意两个提交开始——通常是两个 b运行ch 名称找到的两个提交——然后 向后、Git 可以找到 最佳常见提交。在这种情况下,很容易看出 H
和 G
以及更早的提交在两个 b运行 上。 H
是 最好的 这样的提交,因为它最接近两个 branch-tip 提交。
Git 将此最佳公共/共享提交称为 合并基础 。要使用git merge
,那么,我们做两件事:
- 运行
git checkout
上两个 b运行ches 中的一个,这样 HEAD
附加到找到的 b运行ch其中一个提示提交;然后
- 运行
git合并<em>otherb运行ch</em>
,这样git merge
可以定位到其他tip commit.
Git然后自己找到合并基础。在我们的例子中,我们可能 运行:
git checkout master
git merge develop
这将使用 H
作为合并基础。
要查找 更改的内容 ,Git 将使用 git diff
命令的内部变体。 Commit H
,合并基础,包含所有文件的快照。提交 J
,在 master
的末尾,也包含所有文件的快照。使用git diff
、Git可以比较这两个快照:
git diff --find-renames <hash-of-H> <hash-of-J> # what we changed
然后,再次使用git diff
,Git可以比较H
和L
,看看他们在develop
上有什么变化:
git diff --find-renames <hash-of-H> <hash-of-L> # what they changed
Merge 现在的工作是合并这两组更改。这可能包括添加新文件和删除现有文件,但对于更典型的合并,我们可能只有一个或几个文件“被我们更改”和一个或几个文件“被他们更改”。
合并冲突发生在:
- 我们和他们都对某个文件做了一些更改,和
- Git 不能合并 这两个变化本身。
因此,如果我们修改文件 F1 的第 42 行,并且他们修改任何其他文件的任何行而不触及文件 F1,Git 将简单地采用我们的 F1 版本,因为他们没有更改该文件。如果他们修改了文件 F2 而我们没有修改它,Git 将只采用他们的文件版本 F2。但是,如果我们都触摸了 F3,Git 将需要合并我们的更改——对我们更改的任何行——以及它们的更改。如果这些更改 重叠或接触 ,Git 将声明合并冲突。
Git 如果我们删除了他们修改过的文件(反之亦然),或者在其他各种情况下,现在还不是那么重要,
Git 也会声明合并冲突。请注意,删除文件是一个“整个文件”的更改,它会自动发生冲突,而不管另一方修改了该文件中的哪些行。但是如果我们删除了一个文件,而他们没有动它,Git 就可以了:我们的“删除这个文件”和他们的“对这个文件什么都不做”的组合就是删除文件。
如果Git能够组合我们的更改和他们的更改,Git将这些组合更改应用于合并库中的快照——在提交中H
,在这种情况下——然后创建一个新的 merge commit,我们在这里称之为 M
。新的合并提交有一个快照,就像任何提交一样。它有元数据,就像任何提交一样:你是作者和提交者,“现在》;用于两个时间戳。但是,与普通提交不同的是,新提交有 两个 个父项。一个是通常的:我们从 运行 git merge
开始的提交,在本例中是提交 J
。另一个是我们在命令行中命名的提交:在本例中,提交 L
。所以生成的合并提交看起来像这样:
I--J
/ \
...--G--H M <-- master (HEAD)
\ /
K--L <-- develop
同样,提交 M
中的 快照 是 合并 H
-vs- 的结果J
随着H
-vs-L
的变化而变化。如果 Git 能够自行组合这些更改,Git 会这样做,然后将它们应用到 H
中的快照。这保留了我们的更改,但也添加了他们的更改。
请注意,从提交 M
返回,我们不仅会访问提交 J
,还会访问提交 L
。所以现在,所有 这些提交都在 master
b运行ch 上。 b运行ch 一次获得了三个提交:新提交 M
,还有提交 K
和 L
,之前只在 develop
.
你的第一期
Also I get these "delete mode" and "create mode" messages that I don't understand.
这是 Git 的说法,您的更改和他们的更改的组合包括删除一些文件——Git 会告诉您 哪个 文件名正在被删除——并创建一些其他不同的文件(Git 会再次告诉您哪些文件)。 mode 部分是文件的模式:文件可执行 (mode 100755
) 或不可执行 (mode 100644
)。这是仅有的两种允许模式。1
您可以明白为什么 Git 认为这些文件是通过 运行 自己执行这两个 git diff --find-renames
命令中的一个或两个命令删除的。这里棘手的部分是找到合并基础提交的哈希 ID,但是 Git 有一个命令可以做到这一点:
git merge-base --all <name-or-hash-id> <name-or-hash-id>
会完成这项工作。例如,如果您在 master
上执行 git merge develop
,而 master
确定了提交 J
并且 develop
确定了提交 L
,您可以找到提交 J
和 L
的哈希 ID,并将这两个用作 git merge-base --all
的参数。然后,您可以将此哈希 ID 与提交 J
的哈希 ID 进行比较,并再次与提交 L
.
的哈希 ID 进行比较
(或者,如果您愿意冒一点点风险来拥有多个合并基础——git merge-base --all
命令将查明是否是这种情况,如果是,您需要一个稍微复杂一点,但通常情况并非如此——您可以使用 git diff
中内置的 three-dot 语法。由于 space 原因,我不会在这里详细介绍。)
1在 2005 年创建的 Git 存储库中,允许的文件模式更多。这被发现是一个坏主意,现代 Git 只生成这两种模式,但是 git fsck
仍然允许模式 100664
,例如,以容纳这些古老的存储库。请记住,任何提交都不能更改,因此无法修复这些包含 mode 100664
文件的提交。
Fast-forwards
有时候,如果你 运行:
git checkout master
git merge develop
Git 会告诉您它执行了 fast-forward,而不是合并。如果我们再次绘制提交图,这意味着什么就更清楚了。假设我们开始于:
...--G--H <-- master, develop (HEAD)
然后以通常的方式向 develop
添加一些提交:
...--G--H <-- master
\
I--J <-- develop (HEAD)
如果我们现在检查master
并合并develop
,Git将以通常的方式找到合并基础:从两个b运行ch开始tip 提交 H
和 J
,并根据需要向后工作以找到最佳 shared 提交。但是这一次,在从 J
退回两步后,Git 到达提交 H
,这是另一个提交。因此 Git 可以从 H
后退 零 步,因此使用 H
作为此合并的合并基础。
此合并将:
- diff
H
与自身进行对比以查看 we 发生了什么变化(当然什么也没有发生!);和
- 将
H
与 J
进行比较,看看 他们 发生了什么变化;然后
- 什么都不结合。
这个组合的结果,当应用于提交 H
时,显然会匹配与提交 J
关联的快照。
因此,在这种情况下,Git 将默认采用 short-cut。它根本不会merge。相反,它将简单地检查另一个提交——在本例中为提交 J
——同时将 current b运行ch name 向前拖动,这样我们就结束了与:
...--G--H--I--J <-- master (HEAD), develop
您可以强制 Git 进行真正的合并,使用 git merge --no-ff
,这会禁用 fast-forward short-cut。这次 Git 真的会比较 H
和自己,比较 H
和 J
,并结合两组变化:
...--G--H------K <-- master (HEAD)
\ /
I--J <-- develop
(什么时候有用甚至是否有用在某种程度上是品味问题,而不是正确性。)
我认为这就是您所看到的情况。另请参阅 ,这是我接近尾声时出现的
“已经是最新的”
还有一个 somewhat-interesting 合并案例。假设你在一些 b运行ch 上,比如 master
,你 运行 git merge develop
并收到消息 Already up to date.
这意味着你有一个看起来像这样的情况:
...--G--H <-- develop
\
I--J <-- master (HEAD)
Git 照常计算合并基数,但这次合并基数 H
落后于 当前 b[=581= 的尖端]ch master
。它实际上是另一个 b运行ch 的尖端。当两个名称定位到同一个提交时也会发生这种情况(例如,如果两个名称都指向 H
,或都指向 J
)。
结论
要查看 git merge
会做什么:
- 绘制图表(或让Git为您绘制;参见
git log --graph
,通常与--all --decorate --oneline
一起使用)。
- 找到 merge base 提交和两个 tip 提交。
- 查看合并基础是一个还是两个提示提交。如果是这样,则合并是微不足道的(fast-forward-able)或已经完成。
- 否则需要真正的合并。如果需要,使用
git diff
查看将合并的两组更改。
- 如果更改集不是您想要的,请检查从合并基础到任何 b运行ch 提示的提交,其中存在一些问题:
- 出了什么问题?
- 您将来如何防止(或至少阻止)这种情况?
- 如果合适,向一个或两个 b运行 分支添加新提交以解决问题。
如有必要,请考虑使用 git merge --no-commit
让 Git 开始 合并但不 完成 合并。然后您可以更正合并,但请注意,这会产生一些人所说的 evil merge。如果你有 Git repeat 这个合并,2 你将不得不做同样的手动修正。或者,让 Git 进行合并,然后添加修复提交。这样做的好处是,如果让 Git 重复合并,它会得到相同的(坏的)结果,但是你可以让 Git 重复修复。
2旧的,now-deprecated git rebase -p
和新的 git rebase -r
命令将“复制”合并,就像任何 rebase 的方式一样复制普通提交,但与普通提交不同,git cherry-pick
不能 复制合并提交。所以这些通过 重复 合并来工作。此重复不包括您在 运行 和 git merge
时指定的任何标志,也不包括您所做的任何手动修正。
感谢大家的意见,问题已解决。
所以我缺少的是合并基础。我从头开始使用系统的原始版本,并从那里创建了两个新分支,一个用于自定义旧版本,一个用于更新。从那里开始,合并过程如我所料。
我是 git 的新手,我无法让它按照我想要的方式合并更改。所以这就是我想要完成的事情:
我需要更新我们的学习管理系统。 我正在处理 2 个分支:mergebranch 和 update。在 mergebranch 上,我有旧版本的系统,其中有一些自定义更改和文件不在更新中,但我也需要它们在更新版本中。然后在更新分支上,我有新版本的新文件。
我尝试了 git merge update
,这导致 git 删除了 mergebranch 中的自定义文件,但不在更新中。此外,没有合并冲突,我觉得这很奇怪。我查看了第一行只有版本号的版本文本文件。我预计会发生冲突,但 git 只是用新版本号覆盖了旧版本号。似乎 git 只是将新版本复制到旧版本上,并没有合并发生。我还收到这些我不理解的“删除模式”和“创建模式”消息。
这就是我在 git 中所做的:
- 正在将文件从服务器克隆到本地目录
git checkout -b update
- 删除旧文件并用更新中的新文件替换它们
git add .
git commit -m "Update vanilla version"
git checkout master
git checkout -b mergebranch
git merge update
谁能帮帮我?我迷路了。
On
mergebranch
, I have the old version of the system that has some custom changes and files that are not in theupdate
, but I need them to be in theupdated
version as well.
在 Git 中,您总是将一个分支合并到您当前签出的分支中。因此,如果您想将 mergedbranch
中的更改引入 updated
,您必须将 mergebranch
合并到 update
,方法是:
git checkout update
git merge mergebranch
从分支 branch1 获取一些代码到另一个 branch2 :
结账到 branch2 :
git checkout branch2
通过运行拉命令从远程分支branch1获取代码:
git pull origin branch1
更新
git fetch origin
git checkout origin/mergebranch
5-2) 合并这个分支:
git merge --no-ff update
解决冲突并纠正错误(如果存在)然后将此版本(如果完美)推送到您的分支 ex 中:mergebranch
这个新版本你可以把它推送到 master 或 mergebranch
git push origin mergebranch
如果您与显示的头部消息分离,则改为执行此操作:
git push origin HEAD:mergebranch
根据你的描述,我认为你有这段历史(时间从左到右流动):
D--E--F <-- update
/
--A--B--C <-- mergebranch
当您发出合并命令时,mergebranch
指向提交 C
。这是 so-called fast-forward 的情况。不需要实际合并。 Git 只是将 mergebranch
重新指向提交 F
,因此它等于分支 update
。
D--E--F <-- update, mergebranch
/
--A--B--C
如果两个分支真的像这样分开,则需要进行实际的合并操作:
D--E--F <-- update
/
--A--B--C
\
X--Y <-- mergebranch
在这种情况下,将创建一个合并提交:
D--E--F <-- update
/ \
--A--B--C M <-- mergebranch
\ /
X-----Y
当提交 D
、E
和 F
删除一些文件并添加其他文件时,这些删除和添加将反映在分支 [=15= 的最终状态中],无论操作是 fast-forward 还是真正的合并,因为使用命令
git merge update
你说:“我想要分支 updated
从当前分支 (C
) 分叉到分支 updated
的尖端 (F
) 集成到当前分支中。"
在您理解 git merge
之前,您需要了解 Git 并不是关于 b运行ches 或文件的。 Git 是关于 提交 的。这意味着您需要准确地知道提交是什么以及做什么。我们不会在这里涵盖所有内容,但这些是现在的相关要点:
每个提交都有编号。这些数字不是简单的计数——它们不是从提交 1 到提交 2 再到提交 3——而是丑陋的大哈希 ID。这些看起来 运行dom,但实际上不是:每个实际上都是提交内容的加密校验和。您将在
git log
输出中看到这些哈希 ID(或 运行git log
现在可以看到它们)。由于这种加密校验和编号,任何提交的任何部分都不能更改。提交一旦完成,就完全 read-only,而且大部分是永久性的。 (可以摆脱提交,但我们不会在这里讨论。)
一个提交有两部分,它的数据和它的元数据。数据部分包含 Git 在您(或任何人)提交时知道的每个文件的完整快照。元数据包含有关提交本身的信息,例如提交人、时间和原因:他们的姓名、电子邮件地址和日志消息。
在元数据中,Git 添加了一些用于 Git 自身的信息:每个提交都包含其前一个提交的提交编号(哈希 ID)。 Git 将其称为提交的 parent。 (大多数提交恰好具有这些父 ID 之一,但正如我们将看到的,合并提交具有更多。)
Git 可以通过 hash-ID 查找任何提交——或任何内部 Git 对象。因此,如果我们手头有一个哈希 ID,我们就说这个 指向 提交,Git 现在可以找到它。而且,因为每个提交都存储其父项的哈希 ID,所以这些提交指向向后。这意味着我们可以像这样绘制一个简单的普通提交线性链:
... <-F <-G <-H
其中每个大写字母代表一个实际的哈希 ID。如果我们知道散列 ID H
,我们可以 Git 找到实际提交,其中包含文件快照和较早提交的散列 ID G
。这让 Git 找到 G
,其中包含快照和哈希 ID F
,让 Git 找到 F
,等等。
这就是 Git 的工作方式,一般来说:向后,从 last 提交。请注意,例如,提交 G
无法更改:它可以指向 F
,因为 F
在我们(或任何人)创建 G
时存在,但是 H
还不存在,在我们创建它之前我们不会知道 H
的 ID。所以 G
可以指向 F
,但不能指向 H
。 H
可以指向 G
,但不能指向以后的内容。
但是我们还有一个问题。 Git 从哪里获取哈希 ID H
?这就是 b运行ch names 进来的地方。
一个 b运行ch 名称持有一个提交哈希 ID
鉴于提交链:
...--F--G--H
Git只是简单地将hash ID H
放入一些b运行ch name,比如master
。这个名字然后指向提交 H
,这样很容易找到:
...--G--H <-- master
如果我们现在想添加一个 second b运行ch 名称,Git 需要我们选择一些 existing commit,并将新名称指向此提交。很多时候,我们选择我们现在使用的提交——例如,提交 H
:
...--G--H <-- develop, master
现在我们有两个 names——对于同一个提交,此刻——我们需要一种方法来知道我们实际上使用哪个名字。为了解决这个问题,Git 将特殊名称 HEAD
附加到一个 b运行ch 名称,因此我们应该更新绘图:
...--G--H <-- develop, master (HEAD)
这表明虽然两个名称都选择提交 H
,但我们使用的名称是 master
。请注意,每个提交都在 b运行ches.
假设我们现在在 master
上进行了两次 new 提交(没有明显的原因,但也许我们忘记先切换到 develop
)。当我们进行第一次新提交时,Git 将:
- 保存Git知道的每个文件的快照;
- 将我们的姓名和电子邮件地址添加为作者和提交者,并将时间戳设置为“现在”;
- 添加我们的日志消息;
- 使用 当前提交的哈希 ID
H
作为我们新提交的父哈希; - 写出新的提交,从而获得自己的唯一编号,但我们将其称为
I
;和 - 将新提交的哈希 ID 写入当前 b运行ch 名称.
结果是:
I <-- master (HEAD)
/
...--G--H <-- develop
如果我们在此状态下进行第二次新提交,我们将得到:
I--J <-- master (HEAD)
/
...--G--H <-- develop
此时,通过 H
的提交仍在两者上运行ches,但提交 I
和 J
仅在 master
.
现在让我们 运行 git checkout develop
(或者在 Git 2.23 和更高版本中,git switch develop
会做同样的事情)。这使得我们当前的 b运行ch name 变为 develop
并且我们当前的 commit 返回到 commit H
:
I--J <-- master
/
...--G--H <-- develop (HEAD)
Git 将更新其内部 next-commit 文件(在 Git 的 index 又名 暂存区,我们这里没有涉及)和我们的工作树文件来匹配提交 H
,这样我们就可以从永久保存在 H
中的相同文件开始。如果我们现在进行新的提交,我们会得到:
I--J <-- master
/
...--G--H
\
K <-- develop (HEAD)
请注意,每个 b运行ch 名称仅标识一个提交:J
代表 master
,K
代表开发。 Git 将这些称为 b运行ches 的 tip commits。
进行第二次新提交给我们:
I--J <-- master
/
...--G--H
\
K--L <-- develop (HEAD)
提交 K
和 L
现在仅在 develop
上。我们现在处于 git merge
有意义的情况。
真正的合并
git merge
的作用一句话就可以说清楚,但细节就比较复杂了。 合并就是合并更改。但是我们刚刚看到 Git 实际上并没有 store 更改。每个提交都有每个文件的完整快照。那么Git如何做到这一点呢?
Git对此的回答是回到我们正在制作的图纸上。这些绘图生成了一个 提交图 。通过从任意两个提交开始——通常是两个 b运行ch 名称找到的两个提交——然后 向后、Git 可以找到 最佳常见提交。在这种情况下,很容易看出 H
和 G
以及更早的提交在两个 b运行 上。 H
是 最好的 这样的提交,因为它最接近两个 branch-tip 提交。
Git 将此最佳公共/共享提交称为 合并基础 。要使用git merge
,那么,我们做两件事:
- 运行
git checkout
上两个 b运行ches 中的一个,这样HEAD
附加到找到的 b运行ch其中一个提示提交;然后 - 运行
git合并<em>otherb运行ch</em>
,这样git merge
可以定位到其他tip commit.
Git然后自己找到合并基础。在我们的例子中,我们可能 运行:
git checkout master
git merge develop
这将使用 H
作为合并基础。
要查找 更改的内容 ,Git 将使用 git diff
命令的内部变体。 Commit H
,合并基础,包含所有文件的快照。提交 J
,在 master
的末尾,也包含所有文件的快照。使用git diff
、Git可以比较这两个快照:
git diff --find-renames <hash-of-H> <hash-of-J> # what we changed
然后,再次使用git diff
,Git可以比较H
和L
,看看他们在develop
上有什么变化:
git diff --find-renames <hash-of-H> <hash-of-L> # what they changed
Merge 现在的工作是合并这两组更改。这可能包括添加新文件和删除现有文件,但对于更典型的合并,我们可能只有一个或几个文件“被我们更改”和一个或几个文件“被他们更改”。
合并冲突发生在:
- 我们和他们都对某个文件做了一些更改,和
- Git 不能合并 这两个变化本身。
因此,如果我们修改文件 F1 的第 42 行,并且他们修改任何其他文件的任何行而不触及文件 F1,Git 将简单地采用我们的 F1 版本,因为他们没有更改该文件。如果他们修改了文件 F2 而我们没有修改它,Git 将只采用他们的文件版本 F2。但是,如果我们都触摸了 F3,Git 将需要合并我们的更改——对我们更改的任何行——以及它们的更改。如果这些更改 重叠或接触 ,Git 将声明合并冲突。
Git 如果我们删除了他们修改过的文件(反之亦然),或者在其他各种情况下,现在还不是那么重要,
Git 也会声明合并冲突。请注意,删除文件是一个“整个文件”的更改,它会自动发生冲突,而不管另一方修改了该文件中的哪些行。但是如果我们删除了一个文件,而他们没有动它,Git 就可以了:我们的“删除这个文件”和他们的“对这个文件什么都不做”的组合就是删除文件。
如果Git能够组合我们的更改和他们的更改,Git将这些组合更改应用于合并库中的快照——在提交中H
,在这种情况下——然后创建一个新的 merge commit,我们在这里称之为 M
。新的合并提交有一个快照,就像任何提交一样。它有元数据,就像任何提交一样:你是作者和提交者,“现在》;用于两个时间戳。但是,与普通提交不同的是,新提交有 两个 个父项。一个是通常的:我们从 运行 git merge
开始的提交,在本例中是提交 J
。另一个是我们在命令行中命名的提交:在本例中,提交 L
。所以生成的合并提交看起来像这样:
I--J
/ \
...--G--H M <-- master (HEAD)
\ /
K--L <-- develop
同样,提交 M
中的 快照 是 合并 H
-vs- 的结果J
随着H
-vs-L
的变化而变化。如果 Git 能够自行组合这些更改,Git 会这样做,然后将它们应用到 H
中的快照。这保留了我们的更改,但也添加了他们的更改。
请注意,从提交 M
返回,我们不仅会访问提交 J
,还会访问提交 L
。所以现在,所有 这些提交都在 master
b运行ch 上。 b运行ch 一次获得了三个提交:新提交 M
,还有提交 K
和 L
,之前只在 develop
.
你的第一期
Also I get these "delete mode" and "create mode" messages that I don't understand.
这是 Git 的说法,您的更改和他们的更改的组合包括删除一些文件——Git 会告诉您 哪个 文件名正在被删除——并创建一些其他不同的文件(Git 会再次告诉您哪些文件)。 mode 部分是文件的模式:文件可执行 (mode 100755
) 或不可执行 (mode 100644
)。这是仅有的两种允许模式。1
您可以明白为什么 Git 认为这些文件是通过 运行 自己执行这两个 git diff --find-renames
命令中的一个或两个命令删除的。这里棘手的部分是找到合并基础提交的哈希 ID,但是 Git 有一个命令可以做到这一点:
git merge-base --all <name-or-hash-id> <name-or-hash-id>
会完成这项工作。例如,如果您在 master
上执行 git merge develop
,而 master
确定了提交 J
并且 develop
确定了提交 L
,您可以找到提交 J
和 L
的哈希 ID,并将这两个用作 git merge-base --all
的参数。然后,您可以将此哈希 ID 与提交 J
的哈希 ID 进行比较,并再次与提交 L
.
(或者,如果您愿意冒一点点风险来拥有多个合并基础——git merge-base --all
命令将查明是否是这种情况,如果是,您需要一个稍微复杂一点,但通常情况并非如此——您可以使用 git diff
中内置的 three-dot 语法。由于 space 原因,我不会在这里详细介绍。)
1在 2005 年创建的 Git 存储库中,允许的文件模式更多。这被发现是一个坏主意,现代 Git 只生成这两种模式,但是 git fsck
仍然允许模式 100664
,例如,以容纳这些古老的存储库。请记住,任何提交都不能更改,因此无法修复这些包含 mode 100664
文件的提交。
Fast-forwards
有时候,如果你 运行:
git checkout master
git merge develop
Git 会告诉您它执行了 fast-forward,而不是合并。如果我们再次绘制提交图,这意味着什么就更清楚了。假设我们开始于:
...--G--H <-- master, develop (HEAD)
然后以通常的方式向 develop
添加一些提交:
...--G--H <-- master
\
I--J <-- develop (HEAD)
如果我们现在检查master
并合并develop
,Git将以通常的方式找到合并基础:从两个b运行ch开始tip 提交 H
和 J
,并根据需要向后工作以找到最佳 shared 提交。但是这一次,在从 J
退回两步后,Git 到达提交 H
,这是另一个提交。因此 Git 可以从 H
后退 零 步,因此使用 H
作为此合并的合并基础。
此合并将:
- diff
H
与自身进行对比以查看 we 发生了什么变化(当然什么也没有发生!);和 - 将
H
与J
进行比较,看看 他们 发生了什么变化;然后 - 什么都不结合。
这个组合的结果,当应用于提交 H
时,显然会匹配与提交 J
关联的快照。
因此,在这种情况下,Git 将默认采用 short-cut。它根本不会merge。相反,它将简单地检查另一个提交——在本例中为提交 J
——同时将 current b运行ch name 向前拖动,这样我们就结束了与:
...--G--H--I--J <-- master (HEAD), develop
您可以强制 Git 进行真正的合并,使用 git merge --no-ff
,这会禁用 fast-forward short-cut。这次 Git 真的会比较 H
和自己,比较 H
和 J
,并结合两组变化:
...--G--H------K <-- master (HEAD)
\ /
I--J <-- develop
(什么时候有用甚至是否有用在某种程度上是品味问题,而不是正确性。)
我认为这就是您所看到的情况。另请参阅
“已经是最新的”
还有一个 somewhat-interesting 合并案例。假设你在一些 b运行ch 上,比如 master
,你 运行 git merge develop
并收到消息 Already up to date.
这意味着你有一个看起来像这样的情况:
...--G--H <-- develop
\
I--J <-- master (HEAD)
Git 照常计算合并基数,但这次合并基数 H
落后于 当前 b[=581= 的尖端]ch master
。它实际上是另一个 b运行ch 的尖端。当两个名称定位到同一个提交时也会发生这种情况(例如,如果两个名称都指向 H
,或都指向 J
)。
结论
要查看 git merge
会做什么:
- 绘制图表(或让Git为您绘制;参见
git log --graph
,通常与--all --decorate --oneline
一起使用)。 - 找到 merge base 提交和两个 tip 提交。
- 查看合并基础是一个还是两个提示提交。如果是这样,则合并是微不足道的(fast-forward-able)或已经完成。
- 否则需要真正的合并。如果需要,使用
git diff
查看将合并的两组更改。 - 如果更改集不是您想要的,请检查从合并基础到任何 b运行ch 提示的提交,其中存在一些问题:
- 出了什么问题?
- 您将来如何防止(或至少阻止)这种情况?
- 如果合适,向一个或两个 b运行 分支添加新提交以解决问题。
如有必要,请考虑使用 git merge --no-commit
让 Git 开始 合并但不 完成 合并。然后您可以更正合并,但请注意,这会产生一些人所说的 evil merge。如果你有 Git repeat 这个合并,2 你将不得不做同样的手动修正。或者,让 Git 进行合并,然后添加修复提交。这样做的好处是,如果让 Git 重复合并,它会得到相同的(坏的)结果,但是你可以让 Git 重复修复。
2旧的,now-deprecated git rebase -p
和新的 git rebase -r
命令将“复制”合并,就像任何 rebase 的方式一样复制普通提交,但与普通提交不同,git cherry-pick
不能 复制合并提交。所以这些通过 重复 合并来工作。此重复不包括您在 运行 和 git merge
时指定的任何标志,也不包括您所做的任何手动修正。
感谢大家的意见,问题已解决。
所以我缺少的是合并基础。我从头开始使用系统的原始版本,并从那里创建了两个新分支,一个用于自定义旧版本,一个用于更新。从那里开始,合并过程如我所料。