[英]Git merge error - different config files in different branches - using tilde - overwritten master branch history - where did I go wrong?
最近,我遇到了一个令人讨厌的合并问题,想知道是否有人可以帮助我理解它,因此不会再发生。
在我的Git存储库中, master
分支是生产环境,我想将dev
分支用作登台环境。
我的计划是在master
和dev
上以不同状态维护配置文件:
master
,配置包含实时第三方API等的连接设置。 dev
,配置包含所有API等的沙盒版本的设置。 我是通过从master
(1)分支分支,将更改提交到配置(2),然后使用“我们的”策略(4)合并到master
来创建dev
的,这样配置将在每个分支上保持不同的状态,但是它们位于他们历史上的观点相同(因此将来对dev
更改可以合并到master
而无需更改配置):
1 git checkout -b dev
2 git commit -am "Dev config settings"
3 git checkout master
4 git merge -s ours dev
我还可以,直到我需要再次编辑配置文件为止。 我对dev
进行了一些更改,如下所示:
commit_1
commit_2
commit_3
commit_to_config_file
commit_5
commit_6
...并四处搜寻,发现可以使用波浪号和数字合并历史记录的不同部分 。
但是,我意识到现在我会误解了此信息。 我认为波浪号的编号方式如下:
commit_1 # dev~6
commit_2 # dev~5
commit_3 # dev~4
commit_to_config_file # dev~3
commit_5 # dev~2
commit_6 # dev~1 = most recent commit
所以我对master
做了这个:
git merge dev~4
git merge -s ours dev~3
git merge dev
...并遇到合并冲突。
我现在还没有意识到我的编号错误,因此解决了我的mergetool中的冲突。 之后,我发现master
上存在错误,并且认为处理它们的最简单方法是直接进行更正。 现在,一切都在生产中运行良好。
在提交了master
,我现在需要使两个分支之间的历史记录恢复同步。 我认为我应该通过再次使用我们的策略合并到dev
身上,从而跳过我对master
所做的提交:
git checkout dev
git merge master -s ours
几天后,我在dev
分支上进行了更多更改,并合并为master
:
git checkout master
git merge dev
不久之后,我发现生产环境(灾难性地)是使用沙箱配置设置! 我用dev
历史覆盖了master
分支历史。 现在它说我将沙箱设置提交到master
上的配置文件中,大约14天前-我错过了,因为在将更改推向原点之前我没有检查历史那么久。
1)为什么将master
合并到dev
然后将dev
合并回master
会导致历史被覆盖?
2)我应该怎么做才能保留不同的历史?
3)我现在认为我上面的示例中的代字号实际上应该是这样的:
commit_1 # dev~5
commit_2 # dev~4
commit_3 # dev~3
commit_to_config_file # dev~2
commit_5 # dev~1
commit_6 # dev = most recent commit
... 那是对的吗?
4)我是否使用一种很好的方法来在两个分支中维护不同的配置文件? (我刚刚看过使用.gitattributes的方法 ,它看起来要好得多)。
首先,让我们介绍这一部分:
我是否使用一种很好的方法在两个分支中维护不同的配置文件? (我刚刚看过使用.gitattributes的方法 ,它看起来要好得多)。
至少一般而言,我不会推荐这两种方法。
相反,如果您有一个控制文件的配置文件,则根本不要检入(至少不在此存储库中检入),您可以将其检入其他存储库,并使此处使用的文件成为指向该存储库的符号链接。例如,“真实的”配置文件存储在另一个存储库中。 将示例或示例或启动程序配置作为源代码控制文件。 如有必要,让系统将此文件复制到实际配置文件(通过.gitignore
)。
在某些情况下,您可能会将配置分为“系统配置”(可能会被跟踪)和“用户配置”(通常不会被跟踪,并且可能完全位于另一个目录中)。 将其与例如.gitattributes
和$HOME/.gitconfig
,其中$HOME/.gitconfig
设置了如何处理源代码控制下的文件,而$HOME/.gitconfig
设置了如何记录提交的名称和电子邮件地址。 前者确实是来源的属性,而后者则不是。
在.gitattributes
使用合并驱动程序的.gitattributes
在于,此类合并驱动程序仅在“真正合并”的情况下运行,这很好,请参阅下面的较长部分。
...可以使用波浪号后跟数字来合并历史记录的不同部分。
这是真的(至少在各种意义上都是如此),但可能会引起误解。 实际上,您可以使用任何提交哈希ID运行git merge
。 当您运行git merge branchX
,Git首先将名称branchX
转换为提交哈希ID。 提交哈希ID是名称branchX
指向的提交哈希ID:
o <-- branchW
/
...--o--o--o--o <-- branchX
\
o--o <-- branchY
在这里,每个圆形o
节点都代表一个提交(一个具有丑陋的哈希ID的对象),并且分支名称只是充当该提交的可移动指针。 为了增加一个类似于branchW
的分支,我们运行git checkout branchW
,它将“ HEAD附加到”分支:
o <-- branchW (HEAD)
/
...--o--o--o--o <-- branchX
\
o--o <-- branchY
并用提示提交内容填充Git的索引 ,并同样填写工作所在的工作树。 (索引是您构建下一个提交的位置,因此它首先与工作树和当前提交相匹配。)然后,我们在工作树中修改文件,并在其中进行工作。 然后我们将更改后的文件复制回索引,以便下次提交将快照更新的版本,而不是重新快照旧版本; 然后我们运行git commit
。
git commit
命令进行一个新的提交*
,其父是分支的当前提示:
o--*
/
...--o--o--o--o <-- branchX
\
o--o <-- branchY
然后将新提交的哈希ID写入与HEAD
相连的分支名称中,因此branchW
指向*
而不是其父级:
o--* <-- branchW (HEAD)
/
...--o--o--o--o <-- branchX
\
o--o <-- branchY
如果运行git merge
并为其指定分支名称,则git merge
找到分支名称指向的尖端提交。 如果您运行git merge
并提供任何可标识其他提交的内容,则git merge
找到其他提交。
找到另一个提交后,它的操作变得有些复杂。 现在让我们进入这部分:
为什么将master合并到dev和将dev合并回master会导致历史被覆盖?
没有! 您正在考虑历史和内容 ,好像它们是同一回事,但它们却大不相同。
在Git中,提交就是历史。 我在上面绘制的图形显示了八点历史记录-八次提交-一旦我们添加了一个新的提交,它已成为branchW
的新技巧。 (当然, ...
部分代表更多历史记录,但这不是我们目前关注的历史记录。)
每个提交都存储一个(单个) 快照 ,这是历史记录中的该源。 正如我上面提到的,进入快照的内容就是索引中的内容,Git也将其称为暂存区域 ,有时也称为缓存 。 它具有多个角色,但主要的作用是它是您进行新提交时进入每个新提交的所有文件的源。
每次添加提交时,都会添加更多历史记录。 每个提交都有一个向后链接,将提交连接到其父级 。 合并提交-在这里我们将单词“ merge
作为形容词或有时用作名词:“ merge”表示“ 合并提交” -有两个 (或更多,但在此不必担心)这些向后链接。 第一个告诉您关于正常的第一个父母的信息,一如既往; 第二个告诉您-以及Git-哪些提交是通过合并操作引入的,因此不再需要考虑。 最后一点将成为问题的关键。
请务必记住, 每次提交都是纯快照。 在此级别上,没有将更改作为更改的概念 。 这只是此级别的快照! 但是大多数提交都有一个父对象 , 如果将父对象的快照与子对象的快照进行比较,则会得到一个更改。
如果比较两次提交,将看到现有文件发生了什么,是否将所有文件全部删除以及是否创建了任何文件。 换句话说,Git可以通过使用历史记录将快照转换为变更集。 但是您不必将父母与孩子进行比较。 相反,您可以将一些曾祖父母或外祖父母与孩子进行比较,以获得更长期的看法。 这就是git merge
进入的地方。我怀疑您实际上对“作为一个词的合并”有一个相当不错的句柄,但我们将在稍后再讨论。
正如您最终怀疑的那样, ~
符号从零开始倒数,而不是从1开始倒数。 让我们绘制一个略有不同的图形,并为提交赋予单个字母名称,以便我们讨论它们:
...--B--C--D <-- master
\
E--F--G--H--I--J <-- dev
dev
名称标识提交I
名称(或实际上几乎可以识别提交的任何名称)都可以附加~
或^
字符,后跟数字。 所有这些都记录在gitrevisions手册页中 ,但是简而言之,波浪号后跟数字意味着“ 算了那么多第一亲链接”。 对于非合并提交,只有一个父级,因此很明显哪个链接也是第一个父级。 因此dev~0
不退回任何步骤,名称提交J
; dev~1
倒退一步,名称提交I
; 等等。
请注意,如果我们倒退六个步骤,则将提交B
命名为master
B
这是Git的一个奇怪功能:提交一次可以在多个分支上 。 (如果不是大多数其他版本控制系统,大多数其他版本控制系统的行为就不会这样:一次提交,一次提交就在您创建的分支上,而且越来越多。)因此,有时最好将提交视为被“包含在”分支中:提交B
包含在master
和dev
。
现在让我们看看git merge
作用-但要注意,这有点复杂。 有一个不幸的是大量案件在这里,但让我们来看看合并作为一种动词, 合并 ,导致在合并案。 我们已经提到了合并的形容词/名词形式。 在合并结束时, git merge
通常会进行合并提交,根据定义,此提交有两个父级:第一个父级是运行git merge
时当前的提交(是HEAD
),第二个是您将其命名为git merge
一个参数。 但是,如何提交呢? 在这里,我们体验动词形式, 以进行合并 :
git checkout master; git merge dev
提交之一是当前提交D
(又名HEAD
,又称master
)。 另一个是提交J
(aka dev
)。
当Git合并这两个提交时,它必须标识第三个提交,我们将其称为merge base 。 像master
和dev
这样的历史就出现在这里,因为从广义上讲,合并基础就是分支“聚集在一起”的提交。 这样的提交可能不止一次; 在这种情况下,Git将一个“最近”的端点作为终点。 因此,如果我们有:
...--B--C--D <-- master (HEAD)
\
E--F--G--H--I--J <-- dev
那么B
,和之前的一切B
,是一个可能的候选人,但由于B
是“最接近”两个分支的顶端提交D
和I
,Git会总是挑B
。
Git现在具有必要的三个输入,并且可以使用B
作为合并基础来合并D
和J
对于正常情况,Git现在将运行两个 git diff
比较,就像您运行了一样:
git diff --find-renames B D > /tmp/b-vs-d.patch # what we did
git diff --find-renames B J > /tmp/b-vs-j.patch # what they did
然后,Git结合了两组更改。 如果我们在“我们这边”(b-vs-d)进行的任何更改与他们在其一边(b-vs-j)进行的任何更改都触及同一文件的同一行,则Git声明冲突。 ,但我们所做的完全相同 。 如果我们都进行了相同的更改,则Git只会获取该更改的一个副本。
如果一切正常(通常是正常的话),就不会有冲突,Git会建立一个工作树和索引,其中包括“ B的一切,根据我们更改的一切进行更改以及根据更改的所有内容进行更改”。 Git现在能够进行新的合并提交,因此我们将其绘制为:
...--B--C--D------------K <-- master (HEAD)
\ /
E--F--G--H--I--J <-- dev
提交K
有两个父母D
和J
,这是正常的合并。
但是你没有那样做。 让我们将其取消绘制,然后返回原始图。 相反,您运行了:
git merge dev~4
因此,让我们画出这样的效果,知道dev~4
从J
到F
跨四个第一个父级链接返回:
...--B--C--D--K <-- master (HEAD)
\ /
E----F--G--H--I--J <-- dev
GIT中合并B
-到- D
与上主变化B
-到- F
上变化dev
,并取得新的合并提交K
。
然后您运行:
git merge -s ours dev~3
-s ours
修改了动词形式merge ,而没有改变名词形式。 动词形式的更改包括以下事实,即Git根本不用理会合并基数(不必这样做)。 但是,如果Git 确实计算了合并基础,那会是什么? 要找出答案,请从K
开始并沿所有可能的路径向后工作,然后从G
( dev~3
)开始并也向后工作。
从K
我们回到D
和F
从G
我们回到F
提交F
在两个分支上,并且最接近两个分支提示,因此F
是合并基础。 然后, -s ours
合并将完全忽略F
,从K
获取树 (快照),并照常与两个父级进行新的合并提交:
...--B--C--D--K--L <-- master (HEAD)
\ / /
E----F--G--H--I--J <-- dev
现在您运行了:
git merge dev
再次,我们从找到( L
和J
)的合并基数开始。 根据需要向后工作: L
的父母是K
和G
, J
的父母是I
,父母是H
,父母是G
这意味着合并基数为G
,Git现在将计算两个差异:
git diff --find-renames G L > /tmp/g-vs-l.patch
git diff --find-renames G J > /tmp/g-vs-j.patch
Git现在从G
的树/快照开始,从g-vs-l
应用我们的更改,从g-vs-j
应用我们的更改,并遇到合并冲突。 在这一点上,Git只是停止了索引和工作树中记录的部分合并(以及冲突)。
[我]解决了我的mergetool中的冲突。
这样就完成了“动词合并”过程,解决了索引和工作树冲突的文件,并将所有最终版本添加到索引中。 您现在可以运行git merge --continue
或git commit
完成合并以获取:
...--B--C--D--K--L--------M <-- master (HEAD)
\ / / /
E----F--G--H--I--J <-- dev
M
的树 (快照)是master
上的最后一个合并,是您解决冲突时构造的树 。
在提交了master信息后,我现在需要使两个分支之间的历史记录恢复同步。 我认为我应该通过再次使用我们的策略合并到开发者身上,从而跳过我对大师所做的提交:
git checkout dev git merge master -s ours
让我们看看它的作用。 在git checkout dev
步替换尖端的索引和工作树内容提交的dev
,重视HEAD
到dev
,并提供:
...--B--C--D--K--L--------M <-- master
\ / / /
E----F--G--H--I--J <-- dev (HEAD)
我们在图中看到的唯一一件事是HEAD
的移动,但是索引和工作树当然也发生了变化。
git merge
步骤现在有点特殊。 和以前一样,您正在使用-s ours
来调用“我们的”策略。 这完全忽略了合并基础。 它只是进行了一次新的提交,与两个父级一起重用了当前的索引内容,所以我们来画一下:
...--B--C--D--K--L--------M <-- master
\ / / / \
E----F--G--H--I--J---N <-- dev (HEAD)
N
的树与J
的树匹配; N
( N~1
)的第一个父级是J
, N
的第二个父级是M
几天后,我在dev分支上做出了更多更改...
好的,让我们绘制一些更多的dev
提交:
...--B--C--D--K--L--------M <-- master
\ / / / \
E----F--G--H--I--J---N--O--P <-- dev (HEAD)
并合并为主人:
git checkout master git merge dev
第一步将HEAD
移至master
,并更改索引和工作树内容以匹配M
第二步找到当前提交M
和命名提交P
的合并基础 。 这是我们可以在两个分支上找到的第一个提交。 这次,从P
开始并向后工作。 其父是O
; O
的父母是N
; 和N
有两个家长,**包括M
。
在这一点上,由于我们正在进行普通的(不是-s ours
)合并,因此Git会做一些奇怪的事情:它完全不会干扰任何一种合并 。 它跳过“动词合并”步骤和 “名词合并”步骤。 相反,它只是立即使得索引和工作树匹配其他承诺,使当前分支名点到另一个承诺:
...--B--C--D--K--L--------M
\ / / / \
E----F--G--H--I--J---N--O--P <-- master (HEAD), dev
现在两个分支名称都指向同一提交! 现在,Commit P
是两个分支的尖端提交; 两个分支都包含所有相同的提交。
无论如何,我们可以使用git merge --no-ff
强制Git进行合并提交。 让我们看看如果使用它会发生什么。 Git将找到合并基础M
,比较M
与M
(无变化),比较M
与P
(它们的变化),并使用它们的变化构建新的提交:
...--B--C--D--K--L--------M---------Q <-- master (HEAD)
\ / / / \ /
E----F--G--H--I--J---N--O--P <-- dev
Q
的树将与P
的树匹配。 换句话说,因为合并的基础是M
本身,我们为自己设定了让任何合并dev
量采取的最终提交dev
作为我们的快照,我们是否做到这一点直接(作为一个快进非合并设置master
点指向提交P
)或通过强制实际合并提交Q
来重用P
的树。
现在,我们对某些问题有了明确的答案,而对于其他一些问题则有指导原则:
1)为什么将master合并到dev中,然后将dev合并回master中,会导致历史被覆盖?
它没有:它只是增加了新的历史。 问题在于新的历史记录使您面临危险。 这始终是一个潜在的问题。 所有合并都需要某种测试和/或检查,因为Git只是遵循一堆简单的文本规则来组合变更集。
2)我应该怎么做才能保留不同的历史?
实际上,除了使用--no-ff
标志强制合并操作进行合并提交外,什么都没有。 从根本上讲,问题在于您将配置视为是与每个快照相对应的文件,并且需要像任何快照中的任何其他文件一样合并。 有时候是真的,有时候不是。 只要文件在这样的树中,就必须检查每次合并的结果。
3)现在,我认为我上面的示例中的代字号编号(相差一个)
正确。
4)我是否使用一种很好的方法来在两个分支中维护不同的配置文件?
这部分很难说。 但是,如果您使用merge=ours
合并驱动程序,请注意, 只有在所有三个输入提交中文件的哈希ID不同时,它才会触发。 也就是说,合并库中的内容必须与HEAD
提交中的内容不同,并且两者都必须与另一个提交中的内容不同。 当然,如果合并碱是相同的提交为输入提交,在合并基础的所有文件必须匹配在尖端提交的至少一个的所有文件。 如果两个技巧的文件内容相同,那么您还是可以的。 因此,在以下情况下,这确实出错了:
HEAD
提交不同,但是 HEAD
提交,在此情况下 HEAD
提交中的文件不同,那么您已经选择了它们的更改! (要了解其工作原理,请尝试运行:
git rev-parse HEAD:Makefile
例如,如果您的提交中有一个名为Makefile
的文件。 每个提交中的每个文件都有一个哈希ID。 哈希ID取决于文件的内容。 如果文件在两个不同的提交中是相同的,则这两个不同的提交在其树中使用相同的Blob哈希,以便它们共享基础文件的单个副本。)
配置文件主要是针对overwrited master
分支时候,第一时间合并dev
分支到master
用快进合并(递归合并策略)分支。
对于工作流程,当您从master
分支创建dev
并在dev
分支上更改配置文件时,假定提交历史记录为打击:
...---A---B master
\
C dev
将dev
合并到master
,提交历史将是:
...---A---B---C master,dev
因此, master
分支上的配置文件会自动更改为与dev
分支相同的版本(我们的合并策略实际上不起作用)。
顺便说一句:由于您需要为每个分支保留不同的配置文件,因此您也可以忽略完整分支的配置文件。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.