← All TILs · git

A branch you fetched was force-pushed: keep the old tip, then `rebase --onto`

git - 2026-09-27

A coding agent pushed a commit to my branch (2913eb4), and I fetched it. It then amended the commit message, force-pushed the new version (504a439, same code), and pushed its next step (d23665b) on top. My clone was left on a commit GitHub no longer had.

sequenceDiagram
    participant Agent as Agent's clone
    participant GH as GitHub (origin/feature)
    participant Me as My clone

    Agent->>GH: git push: 2913eb4
    Me->>GH: git fetch
    Agent->>Agent: git commit --amend: 504a439
    Agent->>GH: git push --force: 504a439, then d23665b
    Me->>GH: git fetch
    GH-->>Me: (forced update)
    Note over Me: feature still ends in 2913eb4

This is for following a rewrite that was intended (an amend, squash or rebase). Tested with Git 2.43 in scratch clones, in three cases: - a message-only rewrite; - a rewrite that changed the code, with an unpushed commit of mine on top; - a rewrite that discarded a commit I had pushed.

Spot it

 + 2913eb4...d23665b feature    -> origin/feature  (forced update)

Your branch and 'origin/feature' have diverged,
and have 1 and 2 different commits each, respectively.

The first line comes from git fetch: the old tip on the left, the new one on the right. The other two come from git status, whose counts include every commit on each side since the fork. After a rebase, that means all the old versions plus any of yours, without saying which is which. Don't follow git status's hint to run git pull yet.

flowchart TD
    START["git fetch: '(forced update)'"] --> KEEP["0. git branch before-force-push origin/feature@{1}"]
    KEEP --> LOG["1. git log --oneline origin/feature..feature"]
    LOG --> Q{"Any commit of mine?"}
    Q -->|no| RESET["2. git reset --hard origin/feature"]
    Q -->|yes| ONTO["2. git rebase --onto origin/feature #lt;last commit not mine#gt;"]

0. Keep the old tip

git branch before-force-push origin/feature@{1}

@{1} is the previous value of origin/feature, taken from its reflog; git reflog show origin/feature confirms it. This costs nothing, and it's the step to push (git push origin before-force-push) if the rewrite turns out to be a mistake.

1. See what's only yours

git log --oneline origin/feature..feature
git range-diff feature...origin/feature

The first command lists everything your branch has that the new remote doesn't: the old versions of rewritten commits, and your own commits. Git can't tell the two apart; the messages, and the authors with --format='%h %an %s', can. range-diff pairs old and new versions (1: 2913eb4 ! 1: 504a439) and shows what changed, a message-only fix included. It only pairs them when the changes are similar, though. When the rewrite changed the code, it listed old and new versions separately, like my own commit.

2. Reset, or replay only your commits

Nothing of yours in the list:

git status        # reset --hard discards uncommitted changes
git reset --hard origin/feature

Commits of yours on top: name the last commit that isn't yours, and replay only what comes after it:

git rebase --onto origin/feature 2913eb4

This worked in both tests with commits of mine. They end up on top of d23665b, with new hashes.

What not to run

git pull --rebase, like git rebase with no argument, skips every commit that origin/feature ever pointed to, reading them from its reflog (--fork-point). That reflog includes your own pushes. In the third test, git pull --rebase answered Successfully rebased and updated refs/heads/feature, and my pushed commit was gone, with no conflict and no warning.

Command, on the diverged branch Result in the tests
git pull, no config Refuses: Need to specify how to reconcile divergent branches
git pull with pull.rebase false Merges: a conflict here, or both versions kept for good
git pull with pull.ff only Refuses: Not possible to fast-forward
git pull --rebase, git rebase Old versions dropped, and your pushed commits too
git rebase origin/feature Old versions replayed: skipped if identical, conflict if not
git rebase --onto origin/feature <old> Only your commits replayed
git reset --hard origin/feature Branch equals the remote; old tip kept by step 0

git config pull.ff only (per repository, or --global) makes every git pull stop on a diverged branch, even with pull.rebase set. Many people set pull.rebase false long ago to silence a hint, which turns the refusal into a merge.

On the pushing side

Don't rewrite a branch others have fetched; a commit message can be fixed in a later commit. If you must, use git push --force-with-lease. It refuses when the remote moved since your last fetch. In the third test it rejected the agent's push (stale info), which would have saved my commit. Then tell the others the old and new hashes.

Sources

Created 2026-09-27T21:56:33+02:00 · Edit