Git 操作提交历史记录,同时避免分支之间的合并冲突

Git 操作提交历史记录,同时避免分支之间的合并冲突,git,version-control,branching-and-merging,Git,Version Control,Branching And Merging,我们的团队第一次开始使用git。我们有三个分支机构:master、a和b。Master有五个提交(commit1到commit5),而分支a和b也包含所有五个提交以及它们自己的一些新提交。a和b将在将来合并为master 最近,我发现提交2和3不属于master。它应该属于a。主服务器应仅具有提交1、4和5。提交2和3将在稍后合并。此时我们还没有创建单独的分支。这是一个问题,因为a和b是从master分支出来的,因此具有相同的前5次提交 有没有一种方法可以将提交2和3从master和b中删除,同

我们的团队第一次开始使用git。我们有三个分支机构:master、a和b。Master有五个提交(commit1到commit5),而分支a和b也包含所有五个提交以及它们自己的一些新提交。a和b将在将来合并为master

最近,我发现提交2和3不属于master。它应该属于a。主服务器应仅具有提交1、4和5。提交2和3将在稍后合并。此时我们还没有创建单独的分支。这是一个问题,因为a和b是从master分支出来的,因此具有相同的前5次提交


有没有一种方法可以将提交2和3从master和b中删除,同时避免以后在将a合并到master中时发生合并冲突?

简短的回答通常是“否”。但是,由于提交的内容相对较少,而且您的团队可能足够小,因此有一种方法可以公平、干净地处理此问题,还有一些方法可能会比较混乱,但可能就足够了。(另外,还有一个简短的旁注:有一些更快的方法可以完成我在下面描述的几乎所有事情,使用power tools版本的单个一次提交方法。出于篇幅原因,我将不使用它们。)

TL;DR摘要 我可能会选择重写历史,因为存储库非常小,而且您的团队可能有能力处理它。(如果每个人都能一起工作几个小时左右,这一点尤其正确。)在一个更大的项目中,我可能会选择下面显示的最后一个(混合)备选方案,或者在许多情况下,忽略这个问题,因为我标记的提交
B
C
可能不会造成太大的麻烦。但所有这些都是价值观,不一定有“正确”的答案

长部分 主要技巧是从绘制提交开始。请记住,在Git中,提交由其丑陋的大哈希ID标识,每个提交都记录其父提交的哈希ID。这意味着提交总是“向后点”,事实上Git总是向后工作。每个提交还包含整个源代码的完整快照:每当您将提交视为更改时,都要让Git从该提交向后看它的父级,然后比较两个快照

每个提交的散列ID(通过加密散列)非常依赖于提交的所有内容:作者和提交人的名称、电子邮件地址和时间戳、源快照、提交消息以及父提交散列ID。因此,如果您试图更改有关提交的任何内容,您将获得一个新的、不同的哈希ID

不过,我不会写出大而难看的散列ID,我只会在每次提交时使用一个大写字母(并且在26次提交后用完…)。您或多或少都有这样的情况(我只是猜测两个分支中的每一个都有两个提交):

无法实现此关系图,因为
D
的哈希ID表示一个提交,其父级是
a
,其源快照是当前
D
中的快照,这与
C
中的快照类似

我们可以做的是进行一次提交,我们称之为
D'
,它将
a
作为其父级,并将更改从-
C
-转换为-
D
,并将该更改应用于
a
中的快照。我们通过使用
git checkout
直接签出commit
A
(作为“分离头”),然后使分支名称
new master
指向它,使用:

$ git checkout <hash of A>
$ git checkout -b new-master
Cherry pick将比较
D
与其父
C
,并使用结果更改将更改应用到我们现在的位置(
HEAD=new master
),并进行新提交
D'

A--D'   <-- new-master (HEAD)
 \
  B         H--I   <-- branch-b
   \       /
    C--D--E   <-- master
           \
            F--G   <-- branch-a
注意,这与你希望拥有的东西有多相似。让我们停止在图的下半部分绘制,但记住它仍然在那里,现在创建
new-branch-a
,并开始绘制:

$ git checkout -b new-branch-a

A--D'-E'   <-- new-master, new-branch-a (HEAD)
(对其余三次提交重复此操作):

我们现在所要做的就是删除原来的三个分支名称,并将这三个分支重命名为
master
branch-a
、和
branch-b
,任何不记得原始散列ID的人都会认为历史以某种方式神奇地,已经重写。
历史实际上根本没有改变,每个拥有原始提交和原始名称的人仍然拥有原始历史,这就是它变得有点毛茸茸的地方,因为Git是分布式的

拥有存储库克隆的每个人都有其原始哈希ID的所有原始提交。这意味着团队的每个成员在他们自己的Git存储库中都有格式为
origin/master
origin/branch-a
的名称,和
origin/branch-b
,它们记住提交
E
G
I
的散列ID,并且它们具有您不想要原始历史记录而不是重写历史记录的所有原始提交

他们必须小心地采取任何建立在
E
G
I
基础上的工作,并移植这些工作(使用
git cherry pick
或同等工具),使之从
E'
G'
I'
发展而来。如果你的团队足够小,而且每个人都参加了会议,讨论了如何处理这次历史重写,那么对他们来说,这可能和重写对你来说一样容易

棘手的是,如果他们不小心,他们可以将所有原始提交重新引入到自己的历史记录中(通过让分支名称指向原始提交,或者指向父级指向原始提交的提交)。然后,当他们将他们的历史与您重写的历史合并时,他们会重新介绍所有原始历史。这种东西
$ git checkout -b new-master <hash of A>
A   <-- new-master (HEAD)
 \
  B         H--I   <-- branch-b
   \       /
    C--D--E   <-- master
           \
            F--G   <-- branch-a
$ git cherry-pick <hash of D>
A--D'   <-- new-master (HEAD)
 \
  B         H--I   <-- branch-b
   \       /
    C--D--E   <-- master
           \
            F--G   <-- branch-a
A--D'-E'   <-- new-master (HEAD)
 \
  B         H--I   <-- branch-b
   \       /
    C--D--E   <-- master
           \
            F--G   <-- branch-a
$ git checkout -b new-branch-a

A--D'-E'   <-- new-master, new-branch-a (HEAD)
$ git cherry-pick <hash of B>

A--D'-E'   <-- new-master
       \
        B'   <-- new-branch-a (HEAD)
A--D'-E'   <-- new-master
       \
        B'-C'-F'-G'   <-- new-branch-a (HEAD)
        H'-I'   <-- new-branch-b (HEAD)
       /
A--D'-E'   <-- new-master
       \
        B'-C'-F'-G'   <-- new-branch-a
              H--I   <-- branch-b
             /
A--B--C--D--E   <-- master
             \
              F--G   <-- branch-a
$ git checkout branch-a
$ git revert <hash of C>
              H--I   <-- branch-b
             /
A--B--C--D--E   <-- master
             \
              F--G--J   <-- branch-a (HEAD)
              H--I   <-- branch-b
             /
A--B--C--D--E   <-- master
             \
              F--G--J--K   <-- branch-a (HEAD)
              H--I--L--M   <-- branch-b (HEAD)
             /
A--B--C--D--E   <-- master
             \
              F--G--J--K   <-- branch-a
git diff --find-renames E <tip of master>   # what we changed
git diff --find-renames E <tip of branch-a> # what they changed
              H--I   <-- branch-b
             /
A--B--C--D--E   <-- master
             \
              F--G   <-- branch-a
              H--I   <-- branch-b
             /
A--B--C--D--E--J--K   <-- master (HEAD)
             \
              F--G   <-- branch-a
$ git checkout branch-a && git merge master
# write a commit message explaining that this is a special backout merge
(and repeat for branch-b)
              H--I--M   <-- branch-b (HEAD)
             /     /
A--B--C--D--E--J--K   <-- master
             \     \
              F--G--L   <-- branch-a
              H--I--M   <-- branch-b
             /     /
A--B--C--D--E--J--K--B'-C'  <-- master (HEAD)
             \     \
              F--G--L   <-- branch-a
              H--I--M   <-- branch-b
             /     /
A--B--C--D--E--J--K--B'-C'  <-- master
             \     \
              F--G--L--N--O--P   <-- branch-a
              H--I--M   <-- branch-b
             /     /
A--B--C--D--E--J--K--B'-C'-----Q  <-- master (HEAD)
             \     \          /
              F--G--L--N--O--P   <-- branch-a