执行 rebase 后,Git 提交会在同一分支中重复

发布于 2025-01-04 22:26:57 字数 1875 浏览 1 评论 0 原文

我了解 Pro Git 中关于变基的危险。作者基本上告诉你如何避免重复提交:

不要对已推送到公共存储库的提交进行变基。

我将告诉你我的特殊情况,因为我认为它并不完全适合 Pro Git 场景,而且我仍然会出现重复提交。

假设我有两个远程分支及其本地对应分支:

origin/master    origin/dev
|                |
master           dev

所有四个分支都包含相同的提交,我将在 dev 中开始开发:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

经过几次提交后,我将更改推送到 origin /dev

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

我必须返回 master 进行快速修复:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

然后返回 dev 我重新调整更改以将快速修复包含在我的实际中开发:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

如果我显示提交的历史记录GitX/gitk 我注意到 origin/dev 现在包含两个相同的提交 C5'C6',它们与 Git 不同。现在,如果我将更改推送到 origin/dev 这就是结果:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

也许我没有完全理解 Pro Git 中的解释,所以我想知道两件事:

  1. 为什么 Git 会重复这些提交变基时?是否有特殊原因要这样做,而不是在 C7 之后应用 C5C6
  2. 我怎样才能避免这种情况?这样做明智吗?

I understand the scenario presented in Pro Git about The Perils of Rebasing. The author basically tells you how to avoid duplicated commits:

Do not rebase commits that you have pushed to a public repository.

I am going to tell you my particular situation because I think it does not exactly fit the Pro Git scenario and I still end up with duplicated commits.

Let's say I have two remote branches with their local counterparts:

origin/master    origin/dev
|                |
master           dev

All four branches contains the same commits and I am going to start development in dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

After a couple of commits I push the changes to origin/dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

I have to go back to master to make a quick fix:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

And back to dev I rebase the changes to include the quick fix in my actual development:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

If I display the history of commits with GitX/gitk I notice that origin/dev now contains two identical commits C5' and C6' which are different to Git. Now if I push the changes to origin/dev this is the result:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

Maybe I don't fully understand the explanation in Pro Git, so I would like to know two things:

  1. Why does Git duplicate these commits while rebasing? Is there a particular reason to do that instead of just applying C5 and C6 after C7?
  2. How can I avoid that? Would it be wise to do it?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

∞梦里开花 2025-01-11 22:26:57

简短回答

您忽略了运行 git push 的事实,出现以下错误,然后继续运行 git pull :

To [email protected]:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

尽管 Git 试图提供帮助,但它的“git pull”建议很可能不是您想要做的。

如果您:

  • 单独处理“功能分支”或“开发人员分支”,那么您可以运行 git push --force 使用 post-rebase 更新远程提交(根据 user4405677 的回答)。
  • 同时与多个开发人员一起在一个分支上工作,那么您可能首先不应该使用git rebase。要使用 master 的更改来更新 dev,您应该运行 git merge mastergit rebase master dev > 在 dev 上(根据 Justin 的回答)。

稍微长一点的解释

Git 中的每个提交哈希都基于许多因素,其中之一是它之前的提交的哈希。

如果您重新排序提交,您将更改提交哈希值;变基(当它执行某些操作时)将更改提交哈希值。这样,运行 git rebase master dev 的结果(其中 devmaster 不同步)将创建 提交(并因此散列)与 dev 上的内容相同,但在 master 上插入了它们之前的提交。

您可能会以多种方式陷入这种情况。我能想到的两种方法:

  • 您可以在 master 上进行提交,您希望将其作为您的 dev 工作的基础
  • 您可以在 dev 上进行提交已经被推送到远程,然后您可以继续更改(重写提交消息、重新排序提交、压缩提交等)。

让我们更好地理解发生了什么 - 这是一个示例:

您有一个存储库:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

存储库中的初始线性提交集

然后您可以继续更改提交。

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(在这里,您必须相信我的话:在 Git 中,有多种方法可以更改提交。在本例中,我更改了 C3 的时间,但您要插入新的提交,更改提交消息,重新排序提交,将提交压缩在一起等)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

The same commits with new hashes

这就是它所在的位置重要的是要注意提交哈希值不同。这是预期的行为,因为您已经更改了有关它们的某些内容(任何内容)。这没关系,但是:

A graph log shown that master is out-of-sync with the remote

尝试推送将显示出现错误(并提示您应该运行 git pull )。

$ git push origin master
To [email protected]:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

如果我们运行 git pull,我们会看到以下日志:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

或者,以另一种方式显示:

A graph log shown a merge commit

现在我们在本地有重复的提交。如果我们要运行 git Push,我们会将它们发送到服务器。

为了避免进入这个阶段,我们可以运行git push --force(我们改为运行git pull)。这将毫无问题地将我们的带有新哈希值的提交发送到服务器。要解决此阶段的问题,我们可以重置回运行 git pull 之前的状态:

查看 reflog (git reflog) 以查看提交哈希值我们运行git pull之前。

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

上面我们看到 ba7688a 是我们在运行 git pull 之前所处的提交。有了提交哈希,我们就可以重置回该值(git reset --hard ba7688a),然后运行git push --force

我们就完成了。

但是等等,我继续基于重复的提交进行工作

如果您不知何故没有注意到提交是重复的并继续在重复的提交之上继续工作,那么您真的把自己搞得一团糟。混乱的大小与重复项之上的提交数量成正比。

它看起来像什么:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

或者,以另一种方式显示:

显示重复提交之上的线性提交的日志图

在这种情况下,我们想要删除重复提交,但保留基于它们的提交 - 我们想要保留 C6 到 C10。与大多数事情一样,有多种方法可以解决此问题:

要么:

  • 在最后一次重复的提交1处创建一个新分支,cherry-pick 每次提交(C6 到 C10)到该新分支,并将该新分支视为规范分支。
  • 或者运行 git rebase --interactive $commit,其中 $commit 是重复提交2之前的提交>。在这里我们可以彻底删除重复的行。

1 无论您选择两者中的哪一个,ba7688a2a2e220 都可以正常工作。

2 在本例中,它将是 85f59ab

TL;DR

advice.pushNonFastForward 设置为

git config --global advice.pushNonFastForward false

Short answer

You omitted the fact that you ran git push, got the following error, and then proceeded to run git pull:

To [email protected]:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Despite Git trying to be helpful, its 'git pull' advice is most likely not what you want to do.

If you are:

  • Working on a "feature branch" or "developer branch" alone, then you can run git push --force to update the remote with your post-rebase commits (as per user4405677's answer).
  • Working on a branch with multiple developers at the same time, then you probably should not be using git rebase in the first place. To update dev with changes from master, you should, instead of running git rebase master dev, run git merge master whilst on dev (as per Justin's answer).

A slightly longer explanation

Each commit hash in Git is based on a number of factors, one of which is the hash of the commit that comes before it.

If you reorder commits you will change commit hashes; rebasing (when it does something) will change commit hashes. With that, the result of running git rebase master dev, where dev is out of sync with master, will create new commits (and thus hashes) with the same content as those on dev but with the commits on master inserted before them.

You can end up in a situation like this in multiple ways. Two ways I can think of:

  • You could have commits on master that you want to base your dev work on
  • You could have commits on dev that have already been pushed to a remote, which you then proceed to change (reword commit messages, reorder commits, squash commits, etc.)

Let's better understand what happened—here is an example:

You have a repository:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

Initial set of linear commits in a repository

You then proceed to change commits.

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(This is where you'll have to take my word for it: there are a number of ways to change commits in Git. In this example I changed the time of C3, but you be inserting new commits, changing commit messages, reordering commits, squashing commits together, etc.)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

The same commits with new hashes

This is where it is important to notice that the commit hashes are different. This is expected behaviour since you have changed something (anything) about them. This is okay, BUT:

A graph log showing that master is out-of-sync with the remote

Trying to push will show you an error (and hint that you should run git pull).

$ git push origin master
To [email protected]:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

If we run git pull, we see this log:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Or, shown another way:

A graph log showing a merge commit

And now we have duplicate commits locally. If we were to run git push we would send them up to the server.

To avoid getting to this stage, we could have run git push --force (where we instead ran git pull). This would have sent our commits with the new hashes to the server without issue. To fix the issue at this stage, we can reset back to before we ran git pull:

Look at the reflog (git reflog) to see what the commit hash was before we ran git pull.

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

Above we see that ba7688a was the commit we were at before running git pull. With that commit hash in hand we can reset back to that (git reset --hard ba7688a) and then run git push --force.

And we're done.

But wait, I continued to base work off of the duplicated commits

If you somehow didn't notice that the commits were duplicated and proceeded to continue working atop of duplicate commits, you've really made a mess for yourself. The size of the mess is proportional to the number of commits you have atop of the duplicates.

What this looks like:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Git log showing linear commits atop duplicated commits

Or, shown another way:

A log graph showing linear commits atop duplicated commits

In this scenario we want to remove the duplicate commits, but keep the commits that we have based on them—we want to keep C6 through C10. As with most things, there are a number of ways to go about this:

Either:

  • Create a new branch at the last duplicated commit1, cherry-pick each commit (C6 through C10 inclusive) onto that new branch, and treat that new branch as canonical.
  • Or run git rebase --interactive $commit, where $commit is the commit prior to both the duplicated commits2. Here we can outright delete the lines for the duplicates.

1 It doesn't matter which of the two you choose, either ba7688a or 2a2e220 work fine.

2 In the example it would be 85f59ab.

TL;DR

Set advice.pushNonFastForward to false:

git config --global advice.pushNonFastForward false
嘿咻 2025-01-11 22:26:57

您不应该在这里使用变基,简单的合并就足够了。您链接的 Pro Git 书基本上解释了这种确切的情况。内部工作原理可能略有不同,但我是这样想象的:

  • C5C6 暂时从 dev
  • C7< /code> 应用于 dev
  • C5C6C7 之上播放,创建新的差异并因此新的提交

所以,在你的 dev 中分支,C5C6 实际上不再存在:它们现在是 C5'C6'。当您推送到 origin/dev 时,git 将 C5'C6' 视为新提交,并将它们添加到历史记录的末尾。事实上,如果您在 origin/dev 中查看 C5C5' 之间的差异,您会发现虽然内容是相同的,行号可能不同——这使得提交的哈希值不同。

我将重申 Pro Git 规则:永远不要对除本地存储库之外任何地方都存在的提交进行变基。请改用合并。

You should not be using rebase here, a simple merge will suffice. The Pro Git book that you linked basically explains this exact situation. The inner workings might be slightly different, but here's how I visualize it:

  • C5 and C6 are temporarily pulled out of dev
  • C7 is applied to dev
  • C5 and C6 are played back on top of C7, creating new diffs and therefore new commits

So, in your dev branch, C5 and C6 effectively no longer exist: they are now C5' and C6'. When you push to origin/dev, git sees C5' and C6' as new commits and tacks them on to the end of the history. Indeed, if you look at the differences between C5 and C5' in origin/dev, you'll notice that though the content is the same, the line numbers are probably different -- which makes the hash of the commit different.

I'll restate the Pro Git rule: never rebase commits that have ever existed anywhere but your local repository. Use merge instead.

帅气尐潴 2025-01-11 22:26:57

我认为您在描述步骤时跳过了一个重要细节。更具体地说,您的最后一步,git push on dev,实际上会给您一个错误,因为您通常无法推送非快进更改。

因此,您在上次推送之前执行了 git pull ,这导致了以 C6 和 C6' 作为父级的合并提交,这就是为什么两者都将保留在日志中列出的原因。更漂亮的日志格式可能会让它们更明显地显示它们是重复提交的合并分支。

或者你做了一个 git pull --rebase (或者没有显式的 --rebase ,如果它是由你的配置暗示的),它将原始的 C5 和 C6 拉回你的本地开发(并进一步将以下内容重新重新设置为新的哈希值,C7' C5'' C6'')。

解决这个问题的一种方法可能是 git push -f 在出现错误时强制推送并从原点擦除 C5 C6,但如果其他人在你擦除它们之前也拉了它们,那么你'将会遇到更多麻烦...基本上每个拥有 C5 C6 的人都需要执行特殊步骤来摆脱它们。这正是为什么他们说你永远不应该对已经发布的任何内容进行 rebase。不过,如果所说的“发布”是在一个小团队内进行的话,这仍然是可行的。

I think you skipped an important detail when describing your steps. More specifically, your last step, git push on dev, would have actually given you an error, as you can not normally push non-fastforward changes.

So you did git pull before the last push, which resulted in a merge commit with C6 and C6' as parents, which is why both will remain listed in log. A prettier log format might have made it more obvious they are merged branches of duplicated commits.

Or you made a git pull --rebase (or without explicit --rebase if it is implied by your config) instead, which pulled the original C5 and C6 back in your local dev (and further re-rebased the following ones to new hashes, C7' C5'' C6'').

One way out of this could have been git push -f to force the push when it gave the error and wipe C5 C6 from origin, but if anyone else also had them pulled before you wiped them, you'd be in for a whole lot more trouble... basically everyone that has C5 C6 would need to do special steps to get rid of them. Which is exactly why they say you should never rebase anything that's already published. It's still doable if said "publishing" is within a small team, though.

初熏 2025-01-11 22:26:57

我发现就我而言,这个问题是 Git 配置问题的结果。 (涉及拉取和合并)

问题描述:

症状: rebase 后在子分支上重复提交,这意味着在 rebase 期间和之后进行了多次合并。

工作流程:
以下是我正在执行的工作流程的步骤:

  • 处理“功能分支”(“开发分支”的子分支),
  • 提交并推送对“功能分支”的更改
  • 签出“开发分支”(功能的母分支)和与它一起工作。
  • 提交并推送“开发分支”上的更改
  • 签出“功能分支”并从存储库中提取更改(如果其他人已提交工作)
  • 将“功能分支”重新设置为“开发分支”
  • 推动“功能分支”上的更改分支”

作为此工作流程的后果,自上次变基以来“功能分支”的所有提交都重复... :-(

问题是由于之前子分支的更改引起的rebase。 Git 默认的 pull 配置是“merge”。这是更改在子分支上执行的提交的索引。

解决方案:在 Git 配置文件中,将 pull 配置为在 rebase 模式下工作:

...
[pull]
    rebase = preserve
...

希望它能有所帮助。
JN格克斯

I found out that in my case, this issue the consequence of a Git configuration problem. (Involving pull and merge)

Description of the problem:

Sympthoms: Commits duplicated on child branch after rebase, implying numerous merges during and after rebase.

Workflow:
Here are steps of the workflow I was performing:

  • Work on the "Features-branch" (child of "Develop-branch")
  • Commit and Push changes on "Features-branch"
  • Checkout "Develop-branch" (Mother branch of Features) and work with it.
  • Commit and push changes on "Develop-branch"
  • Checkout "Features-branch" and pull changes from repository (In case someone else has commited work)
  • Rebase "Features-branch" onto "Develop-branch"
  • Push force of changes on "Feature-branch"

As conséquences of this workflow, duplication of all commits of "Feature-branch" since previous rebase... :-(

The issue was due to the pull of changes of child branch before rebase. Git default pull configuration is "merge". This is changing indexes of commits performed on the child branch.

The solution: in Git configuration file, configure pull to work in rebase mode:

...
[pull]
    rebase = preserve
...

Hope it can help
JN Grx

心欲静而疯不止 2025-01-11 22:26:57

您可能从与当前分支不同的远程分支中拉取。例如,当您的分支正在开发跟踪开发时,您可能已经从 Master 中拉取了。如果从非跟踪分支拉取重复提交,Git 将尽职尽责地拉取重复提交。

如果发生这种情况,您可以执行以下操作:

git reset --hard HEAD~n

where n == <不应该存在的重复提交数。>

然后确保您从正确的分支拉取,然后运行:

git pull upstream <correct remote branch> --rebase

使用 --rebase 进行拉取将确保您不会添加无关的提交,这可能会混淆提交历史记录。

这里是 git rebase 的一些指导。

You may have pulled from a remote branch different from your current. For example you may have pulled from Master when your branch is develop tracking develop. Git will dutifully pull in duplicate commits if pulled from a non-tracked branch.

If this happens, you can do the following:

git reset --hard HEAD~n

where n == <number of duplicate commits that shouldn't be there.>

Then make sure you are pulling from the correct branch and then run:

git pull upstream <correct remote branch> --rebase

Pulling with --rebase will ensure you aren't adding extraneous commits which could muddy up the commit history.

Here is a bit of hand holding for git rebase.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文