Rebasing will typically produce this result—this need to use --force
—because rebasing replaces some existing commit(s) with new-and-improved1 commits. To really understand how this works, you need to understand how Git uses and finds commits, and how git push
and other commands work. It's a little bit tricky! First, take a look at my long answer to How to delete all unpushed commits without deleting local changes, to get an idea of what a drawing like:
...--G--H <-- master
\
I <-- feature (HEAD)
might mean. In particular, you need to remember how these letters stand in for the raw hash IDs, how each commit points backwards to its parent commit, and how a branch name points to the latest commit on / contained-in that branch.
1At least, we hope they're improved.
Setting up
Now let's suppose we have a series of commits that's not flawed per se—we don't really need to fix anything in them—but which were made earlier, like this:
...--G--H <-- master
\
I--J <-- feature
(no attached HEAD
indicates that we don't care which one was checked out before this point). We run git checkout master
or git switch
master, then git pull
or similar, and acquire a new master
commit, giving us this:
...--G--H--K <-- master (HEAD), origin/master
\
I--J <-- feature, origin/feature
We also add or update these remote-tracking names, origin/master
and origin/feature
. They are our Git's memory of some other Git's branch names. Our name origin/master
identifies commit K
, as does our own branch name master
now; and our name origin/feature
tells us that over on origin
, they have a copy of our branch name feature
that identifies commit J
too, just like our feature
. (Perhaps they got it when, earlier, we ran git push origin feature
.)
This next part is important: The commit hash IDs—the big ugly strings of letters and digits that these uppercase letters stand in for—are the same across both repositories. The branch names need not be, although in this particular case, they are as well, right now.
Rebase works by copying commits
In this setup, we decide that the flaw with our feature is that it's based on commit H
, when the latest commit is now commit K
. We'd like to have our feature
branch based on commit K
. To do that, we run:
git switch feature # or git checkout feature
giving us:
...--G--H--K <-- master, origin/master
\
I--J <-- feature (HEAD), origin/feature
followed by:
git rebase master
The rebase command lists out the raw hash IDs of those commits that are on branch feature
but are not on master
. In this case, that is the hash IDs of commits I
and J
. (Note that commits H
and earlier are on both branches.) Then, Git uses its special "detached HEAD" mode to start working with commit K
, at the tip of master
:
...--G--H--K <-- HEAD, master, origin/master
\
I--J <-- feature, origin/feature
Git applies what we did in commit I
and makes a new commit from it. This new commit has a new, different hash ID, but re-uses the author name and date-and-time-stamps from I
, and re-uses the commit message from I
, so that the commit looks an awful lot like commit I
. In other words, it's a copy of commit I
.2 We'll call this new copy I'
:
I' <-- HEAD
/
...--G--H--K <-- master, origin/master
\
I--J <-- feature, origin/feature
Having successfully copied I
to I'
, Git now copies J
the same way, resulting in:
I'-J' <-- HEAD
/
...--G--H--K <-- master, origin/master
\
I--J <-- feature, origin/feature
The copying process is now done—there are no more commits to copy—so rebase executes its final step, which is to yank the name feature
off the commit it used to name and make it point to the last-copied commit, in this case J'
:
I'-J' <-- feature (HEAD)
/
...--G--H--K <-- master, origin/master
\
I--J <-- origin/feature
As the drawing implies, in this last step, Git re-attaches HEAD
so that we're back in the normal mode of working with an attached HEAD
, being on the branch.
Note that the two original commits here are no longer findable using the name feature
. If we did not have the name origin/feature
remembering the other Git's feature
, we would have abandoned these two commits entirely. But our Git remembers that their Git remembers commit J
using their name feature
.
In either case, note what we have done. We threw out, or at least tried to throw out, the old commits in favor of these new and improved ones. We still have access to the old commits through our origin/feature
name, because we're remembering that the Git over on origin
is remembering commit J
through its branch name feature
.
2You can, if you like, copy any commit yourself using git cherry-pick
. What rebase
does is to detach your HEAD, then do an automated set of cherry-picks, followed by this branch-name motion, similar to git reset
or git branch -f
. In older version of Git, rebase can default to an alternate strategy that does not literally run git cherry-pick
, but these details don't usually matter.
How git push
works
The git push
command works by having your Git call up some other Git. This other Git has commits and branch names, too. Their branch names need not match your branch names, but if they don't, things get pretty confusing, so most people make their branch names the same here.
Their Git lists out, for your Git, their branch names and commit hash IDs.3 This lets your Git figure out which commits you have that they don't, that they'll need. Your Git then sends those commits to their Git, by their hash IDs. Along with those commits, your Git sends any other internal objects their Git requires.
Having sent the right objects, your Git now sends along one or more polite requests or commands. The polite requests have this form: Please, if it's OK, set your name ______ (fill in a branch or tag name) to ______ (fill in a hash ID). The commands have one of two forms: I think your name ______ (fill in a branch or tag name) is set to ______ (fill in a hash ID). If so, set it to ______! Or: Set your name ______ to ______!
The polite request form will ask them to set their feature
to point to commit J'
, our copy of J
that we used as a new-and-improved version of J
. They, however, have no idea that this is meant to be a new-and-improved copy—all they can tell is that we're asking them to throw out commits I
and J
, and make their name feature
remember commit J'
instead. They say no! They say If I do that, I will lose some commits.
That's what we want them to do: lose commits I
and J
, replacing them with the new-and-improved ones. To make them do that, we must send them a command.
If we use git push --force-with-lease
, we'll send them that conditional command: I think your feature
identifies commit J
; if so, make it identify J'
instead. If they accept this command and do it, we and they will have commits I'-J'
and we can draw our repository like this now:
I'-J' <-- feature (HEAD), origin/feature
/
...--G--H--K <-- master, origin/master
\
I--J [abandoned]
This --force-with-lease
option is generally the right way to do this if it's allowed to do this at all. Doing this forces anyone else who is using the feature
branch, in yet another Git repository, to update their branches using the new-and-improved commits. In general, it's a good idea to get everyone to agree that feature
might be rebased this way, before you start rebasing it this way. What you need to do is figure out who "everyone" is. If that's just yourself, you need only agree with yourself. If that's you and six co-workers, get agreement from the co-workers first.
Using git push --force
, instead of --force-with-lease
, omits the safety check: it just sends to the other Git the command set your feature
without any conditional "I think" part first. If your Git is up to date with their Git, so that your origin/feature
and their feature
both identify commit J
, this is OK. But what if, just after you finished your work and were about to push, someone else added a new commit L
to the feature
in the Git over on origin
? Your force-push will tell that Git to abandon that commit, too.
The force-with-lease option is better because your Git will say to the other Git that you believe their feature
identifies commit J
, not commit L
. They'll say: Whoops, no, mine is now L
and your git push --force-with-lease
will fail. You can now git fetch
, see that there's a new commit L
, and fix up your rebase to copy commit L
too, then try your git push --force-with-lease
again now that your origin/feature
says commit L
.
3The exact mechanism here was rewritten for Git smart protocol v2, which was first turned by default in Git 2.26. I won't go into details, but there's a small but nasty little bug in early v2 protocols where your Git can push way too many objects sometimes. This bug is fixed in Git 2.27. If you have 2.26 and pushes are taking much too long, you can work around it with git -c protocol.version=0 push ...
, or just upgrade.