Skip to content

Instantly share code, notes, and snippets.

@andreaswachowski
Last active September 22, 2021 15:52
Show Gist options
  • Save andreaswachowski/25d2347bfe12c6012b6ecfb0f48881de to your computer and use it in GitHub Desktop.
Save andreaswachowski/25d2347bfe12c6012b6ecfb0f48881de to your computer and use it in GitHub Desktop.
How duplicate commits in Git happen

Before we start: Note the log-output is generated with the following alias:

l = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%ai)%Creset'

Start with a main branch

* f57c841 - (HEAD -> main) Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)

Push main branch to upstream repository

* f57c841 - (HEAD -> main, origin/main) Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)

Fork a feature branch, push it to the upstream repo

* 9edf7bd - (HEAD -> feature, origin/feature) 2nd feature-branch commit (2021-09-12 10:34:12 +0200)
* e95218c - 1st feature-branch commit (2021-09-12 10:33:56 +0200)
* f57c841 - (origin/main, main) Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)

Meanwhile, development on main continues ...

git checkout main; git fetch

* f57c841 - (HEAD -> main) Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)

Note that origin/main is not pointing to the second commit anymore. Where is it?

git checkout feature; git checkout main

Switched to branch 'main'
Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)

git pull

* 2e1f178 - (HEAD -> main, origin/main) Fourth commit (2021-09-12 10:37:57 +0200)
* 07e2136 - Third commit (2021-09-12 10:37:40 +0200)
* f57c841 - Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)

The pull changed the main pointer from the second to the fourth commit.

Back to the feature branch with git checkout feature ...

* 9edf7bd - (HEAD -> feature, origin/feature) 2nd feature-branch commit (2021-09-12 10:34:12 +0200)
* e95218c - 1st feature-branch commit (2021-09-12 10:33:56 +0200)
* f57c841 - Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)

Now we rebase the feature branch against main.

In detail, this means (the following is largely cited from the git-rebase man page): All changes made by commits in the current branch but that are not in main are saved to a temporary area. This is the same set of commits that would be shown by git log main..HEAD (and here, means all feature-branch commits):

* 9edf7bd - (HEAD -> feature, origin/feature) 2nd feature-branch commit (2021-09-12 10:34:12 +0200)
* e95218c - 1st feature-branch commit (2021-09-12 10:33:56 +0200)

The current branch is reset to main (that is, it will point to the "Fourth commit"). The commits that were previously saved into the temporary area are then reapplied to the current branch, one by one, in order.

Let's go:

git rebase main

* a7f06fa - (HEAD -> feature) 2nd feature-branch commit (2021-09-12 10:34:12 +0200)
* 8f87739 - 1st feature-branch commit (2021-09-12 10:33:56 +0200)
* 2e1f178 - (origin/main, main) Fourth commit (2021-09-12 10:37:57 +0200)
* 07e2136 - Third commit (2021-09-12 10:37:40 +0200)
* f57c841 - Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)

Note that, because they were re-applied, the feature-branch-commits now have different hashes than before. Also note that the upstream feature-branch origin/featureis not shown anymore - it still points to the original, unrebased commit 9edf7bd.

We might have had to resolve conflicts during the rebase, similarly as when we had git merge main, but after doing so, we have exactly the code that we want on our feature branch.

So let's push the changes:

10:49:38 $ git push
To github.com:andreaswachowski/testrepo2.git
 ! [rejected]        feature -> feature (non-fast-forward)
error: failed to push some refs to 'github.com:andreaswachowski/testrepo2.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.

We cannot push because the local and upstream feature branches diverged. Naturally so, because we rewrote the feature branch history with the rebase.

The message "Integrate the remote changes (e.g. 'git pull ...') ..." is misleading: At this point, provided we work alone on this branch (!), we want to git push --force-with-lease, overwriting the remote upstream. This is fine here we don't lose anything (except the outdated, feature-branch commits that were based on the 2nd main-commit).

When we switch to main and back to feature, we see

10:56:18 $ git co -
Switched to branch 'feature'
Your branch and 'origin/feature' have diverged,
and have 4 and 2 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

The branches have diverged alright, but although technically correct, the phrasing "different commits" is misleading here. Let's see what happens when we pull:

git pull opens an editor to enter a merge commit message, and the result is:

*   15fb3fb - (HEAD -> feature) Merge branch 'feature' of github.com:andreaswachowski/testrepo2 into feature (2021-09-12 10:58:11 +0200)
|\
| * 9edf7bd - (origin/feature) 2nd feature-branch commit (2021-09-12 10:34:12 +0200)
| * e95218c - 1st feature-branch commit (2021-09-12 10:33:56 +0200)
* | a7f06fa - 2nd feature-branch commit (2021-09-12 10:34:12 +0200)
* | 8f87739 - 1st feature-branch commit (2021-09-12 10:33:56 +0200)
* | 2e1f178 - (origin/main, main) Fourth commit (2021-09-12 10:37:57 +0200)
* | 07e2136 - Third commit (2021-09-12 10:37:40 +0200)
|/
* f57c841 - Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)

The merge unites the local branch with the remote branch, by adding the two commits from the upstream feature-branch (e95218c and 9edf7bd) on top. Now our feature-branch commits appear twice (each)!

Not only is this very confusing. The "Merge branch feature ... into feature" message is also confusing. And what's worse, if we had to resolve any conflicts, we might have to resolve them again during the merge.

The problem here starts with the fact that we rebase commits that we had already pushed.

Either use git merge main instead of git rebase. This should be the course of action when working with multiple developers on that branch.

Or use git push --force-with-lease after the rebase, but only ever (EVER!) do this when you are working alone on the branch (yes we could discuss this in more detail, but just don't go there, it gets too complicated and error-prone).

Lastly: The behaviour is different when having configured git config pull.rebase true. In my experience, this is a helpful option, I have it configured by default (with git config --global pull.rebase true). But it doesn't change the lessons above.

References

I have not yet studied the following in detail, but there is a wealth of information on this topic:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment