Brian
Brian

Reputation: 745

How to recover from a git push -force?

Here is what happened:

I have two remote git branches: master and feature1. For some reason I have to use git push --force for the feature1 branch, but I didn't know when I use git push --force it will also push the master branch. Then, a disaster happened, as I pushed my local master branch to the remote repository.

Luckily, my local branch is not too far away from the remote. Basically, my remote master has two pull requests merged ahead of my local master.

So my problem is: can I reopen the pull request and remerge? I noticed that there is commit version for merge request, so I am worried if I simply make new pull request it will mess up something? Ideally I just want to redo the merging of the two requests.

Is there some other way to recover from this disaster? I learned the --force is a really, really bad choice. :(

Update, example of what happened:

I have following branches:

master
feature1
origin/master
origin/feature1

I integrate two pull requests by using the GitHub's Auto merge pull requests. Then, I didn't fetch the master branch on my local machine. Thus, I think my origin/master is two versions behind the remote master.

Then I accidentally used git -f push, which overwrote the remote branch and now I lost the commits from the pull requests on remote repository.

How can I recover from it without messing up other contributors' history?

Upvotes: 49

Views: 27279

Answers (3)

einpoklum
einpoklum

Reputation: 131425

If you're working locally, do as user4815162342's answer suggests. If you're working with GitHub, and don't have a reflog with the commit you wish to restore, you can use their newer (Nov 2022) REST API to restore overwritten commits as a new branch.

Summary of how you'll get there:

  1. Get a GitHub token
  2. Query GitHub to locate a commit you want to restore
  3. Tell GitHub to create a new branch with the commit you've identified

Let's do it!

1. Prerequisite: Get yourself a GitHub API access token

Go here: https://github.com/settings/tokens

Or, alternatively:

  • Log in to Github.
  • Navigate to your account settings (e.g. press your avatar graphic and choose "Settings").
  • Enter the Developer Settings
  • Select "Personal Access Tokens" (at the moment those seem to be undergoing a rework)
  • Press the "Generate New Token" button.

Now follow the token generation web-form. If you're generating a "fine-grain" token, make sure the token covers write access to your repositories (and perhaps also notification and projects? Won't hurt). Let it expire relatively soon and don't share it with anyone else.

Eventually, you get a token string, looking something like this:

ghp_Admcgz31WjqIIngI5aBDC1TSfMtyF21ZLwOm

2. Determine the SHA hash of the overwritten branch head

(If you magically happen to know the SHA hash - skip to step 3.)

Make sure you have curl installed (it usually is, otherwise - it's packaged with most OS distributions).

In a shell session, execute:

curl -L \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer ${my_github_token}" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/${github_username}/${github_repo_name}/events \
less

... after either setting the three variables appropriately, or replacing them in the command with the relevant values.

You'll get a huge JSON reply (which is why you're paging it using less). The JSON is a list of objects, each of which is an event, of decreasing recency (i.e. last event first). You care about push events; they have

    "type": "PushEvent",

so search for PushEvent. Your last push event is the push --force which you want to recover from, so not that one. How will you identify it? push events have a payload field, which in turn has commits field, which is an array; , and each object in that array has the commit comment of one of the commits which had been pushed. Also, each event has a created_at field to help guide you. Example:

  {
   "id": "41524933759",
    "type": "PushEvent",
    "actor": {
      "id": 8672255,
      "login": "eyalroz",
      "display_login": "eyalroz",
      "gravatar_id": "",
      "url": "https://api.github.com/users/eyalroz",
      "avatar_url": "https://avatars.githubusercontent.com/u/8672255?"
    },
    "repo": {
      "id": 73505627,
      "name": "eyalroz/cuda-api-wrappers",
      "url": "https://api.github.com/repos/eyalroz/cuda-api-wrappers"
    },
    "payload": {
      "repository_id": 73505627,
      "push_id": 20030025836,
      "size": 2,
      "distinct_size": 2,
      "ref": "refs/heads/development",
      "head": "a29ba6343913b6f4c64dd6236839c62f8101744c",
      "before": "f69368bbade08295db8becf44a606c0429470ef8",
      "commits": [
        {
          "sha": "93d92d5891e9cfd89d73eff2d483c0a5ccae5faf",
          "author": {
            "email": "[email protected]",
            "name": "Eyal Rozenberg"
          },
          "message": "Fixes #667: Marking more methods of dimensions classes as `constexpr` and/or `noexcept`",
          "distinct": true,
          "url": "https://api.github.com/repos/eyalroz/cuda-api-wrappers/commits/93d92d5891e9cfd89d73eff2d483c0a5ccae5faf"
        },
        {
          "sha": "a29ba6343913b6f4c64dd6236839c62f8101744c",
          "author": {
            "email": "[email protected]",
            "name": "Eyal Rozenberg"
          },
          "message": "Fixes #668: Subscript/array-like access to `grid::dimensions_t` and `grid::overall_dimensions_t`",
          "distinct": true,
          "url": "https://api.github.com/repos/eyalroz/cuda-api-wrappers/commits/a29ba6343913b6f4c64dd6236839c62f8101744c"
        }
      ]
    },
    "public": true,
    "created_at": "2024-09-01T14:52:39Z"
  },

When you've found a commit you want to restore - typically the last commit in the last push event before the one you forced by mistake - copy its SHA hash (also a field in the objects in the commits array of the event).

3. Create a new branch, with your chosen commit as the HEAD

Choose a name for your new branch; make sure it's not the name of another, existing one. Now, issue the following curl command in a shell session:

curl -L \
  -X POST \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer ${my_github_token}" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/${github_username}/${github_repo_name}/git/refs \
  -d '{"ref":"refs/heads/${new_branch_name}","sha":"${sha_of_commit_to_restore}"}'

... after either setting the three variables appropriately, or replacing them in the command with the relevant values.


GitHub API docs links:

Upvotes: 0

GHugo
GHugo

Reputation: 2654

Note that, with Github, you can use the API to recover a forced push even if you do not have the repository cloned locally (i.e., when you do not have a reflog), or the commit sha.

First, you must get the previous commit sha, the one before the forced push:

curl -u <username> https://api.github.com/repos/:owner/:repo/events

Then you can create a branch from this sha:

curl -u <github-username> -X POST -d '{"ref":"refs/heads/<new-branch-name>", "sha":"<sha-from-step-1>"}' https://api.github.com/repos/:owner/:repo/git/refs

Finally, you can clone the repository locally, and force push again to master:

git clone repo@github
git checkout master
git reset --hard origin/<new-branch-name>
git push -f origin master

Note that with two-factor authentication, you need to provide a token (see here for more information).

Credit: Sankara Rameswaran

Upvotes: 59

user4815162342
user4815162342

Reputation: 154846

When working with github, refer to GHugo's answer which gives a foolproof procedure with github. If you're on an in-house (or other non-github) installation, read on.

You can always restore the previously observed state of master, by resetting to the old commit and issuing another push -f. The steps involved typically look like this:

# work on local master
git checkout master

# reset to the previous state of origin/master, as recorded by reflog
git reset --hard origin/master@{1}

# at this point verify that this is indeed the desired commit.
# (if necessary, use git reflog to find the right one, and
# git reset --hard to that one)

# finally, push the master branch (and only the master branch) to the server
git push -f origin master

Note, however, that this restores remote master to the state most recently retrieved by git fetch or equivalent. Any commits pushed by others after the last time you fetched will be lost. However, those commits will still be available in their reflogs, so they can restore them using steps like the above.

Upvotes: 103

Related Questions