$ Git Game

Someone force-pushed over my commit

Someone pushed with --force, and main no longer has your commit. Your clone still does, which is all you need. Here is how it looks, how to put it back, and the one command that would quietly lose it.

$ git fetch
From ../origin
 + 7a20dd4...864afd7 main       -> origin/main  (forced update)
$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" if you want to integrate the remote branch with yours)

nothing to commit, working tree clean

Put it back on top

git status says the branches have diverged, one commit each: theirs on origin/main, yours on your main, and git log shows yours at the top. Rebase your branch onto what origin/main is now: Git takes your commit off, moves main to their tip, and replays yours on top. The push after it is a fast-forward.

$ git log --oneline
7a20dd4 api: add the endpoint
ab5768d api: the first endpoint
07dec17 readme: the project
$ git rebase origin/main
Successfully rebased and updated refs/heads/main.
$ git log --oneline
5b30b2d api: add the endpoint
864afd7 auth: check the token
ab5768d api: the first endpoint
07dec17 readme: the project
$ git push
To ../origin.git
   864afd7..5b30b2d  main -> main

Both commits are on main again, yours with a new hash, since it has a new parent. If the two touched the same lines, the replay stops on a conflict first. What the forced push itself did is on the forced update page.

The pull that would drop it

git pull --rebase looks like the same move, and here it isn't. A pull works out which commits are yours from the history of origin/main as your clone has seen it, the fork point. A commit that was once on origin/main counts as upstream's, and since it is no longer there, the rebase drops it, and says nothing:

$ git pull --rebase
Successfully rebased and updated refs/heads/main.
$ git log --oneline
864afd7 auth: check the token
ab5768d api: the first endpoint
07dec17 readme: the project
$ git reset --hard ORIG_HEAD
HEAD is now at 7a20dd4 api: add the endpoint
$ git rebase origin/main
Successfully rebased and updated refs/heads/main.
$ git log --oneline
5b30b2d api: add the endpoint
864afd7 auth: check the token
ab5768d api: the first endpoint
07dec17 readme: the project

Successfully rebased, and the commit is gone from the log. ORIG_HEAD still points to where the branch was before the rebase, so git reset --hard ORIG_HEAD is the way back, and then the rebase above, with origin/main named on the command line, which turns the fork-point rule off.

If it isn't on your machine

Then it is on the forcer's. Their remote-tracking branch keeps a reflog of its own, and origin/main@{1} is where main was before their push. Pushing that back is one command, with a lease so it can't erase anything newer:

$ git reflog show origin/main
864afd7 refs/remotes/origin/main@{0}: update by push
7a20dd4 refs/remotes/origin/main@{1}: fetch: fast-forward
ab5768d refs/remotes/origin/main@{2}: update by push
$ git push --force-with-lease origin origin/main@{1}:main
To ../origin.git
 + 864afd7...7a20dd4 origin/main@{1} -> main (forced update)

That restores main to before the force, your commit included; the forcer's own commit is now the one off main, and a rebase and a push, as above, puts it back properly. Hosts keep an erased commit for a while too, and show forced pushes in a branch's activity, so its hash can still be found there.

Why the game does this to you

In Git Game a push --force erases every commit after the forcer's pointer, and the erased commits go back to their owners' local branches: the same place Git leaves them. Next day a pull puts you back at the tip and your push lands on top, which is the rebase above. A reflog armed in your pack does it at once: it fires inside the forcer's pack and puts your erased commits back on top of main before the day is over.

Play a day where this happens

One tap starts a game against the bot, which holds the push --force card too.

play the bot now

Output captured from Git 2.50.1 (Apple Git-155) by scripts/git-output.sh, in a scratch repository with two clones, ana's and bot's, and fixed dates, so the hashes are the same on every run. It is captured again at every Git release.