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'
* f57c841 - (HEAD -> main) Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)
* f57c841 - (HEAD -> main, origin/main) Second commit (2021-09-12 10:30:47 +0200)
* 39e045e - Initial commit (2021-09-12 10:30:10 +0200)
* 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)
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/feature
is 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.
I have not yet studied the following in detail, but there is a wealth of information on this topic: