Git 中的 Merging、Stashing 和 Rebasing 在概念上有什么区别?

What are the conceptual differences between Merging, Stashing and Rebasing in Git?

我一直在 master 分支上大量使用 Merging。但是最近在我的情况下进行功能开发合并对于项目历史来说似乎很复杂。我遇到了 Rebasing,它解决了我的问题。我在解决问题时也遇到了the golden rule of rebasing

我有时也使用 Stashing,它有效,但我觉得同样的事情也可以通过合并来实现。

虽然我使用了这些命令,但我觉得如果有人能解释一下这三个命令在概念上的突出之处facts/rule,将有助于我更清楚地理解。谢谢

存储非常不同,因为它实质上是将您的更改放在一边以备后用。如果您正处于某件事的中间并且必须跳到其他事情并切换分支,这很有用。

合并和变基最终实现的是同一件事——将更改合并到一个分支中。

  • Rebase 提供了可以说是 "cleaner" 的历史记录,并且内联解决了冲突。
  • 通过合并,冲突在合并提交中得到解决。
  • 重新建立基础的分支在完成后仍会合并回主分支。

不同之处在于,有效地解决内联冲突会使冲突从未发生过,因为您正在再次编辑文件以合并导致冲突的更改。

这在您稍后回顾更改时会很有用。

正如您提到的,需要注意的是,重写历史会导致很难或不可能与该特定分支上的其他人协作。


从像 gitg 这样的工具查看图形或视觉表示时,可能很难理解来自长期存在的功能分支的一系列合并。

有了变基,更容易直观地跟踪变化。当您使用 git-bisect 等工具查找 bug 的原始提交时,它也可以提供帮助,因为分支更容易遍历。

选择哪个取决于几件事。

就我个人而言,如果我在一个我独自工作的短期功能分支上,我会经常变基和变基。否则就是合并。


有一种情况你可能别无选择变基 - 如果你从错误的点开始你的分支。例如,您可能已经检查了一个特定的功能并开始处理其他事情。在它所基于的功能之前,可能需要合并和部署其他东西。此时,将您的更改重新定位到不同的分支将使您能够独立于其他分支发布该功能。


当你将一个变基分支合并回主分支时,这仍然是一个合并,即使历史是线性的。您可能会在 git 中看到一条消息,告诉您它进行了 "fast forward" 合并。这意味着它只是将引用 "master" 移动到它的新位置。你可以告诉 git 不要这样做,并且无论如何创建一个合并提交,在 git 合并命令上使用 --no-ff 标志。

线性历史:

* HEAD, master, your_branch: your last commit
|
*
|
*
|
*
|
*
|
* previous master: where master was when you rebased

使用 no-ff 合并提交重新定位历史记录:

* HEAD: merge branch your_branch into master
| \
|  * your_branch the last commit in your branch
|  |
|  * 
|  |
|  *
|  |
|  *
|/
* previous master: the starting point of your branch

假设您有这个存储库。 A、B 和 C 是提交。 master 在 C.

A - B - C [master]

您创建了一个名为 feature 的分支。它指向 C.

A - B - C [master]
          [feature]

你在 masterfeature 上做了一些工作。

A - B - C - D - E - F [master]
         \
          G - H - I [feature]

您想用 master 中的更改更新 feature。您可以将 master 合并到 feature,导致合并提交 J.

A - B - C - D - E - F [master]
         \           \
          G - H - I - J [feature]

如果你这样做的次数足够多,事情就会开始变得混乱。

A - B - C - D - E - F - K - L - O - P - Q [master]
         \           \       \       \ 
          G - H - I - J - M - N - Q - R - S [feature]

这可能看起来很简单,但那是因为我是那样画的。 Git 历史是 a graph (in the computer science sense) 并且没有任何说明必须这样绘制。而且 明确地 没有说什么,例如,提交 M 是分支功能的一部分。你必须从图表中弄清楚这一点,有时这会变得一团糟。

当您决定完成并将 feature 合并到 master 时,事情变得一团糟。

A - B - C - D - E - F - K - L - O - P - Q - T [master]
         \           \       \       \     /
          G - H - I - J - M - N - Q - R - S [feature]

现在很难说 M 原来是 feature 的一部分。同样,我选择了一种很好的绘制方式,但 Git 不一定知道这样做。 M 是 master 和 feature 的祖先。这使得很难解释历史并弄清楚在哪个分支中做了什么。它还可能导致不必要的合并冲突。


让我们重新开始并重新设置基准。

A - B - C - D - E - F [master]
         \
          G - H - I [feature]

将一个分支变基到另一个分支在概念上就像将该分支移动到另一个分支的尖端。将 feature 变基到 master 是这样的:

                      G1 - H1 - I1 [feature]
                     /
A - B - C - D - E - F [master]
         \
          G - H - I

feature 中的每个提交都在 master 之上重播。这就好像你把 C 和 G 之间的差异应用到 F 上,然后称之为 G1。然后将 G 和 H 之间的差异应用于 G1,即 H1。等等。

没有合并提交。就好像你一直在 master 之上写 feature 分支一样。这保持了一个很好的、干净的、线性的历史,没有乱七八糟的合并提交,什么也没有告诉你。

请注意,旧的功能分支仍然存在。只是没有任何东西指向它,它最终会被垃圾收集。这是为了向您展示 rebase 不会重写历史记录;相反,rebase 会创建新的历史记录 然后我们假装一直都是这样。这很重要,原因有二:

首先,如果你搞砸了一个变基,旧的分支仍然存在。您可以使用 git reflog 或使用 ORIG_HEAD.

找到它

其次,也是最重要的一点,rebase 会产生新的提交 ID。 Git 中的所有内容都通过 ID 工作。这就是为什么如果你变基一个共享分支,它会引入复杂性。

关于变基与合并还有很多要说的,所以我就此结束:

  • 要更新分支,请使用 rebase。这避免了混乱的中间合并。
  • 要完成分支...
    • 使用rebase更新它。
    • 然后使用 merge --no-ff 强制创建合并提交。
    • 然后删除功能分支,再也不用了。

您希望在历史记录中看到的最终结果是 "feature bubble"。

                      G1 - H1 - I1
                     /            \
A - B - C - D - E - F ------------ J [master]

这使历史保持线性,同时仍然为代码考古学家提供重要背景,即 G1、H1 和 I1 作为分支的一部分完成,应该一起检查。


存储是完全不同的东西。它基本上是一个特殊的分支来存储补丁。

有时您正在做某事,还没有准备好提交,但您需要做一些其他工作。你可以把它放在 git diff > some.patch 的补丁文件中,重置你的工作目录,做其他工作,提交它,然后应用 some.patch。或者您可以 git stash save 和以后的 git stash pop.

What is the conceptual difference between Merging, Stashing and Rebasing in Git?

我假设你的意思是 "what are these three features used for",而不是你似乎知道的实际技术差异。

正在合并

合并意味着您采用两个分支并最终得到一个包含原始分支中所有内容的分支。这意味着合并在概念上与分支相反。您主要将它用于 "remove" 分支 - 通过将它合并回它的来源。合并表示您不打算向该分支添加更多提交。

重新定位

变基意味着您更改现有分支的来源。从概念上讲,这意味着您将分支保留为它自己的发展路径,您只是模拟您在另一个时间点创建它。

再次从概念上讲,这很有用的原因是,对于大量分支(例如,功能分支),您不关心创建它们的确切时间点。你关心的是他们是 "delta" 到 master,无论 master 现在 。因此,您想始终将它们从 current master 分支出来,只要 master 发生变化,就将它们重新定位到它上面。

合并还是变基?

这些概念意味着什么时候使用哪个是非常明显的。如果你保留分支(作为它自己的实体,即,因为你继续在它上面开发),然后变基。如果您希望分支消失(概念上)并且不想向其添加另一个提交,则合并。

这也意味着他们之间没有一个比另一个更好或更差。它们都是工具,都是用来使用的。两者都可能被滥用;如果不假思索地使用,两者都会对您的存储库造成严重破坏。

收藏

存储与合并或变基完全无关。将其视为您为自己做的本地事情;你很快就把你所做的任何改变都抹掉,处理其他事情,然后恢复之前的状态。它在概念上类似于在其他地方克隆您的存储库的干净副本,在那里工作,然后返回到您以前的存储库。

简而言之,让我们用图表来说明差异。 假设您的 git 日志如下所示,

  A---B---C dev
 /
D---E---F---G   main

git merge dev,合并分支dev到main后,主分支上多了一个commit H

  A---B---C  dev                                  merge
 /         \
D---E---F---G---H  main

git rebase main,所有提交(A、B 和 C)都在 main 上重播,提交历史看起来是线性的

D---E---F---G---H---A`---B`---C`  main            rebase

git stash,就是当你在修改文件的时候,假设你想切换分支,但又不想提交修改后的文件。所以可以把它藏起来,做完其他工作后,你可以切换回你工作的分支,并使用git stash apply所以工作目录是为你准备修改的文件。