git优雅rebase/complete合并到分支

git graceful rebase/complete merge to branch

晚安,

我目前正在尝试弄清楚如何 'overwrite' 一个分支而不使用像 'rebase'.

这样极端的东西

例如:

branch::master
|- dir_a
|  |- file_a
|  |- file_b
|- file_c

branch::dev
|- dir_a
|  |- file_b
|- file_c        [edited compared to branch::master]


branch::master   [after merge from branch::dev with strategy 'ours' (keep changes from branch::dev)]
|- dir_a
|  |- file_a     [is also merged, but should be deleted]
|  |- file_b
|- file_c        [edited version from branch::dev]

要解决上述问题,我可以做一个 'rebase',但这会破坏很多东西,因为 master(head) 分支不是 private/personal。

所以问题是:有没有什么好的方法可以完全overwrite/replacemaster分支的内容,不破坏当前master分支的依赖?


可能影响可能性的信息:

如果需要任何其他说明,请随时提出。 感谢您的思考!

Is there a good way to completly overwrite/replace the content of the master branch ...

不止一个,但在你选择任何一个之前,你需要清楚你所说的 branchcontent 是什么意思.

关于分支要记住的是每个分支 name 简单地说 my latest commit is _____ (使用原始提交哈希 ID 填写空白)。 "on" 分支的提交取决于 提交

每个提交都有一个唯一的哈希 ID。该哈希 ID 表示 that 提交,而不是任何其他提交。一旦提交,它的任何部分都不能更改。所有提交都被完全冻结。提交也大多是永久性的:至多,您可以停止使用提交,并取消查找该提交的方法,我们稍后会看到。

每个提交都有两件事:作为它的主要数据,所有文件的完整快照,以及作为它的元数据——关于关于提交的信息。这包括提交者的姓名和电子邮件地址,以及解释 为什么 他们做出该提交的日志消息。但最重要的是,至少对于 Git,是这样的:当您或任何人进行 new 提交时,Git 记录原始哈希 ID它的直接 parent 提交。该散列 ID 是您刚才提交的散列 ID, 分支中的最后一次提交。同样,此数据(快照)或元数据(包括父哈希 ID)的任何部分都不能从现在开始更改 — 因此这意味着 this 提交会记住 previous branch-tip 提交。

因此,"content of a branch" 可以是:

  • 分支中最后一次提交的原始哈希ID;或
  • 那个哈希 ID 和因此那个提交,加上存储在那个提交中的哈希 ID 和因此之前的提交,加上存储在那个提交中的另一个哈希 ID 和因此另一个之前的提交,加上 ...

如果我们绘制这些提交——这需要对它们的哈希 ID 做出假设;在这里,我将用单个大写字母替换每个实际的哈希 ID——我们得到这样的图片:

             I--J   <-- master
            /
...--F--G--H
            \
             K--L   <-- dev

也就是说,name master 包含提交 J 的原始哈希 ID(不管它是什么)。提交 J 保存提交 I 的原始哈希 ID,它保存提交 H 的哈希 ID。同时,名称 dev 包含提交 L 的哈希 ID,后者包含 K 的哈希 ID,后者包含 H.

的哈希 ID

请注意,通过 H 的提交在 两个 分支上。

在这种情况下,分支在 H 处汇合,至少从 Git 的角度来看:从 结束 并且向后工作,一次提交一个。


1从技术上讲,索引包含文件的名称、它们的模式(+x 或 -x)以及对 的 引用frozen-format 内容。


树枝通常如何生长

要进行新的提交,您可以从 git checkout <em>name</em> 开始。假设本例中的名称是 master。这会找到名称指向 提交——在本例中为 J——并将提交的 read-only 内容从提交中复制到 Git 的索引并放入您的 work-tree。 work-tree 是您可以查看和编辑文件的地方。这还将特殊名称 HEAD 附加到名称 master:

             I--J   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

Git 现在可以看到 当前分支 master(通过查看 HEAD 的附加位置)和 当前提交J(通过查看master点)。您的 work-tree 现在拥有 使用的文件——普通文件,而不是 freeze-dried(压缩和 Git-only)提交的文件——并且在 Git 的索引,J 中有 freeze-dried 个文件的副本 1,准备进入新的提交。

您现在可以随意修改 work-tree。完成后,您可以 运行 git add 处理各种文件。这会将每个添加的文件的 content 复制回 Git 的索引,将它们压缩为 Git 将存储在提交中的 freeze-dried 形式, 替换了之前的副本。或者,您可以 git add 一个新文件,或 git rm 一个现有文件;无论哪种方式,Git 都会相应地更新索引。

那么,你运行git commit。 Git 简单地打包 index 中的任何内容,添加您的姓名和当前时间,添加您的日志消息,并将其作为新提交写出。新提交的 parentcurrent commit 并且 hash ID 是新提交内容的唯一校验和,永远无法更改。我们将此新提交称为 N(跳过 M)。 N 指向 J:

                  N
                 /
             I--J   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

但现在我们得到了使分支有用的技巧:Git 现在写入新提交的哈希 ID进入分支名称。 所以我们现在有:

             I--J--N   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

请注意 HEAD 没有改变:它仍然附加到 master

现在让我们摆脱 提交N。它实际上不会消失——它仍将在我们的存储库中——但我们将安排 name master 再次识别提交 J。为此,我们找到 J 的实际哈希 ID 或它的任何代理,并使用 git reset --hard:

git reset --hard <hash-of-J>

现在我们有:

                  N   [abandoned]
                 /
             I--J   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

如果您在 您的 存储库中执行此操作,而不使用 git push 发送 新提交 N 到一些 other Git 存储库,只有您可以提交 N。没有人会知道你做了这件事!

任意使用git reset

您可以使用 git reset,将 any 分支移动到 any 提交。但是如果你移动一个分支 "backwards",就像我们刚才做的那样,"fall off the end" 的提交变得很难找到。假设您强制名称 master 指向提交 L。那么,您将如何找到提交 J? commit I 怎么样?他们好像不见了!

可能就是你想要的。但是,如果某些 other Git 存储库 已经 提交 IJ,也许有人正在使用该存储库已构建 new 提交 link 回到 J。他们不会感激你要求他们忘记所有的提交,以及 IJ.

如果可以让其他人忘记 IJ,您可以将 master 重置为指向 L。然后你必须使用 git push --force 或等价物来说服一些 other Git 存储库忘记 IJ (可能还有额外的提交) 同样,使用此存储库的克隆的其他人需要确保 他们的 Git 忘记 IJ,等等。

这是使 master 匹配 dev 最快最简单的方法,但它对其他人的干扰也是最大的。 Git "likes" 当分支增长时它,"dislikes" 当它们失去提交时它。

合并的工作原理(缩写)

现在让我们看看如果 运行 git merge devmaster 再次指向 J)会发生什么。 Git 对于此合并操作,需要 三个 输入提交。一个是您当前的提交,合并调用 ours。一个是您选择的任何其他提交,合并调用 theirs第三个——某种意义上说是第一个;至少,一下子就排在第一了——Git发现靠自己。

通过使用 git merge dev,您选择提交 L 作为 theirs 提交。那是因为名字dev选择了提交L。 Git 现在从 both 分支提示向后工作以找到 best shared commit,这在这种情况下,显然是提交 H。 Git 称其为 合并基础

请记住,每次提交都会保存一个快照。所以 commit H 有一个快照,commit J 有一个,commit L 有一个。 Git 可以随时比较任意两个快照 以查看有何不同。为您执行此操作的命令是 git diff。合并操作想知道有什么不同,所以它 运行s 内部等效于:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed on master
git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed on dev

Git 现在可以合并这两组更改,并将合并的 更改应用于H(不是 J,不是 L,而是 H:合并基数)。如果我们添加了一个文件而他们没有,Git 会添加该文件。如果我们删除了一个文件而他们没有,Git 会删除该文件。如果他们更改了文件而我们没有,Git 更改文件。

如果一切顺利,结果就是我们的更改和他们的更改的结合。 Git 会将这个结果填回索引中——这是 Git 进行提交的地方——同时也会更新我们的 work-tree 以便我们可以看到它做了什么。然后,由于一切顺利,Git 进行了新的提交。

我们将 merge 称为 M 的新提交没有一个,而是 两个 个父项。 first 父级与往常一样:M links 回到现有提交 J,这是 master 的地方刚才。但是 Git 添加了我们选择合并的提交 L 作为 second 父级。所以现在的图片是:2

             I--J
            /    \
...--F--G--H      M   <-- master (HEAD)
            \    /
             K--L   <-- dev

提交 M 有快照。它的快照是由Git(而不是你)制作的组合从共同起点——合并基础——并将组合更改应用到 merge-base 快照。

如果您遇到合并冲突,索引将发挥更大的作用并帮助解决冲突。最终合并提交中的最终快照由您决定,而不是 GitM。但是如果没有任何合并冲突,Git 通常会自行合并。 通常在这里是一个重要的词。


2提交 N 仍然存在,但无法 找到 它,你看不到它更多,我们不需要费心将它画进去。最终,Git 将完全删除它——通常是在至少 30 天过去后的某个时间。在那之前,如果你想要它,你可以取回它:你只需要找到它的哈希 ID。


这是一个正常的合并;您可能需要覆盖合并

假设您可以告诉 Git:开始合并,但不要进行合并提交 M Git 会像往常一样找到合并基础,像往常一样合并您的更改和它们的更改,并像往常一样更新您的 work-tree 和 Git 的索引...然后停止,不进行提交。

您完全可以做到这一点,使用 git merge --no-commit。完成后,您可以 将合并结果替换 为您喜欢的任何内容。将您喜欢的任何文件放入 work-tree,然后使用 git add 让 Git 将其复制到索引中。索引副本将进入新的合并提交,现在与您放入 work-tree.

中的任何文件相匹配

您可以在此处完全添加新文件和删除文件。无论您做什么,都取决于 您: 此时您可以控制一切。你可以让合并有任何你想要的内容。

你想要什么——根据你的问题,反正;想想这是否是真的你想要的——是完全忽略从合并基础H到你的提交J的差异,而只是从H 他们的 comimt L。当然,这将与提交 L 中的快照完全匹配。

有一个快速简单的方法,使用 git merge -s ours,告诉 Git 它应该 完全忽略他们分支的更改 并且只使用你的版本一切,即提交 J。不幸的是,这与您在这里想要的相反:您正在寻求一种方法 完全忽略分支的更改 并仅使用 他们的 版本一切,即提交 L.

幸运的是,有一个相当神奇的命令3你可以使用它又快又简单,在这里得到你想要的东西:

git merge --no-commit dev
git read-tree --reset -u dev

这个git read-tree -u命令告诉Git:用我在这里命名的提交中存储的文件替换索引和我的work-tree内容。 --reset 选项告诉 Git 抛出冲突条目,如果合并生成的合并冲突(如果没有的话应该是无害的)。所以现在索引和您的 work-tree 与 L 中的快照匹配。现在你可以完成合并了:4

git merge --continue

这使得新的合并提交 M 来自索引中存储的内容,这要归功于 git read-tree 命令的 -u 选项 - 您还可以在 work-tree。所以现在你有:

             I--J
            /    \
...--F--G--H      M   <-- master (HEAD)
            \    /
             K--L   <-- dev

提交 Mcontent 与提交 Lcontent 完全匹配。 M 第一个父 仍然是 J,第二个仍然是 L,就像任何其他合并一样。但是您已经 "killed off" 在 IJ 中所做的更改,而仅 添加 master 的提交。所有其他 Git 将很乐意 添加 提交 M 他们的 提交集合。


3这根本不是魔法。这是一个 plumbing 命令,用于脚本,而不是用户。然而,read-tree 命令复杂得惊人:它实现了(部分)合并、子树操作和各种其他聪明之处。这是索引存在的一个重要原因,否则会让人感到痛苦。

4喜欢的话可以运行git commit代替。在没有 git merge --continue 的旧 Git 版本中,您将不得不使用 git commit。直接使用 git commit 是可以的:所有 git merge --continue 所做的就是首先检查是否有合并要完成,然后 运行s git commit 为您服务。使用 git merge --continue,您检查您是否真的完成了合并。


也考虑这个选项

假设您想要合并。也就是说,假设您想保持进度:

             I--J   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

原样,但是添加一个新提交 Omaster,其 parentJ,但其 contentLdev?

上的匹配

这也很容易做到。刚刚完成 git checkout master,以便您的索引和 work-tree 匹配 J,您现在 运行:

git read-tree -u dev

(没有前面的合并,就不会有合并冲突,所以不需要 --reset)。

这会替换 Git 的索引和您的 work-tree 内容,真的从提交 L 中获取它们,就像我们在合并示例中所做的那样。现在你可以 运行 git commit 来制作 O:

             I--J--O   <-- master (HEAD)
            /
...--F--G--H
            \
             K--L   <-- dev

提交O内容现在匹配L的内容; history 通过从 master 开始——提交 O——并向后读取 O,然后 J,然后 [=27] =],然后 H,依此类推。

(你想要哪个选项?由你决定。)

如果您想在不进行真正合并的情况下进行合并,只需通过选择 "ours" 合并策略来加入历史:

git checkout dev
git merge -s ours master

你的历史将记录 dev 到 master 的合并,两个分支的历史将保持不变,但文件内容看起来 master 从未存在过(它们只反映 dev 内容)