This is normal. Not desirable, mind you, but normal—it's one reason some people avoid using git rebase entirely.
Long: Why this happens
Remember first what a Git commit is and does:
Each commit stores a snapshot of all of your files, plus some metadata: the name and email address of the person who made the commit, date-and-time stamps, and so on.
Each commit has a unique number. This number isn't a simple count—it doesn't go 1, 2, 3, etc—and is instead a big, ugly, random-looking (but not random at all) hash ID. The hash ID is how two Gits can tell whether they both have a commit, because this hash ID is computed the same way in every Git. If their Git has a commit and you don't, your Git doesn't have the commit number in its database. If your Git has a commit and they don't, your Git has the commit number (and the commit) in its database, and they don't.
Moreover, history in a Git repository is simply the set of commits. Git makes this work out by storing, in each commit, the commit number—the hash ID—of the commit's parent commit, or, for a merge commit, parent commits (plural). These are the commits that come before this commit.
If we disregard merge commits, we get simple, backwards-looking chains of commits, that we can draw like this:
... <-F <-G <-H
Here H stands in for the actual hash ID of the last commit in the chain. In H's metadata, Git has stored the actual hash ID of commit G. So by reading the contents of H, Git can find the commit number for G, which lets Git read commit G, which contains the commit number for F, and so on.
A branch name in Git simply holds the commit number—the big ugly hash ID—of the last commit in the chain. So if your branch master has the above commits, we can draw that like this:
...--F--G--H <-- master
We don't really need the backwards-pointing arrows between commits, as long as we remember that they always point backwards, because nothing inside any commit can ever be changed. That includes the hash ID of the parent commit.
Branch names, and other names like remote-tracking names, however, move. So we'll draw their arrows to remind us.
Drawing your setup
We can draw your initial situation like this:
...--F <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
Now we update our upstream/master, where there are some new commits, with their own unique hash IDs:
git checkout master
git fetch upstream
This gives us:
I--J <-- upstream/master
/
...--F <-- master (HEAD), origin/master
\
G--H <-- workbranch, origin/workbranch
The git checkout step makes sure that our current branch is master, i.e., that we're working with commit F. That's why we have the special name HEAD attached to the branch name master here.
Next, we have Git move our name master to point to the last of the new commits we just got from upstream:
git merge upstream/master
which produces:
I--J <-- master (HEAD), upstream/master
/
...--F <-- origin/master
\
G--H <-- workbranch
Note how master now points to existing commit J. The Git over at origin does not even have commits I-J yet, and our memory of its master, in our origin/master, still points to commit F.
Finally, we run:
git push origin master # note: not origin/master
This has our Git call up the Git over at origin. That's why this is origin master and not origin/master: we want to call up the Git at origin, and send commits based on our master, which is also why the last part is master and not origin/master. So we send commits I-J (which we got from upstream via upstream's master) to origin, and ask origin to set their master to point to commit J.
Assuming they obey, this is what we have locally at the end of this process:
I--J <-- master (HEAD), origin/master, upstream/master
/
...--F
\
G--H <-- workbranch, origin/workbranch
Note that no commits have changed during all of this. This entire process was about getting commits into particular Git repositories (ours, and the one at origin) from some other repository (the one at upstream), and updating our branch name (master) and the name master in the Git at origin (which our Git keeps a memory of in our origin/master).
(This is all quite confusing: it takes a long time to get used to all the repetition. I find it helps to think of each repository as a different "person": Mr Upstream knows about commits I-J, then we learn about them, then we tell Mr Origin.)
Rebase pretends to change commits
For git rebase to do its job, it has to pretend to change a commit. This is actually quite impossible. Instead, rebase takes existing commits and uses them to make new commits, that are slightly different, and therefore have a different commit number.
Let's re-draw our final situation without the up-kink after commit F. We can draw the graph however we like, as long as we can follow from names to commits and then follow the internal backwards-pointing arrows. The git log --graph command draws a graph with newer commits towards the top of the graph, but for StackOverflow, I prefer to draw mine with newer commits towards the right.
...--F--I--J <-- master (HEAD), origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
What we'd like to do is pretend we make commit G from commit J. Of course, we didn't, but git rebase can:
- use Git's detached HEAD mode to extract commit
J to the work-tree;
- use
git cherry-pick to copy commit G here;
- use
git cherry-pick again to copy commit H; and finally
- force the name
workbranch to identify the last-copied commit.
The rebase operation can hit a snag during each of the git cherry-pick steps, and it looks like yours did so once.
We start by telling Git to extract commit H and attach HEAD here. That's how git rebase will decide which commits to copy: it will look at HEAD. So we run:
git checkout workbranch
which gives us:
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch (HEAD), origin/workbranch
Again, no commits have changed, but we're now working with the files extracted from commit H.
Then we run:
git rebase master
Git now lists out the raw hash IDs of the commits that are on workbranch that are not on master. Note that master contains commits ...-F-I-J, ending at J, while workbranch contains commits ...-F-G-H, ending at H. The Fs and earlier commits get canceled out, and the I-J commits aren't on workbranch at all, so the list of commits to copy here is just G and H.
(In your case, there were more than two commits to copy, but the result should be clear enough.)
Next, because we said git rebase master, Git does a special detached-HEAD mode checkout of commit J:
...--F--I--J <-- HEAD, master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
Now Git uses git cherry-pick (or something more or less equivalent, depending on your Git vintage and the flags you pass to git rebase) to copy the changes made in commit G, to where HEAD is now. If all goes well, Git makes the new commit on its own. To remember that it is a copy of G, we'll call this G':
G' <-- HEAD
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
The rebase command proceeds to copy the remaining commits, giving:
G'-H' <-- HEAD
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
Now that all commits are successfully copied (or you've fixed them up and used git rebase --continue as needed), Git yanks the name workbranch over to point to the H' commit, and re-attaches HEAD:
G'-H' <-- workbranch (HEAD)
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- origin/workbranch
It seems as though the two existing commits somehow moved, because the new commits have the same author and time-stamp and log message and so on. What's different are the commit numbers, but who actually bothers to look at those big ugly hash IDs?
Our Git has deliberately forgotten that workbranch used to point to commit H. Instead, our workbranch now points at new-and-improved commit H'. Note, though, that our Git remembers that the Git over at origin has their workbranch remembering existing commit H.
git push
Suppose we now have our Git call up their Git, over at origin, and send commits G'-H' to them:
git push origin workbranch
They'll put G' and H' into their own repository, temporarily at least, and then consider our request to have them change their name workbranch to point to commit H'. But now, they will say no.
When we ask them, politely, to move their workbranch from their (and our) H to our (and theirs now too) H', they say no because if they did that, they would forget how to find commit H. They have no idea that H' is a new-and-improved replacement for H. They know only that if they do what we ask, they will forget H. They won't have a name by which they can still find H.
So, they say no.
git pull
If you now run git pull origin workbranch, or even just git pull with no arguments, you now have your Git call up their Git and ask them about their workbranch. They will say: Oh, sure, my workbranch, it has these two perfectly good commits G and H on it, would you like them? If your Git has thrown out your old G-H already, it takes these copies. If not—and your Git is certain to still have them, because your origin/workbranch keeps remembering them—your Git says it already has them, but thanks anyway, and now your Git knows that their workbranch points to commit H. So your Git updates your origin/workbranch, if needed (it isn't, as your origin/workbranch already still remembers H):
G'-H' <-- workbranch (HEAD)
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- origin/workbranch
and now your Git runs the second half of any git pull.
The git pull command really consists of running two Git commands:
The first command is always git fetch. That's the step that has your Git call up their Git and ask them about their workbranch (and maybe their other branches too, depending on how you run git fetch). This step brings over any commits they have, that you don't, that your Git will need. Then your Git updates your origin/* names wherever that's necessary.
The second command defaults to git merge. The merge runs on whatever commit they said is their branch's last commit.
So here, your Git runs git merge on the hash ID of commit H—their workbranch, which is your origin/workbranch. So your Git now merges your commit H' with the shared commit H:
G'-H'-M <-- workbranch (HEAD)
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H <-- origin/workbranch
Those copies you made, to improve and discard the old G-H, are still there. The old G-H are still there too. New merge commit M brings both branches together. The fact that one branch consists of commits you thought you'd gotten rid of isn't important. The commits still exist, and the merge merges them.
git push, again
Your Git can now send their Git commit M, the new merge. If they make their workbranch identify commit M, their existing commits G-H remain accessible in their repository, so they are satisfied with that. But at this point, you have duplicates of all the commits your git rebase copied (and now they will too). This isn't what you wanted at all.
Note: a successful git push will update your origin/workbranch to remember the fact that their workbranch now remembers commit M, so now the drawing looks like this:
G'-H'-M <-- workbranch (HEAD), origin/workbranch
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H
(We could probably simplify this by moving the G-H line to the top of the drawing, but let's not do that.)
To fix the problem, you must do the following:
- Force your Git to pretend you never made merge commit
M at all.
- Force the Git over at
origin to set his workbranch name to remember commit H' instead of M.
The easiest set of Git commands to do this at this point are git reset --hard and git push --force. Let's see how those work.
git reset --hard
We'll start by making our Git forget our commit M:
git checkout workbranch # if needed - we're probably already there
This ensures that we have:
G'-H'-M <-- workbranch (HEAD), origin/workbranch
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H
in our repository now. Then:
git reset --hard HEAD~1
The HEAD~1 notation means move back one first-parent, from commit M to commit H'. That makes our name workbranch point to commit H'. To draw this, let's move commit M down to the bottom row:
G'-H' <-- workbranch (HEAD)
/ \
...--F--I--J <- \ -- master, origin/master, upstream/master
\ \
G-------H----M <-- origin/workbranch
Now that our workbranch identifies commit H', we run:
git push --force origin workbranch
This has our Git call up their Git, over at origin, tell it about commit H'—they already have it of course, at this point—and then command them, forcefully: Set your branch name workbranch to point to commit H'! (This is from --force, and takes the place of the usual polite request.)
Assuming they obey—that part is up to them, but if you have control over this repository, just make sure you give yourself force-push permissions—they will move their workbranch to point to commit H', and your Git will update your origin/workbranch accordingly:
G'-H' <-- workbranch (HEAD), origin/workbranch
/ \
...--F--I--J <- \ -- master, origin/master, upstream/master
\ \
G-------H----M [abandoned]
Now that they, and you, don't have a name by which to find commit M, you won't even see it. All will be as if it never existed:
G'-H' <-- workbranch (HEAD), origin/workbranch
/
...--F--I--J <-- master, origin/master, upstream/master
Again, this is a normal thing with rebase
The issue with rebasing is that it works by copying commits to new-and-improved commits.
The issue with Git in general is that it is loath to give up commits. It wants to add commits, not remove them in favor of new and improved commits.
Every Git repository will easily add new commits. It won't so easily forget an old one. So to send this particular commit to origin, when origin remembers your old commit H instead of your new-and-improved H', you must force the push. You can use --force-with-lease, which adds a kind of safety check, that their workbranch still remembers H and not some other commit.
If there are other users of the origin Git repository, remember that they, too, might be using or adding on to origin's workbranch. You should make sure that all of these users expect commits to be removed and replaced. Otherwise those other users will be surprised by this kind of action.
Avoiding rebase entirely avoids this kind of surprise, but ultimately it is really up to you and anyone you work with. If you all agree that rebases happen—that some commits can go away, and you won't bring them back if they're supposed to stay gone—then you can work this way.