Github force push 되돌리기

조태상·2021년 6월 28일
2

1년 전 쯤에 10명 가까이 되는 팀원들과 프로젝트를 진행하면서 force push를 통해 약 50여개의 commit을 날려버린 적이 있다.

다 같이 사용하는 공용 레포지토리와 개인 레포지토리를 따로 두고, Pull Request를 통헤 코드를 병합하는 형태로 프로젝트를 진행했었는데,
개인 레포지토리에 force push 하려던 것을 실수로 공용 레포지토리에 push하여 대참사가 벌어진 것이다.

그 때 당시에 force push는 되돌릴 수 없다고 알고 있어서 굉장히 많이 당황했었고, force push를 되돌리는 방법에 대해 많이 찾아보았는데 오늘 그 방법에 대해 공유한다.

본론으로 들어가기에 앞서

이 방법은 Github에서만 사용 가능한 방법임에 유의해야 하며, Github Activity API(Events)를 사용한다.
또한, Github에서 Force push를 되돌릴 수 있다고 해서, force push를 남용하는 일은 결코 좋지 못하다.

Github Force push 되돌리기

Step 0. Access Token 발급 받기

Github API에서 Basic Authentication을 지원 중단했다.

따라서 아래에서 사용하는 Github API는 Access Token을 발급받아 진행해야 한다.
Github Access 토큰은 [Settings / Developer settings / Personal access tokens]에서 발급할 수 있다.

Github Access 토큰을 발급할 때, 해당 토큰의 scope를 설정해야 하는데 repo 전체를 선택해야 다음 과정을 진행하는데 문제가 없다.

직접 테스트를 해 보았는데 repo:status, repo_deployment, public_repo, repo:invite security_events를 각각 선택하는 것과, repo 전체를 선택하는 것은 서로 다른 scope를 가진다.

정확히 repo를 선택해야만 아래 과정에 문제가 발생하지 않는다.

Step 1. Force push 전 마지막 커밋 SHA 값 확인하기

일단, Force push 하기 전에 있었던 마지막 커밋의 SHA 값을 알아내야 한다.

고맙게도, Github에서는 Repository에 push를 포함한 다양한 동작이 있을 때마다 기록을 하며,
Github Activity API를 통해 그 때의 마지막 SHA 값을 확인 할 수 있다.

curl -H "Authorization: token <access-token>" https://api.github.com/repos/<owner>/<repo>/events
예시
curl -H "Authorization: token ghp_thisismyaccesstoken" https://api.github.com/repos/Web-Engine/recover-test/events

위 예시를 통해 얻은 응답 내용은 다음과 같다.

[
  {
    "id": "16958161171",
    "type": "PushEvent",
    "actor": {
      "id": 3965510,
      "login": "Web-Engine",
      "display_login": "Web-Engine",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Web-Engine",
      "avatar_url": "https://avatars.githubusercontent.com/u/3965510?"
    },
    "repo": {
      "id": 381164815,
      "name": "Web-Engine/recover-test",
      "url": "https://api.github.com/repos/Web-Engine/recover-test"
    },
    "payload": {
      "push_id": 7418088186,
      "size": 0,
      "distinct_size": 0,
      "ref": "refs/heads/master",
      "head": "cbe8d565dd4763e53c7b1281b9550453f7574753",
      "before": "afee477d063c83998a39a05f2a008a8470aed7d1",
      "commits": [

      ]
    },
    "public": true,
    "created_at": "2021-06-28T21:29:48Z"
  },
  {
    "id": "16958155891",
    "type": "PushEvent",
    "actor": {
      "id": 3965510,
      "login": "Web-Engine",
      "display_login": "Web-Engine",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Web-Engine",
      "avatar_url": "https://avatars.githubusercontent.com/u/3965510?"
    },
    "repo": {
      "id": 381164815,
      "name": "Web-Engine/recover-test",
      "url": "https://api.github.com/repos/Web-Engine/recover-test"
    },
    "payload": {
      "push_id": 7418085595,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/master",
      "head": "afee477d063c83998a39a05f2a008a8470aed7d1",
      "before": "cbe8d565dd4763e53c7b1281b9550453f7574753",
      "commits": [
        {
          "sha": "afee477d063c83998a39a05f2a008a8470aed7d1",
          "author": {
            "email": "gsts007@gmail.com",
            "name": "TaeSang Cho"
          },
          "message": "Add original message",
          "distinct": true,
          "url": "https://api.github.com/repos/Web-Engine/recover-test/commits/afee477d063c83998a39a05f2a008a8470aed7d1"
        }
      ]
    },
    "public": true,
    "created_at": "2021-06-28T21:29:16Z"
  },
  {
    "id": "16958149731",
    "type": "CreateEvent",
    "actor": {
      "id": 3965510,
      "login": "Web-Engine",
      "display_login": "Web-Engine",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Web-Engine",
      "avatar_url": "https://avatars.githubusercontent.com/u/3965510?"
    },
    "repo": {
      "id": 381164815,
      "name": "Web-Engine/recover-test",
      "url": "https://api.github.com/repos/Web-Engine/recover-test"
    },
    "payload": {
      "ref": "master",
      "ref_type": "branch",
      "master_branch": "master",
      "description": null,
      "pusher_type": "user"
    },
    "public": true,
    "created_at": "2021-06-28T21:28:40Z"
  },
  {
    "id": "16958113320",
    "type": "CreateEvent",
    "actor": {
      "id": 3965510,
      "login": "Web-Engine",
      "display_login": "Web-Engine",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Web-Engine",
      "avatar_url": "https://avatars.githubusercontent.com/u/3965510?"
    },
    "repo": {
      "id": 381164815,
      "name": "Web-Engine/recover-test",
      "url": "https://api.github.com/repos/Web-Engine/recover-test"
    },
    "payload": {
      "ref": null,
      "ref_type": "repository",
      "master_branch": "main",
      "description": null,
      "pusher_type": "user"
    },
    "public": true,
    "created_at": "2021-06-28T21:25:07Z"
  }
]

위 내용을 분석해 보면, 총 4가지 이벤트로 구성이 되어 있다.

  1. PushEvent: 문제를 일으킨 force push
  2. PushEvent: 문제가 발생하기 전 마지막 push
    ...
  3. CreateEvent: master 브랜치를 생성
  4. CreateEvent: 레포지토리를 생성

여기서, force push전 head의 SHA 값인 afee477d063c83998a39a05f2a008a8470aed7d1를 얻어올 수 있다.

Step 2. 마지막 커밋을 기반으로 새로운 브랜치 만들기

Github API를 통해 Step 1에서 확인한 마지막 커밋을 기반으로 한 새로운 브랜치를 생성한다.

curl -H "Authorization: token <access-token>" -X POST -d '{"ref":"refs/heads/<new-branch-name>", "sha":"<sha-from-step-1>"}' https://api.github.com/repos/:owner/:repo/git/refs
예시
curl -H "Authorization: token ghp_thisismyaccesstoken" -X POST -d '{"ref":"refs/heads/recover", "sha":"afee477d063c83998a39a05f2a008a8470aed7d1"}' https://api.github.com/repos/Web-Engine/recover-test/git/refs

Step 3. 새로운 브랜치 Clone 하기

HTTPS, SSH 방식 중 원하는 방법을 선택해 Repository를 clone 한다.
(두 차이점을 모르거나 SSH 방식을 써본적이 없는 사용자라면, HTTPS로 시도하면 된다.)

HTTPS

git clone https://github.com/<owner>/<repo>.git
예시
git clone https://github.com/Web-Engine/recover-test.git

SSH

git clone git@github.com:<owner>/<repo>.git
예시
git clone git@github.com:Web-Engine/recover-test.git

Repository를 clone 한 뒤에, 되돌리려고 하는 브랜치(보통 master)로 이동한다.
보통의 경우에는 이동하지 않아도 master 브랜치겠지만, 확실하게 하기 위해 한번 더 확인하는 과정이다.
그 후, Step 2에서 생성한 브랜치로 강제로 덮어쓴다.

git checkout <branch-name>
git reset --hard origin/<new-branch-name>
예시
git checkout master
git reset --hard origin/recover

Step 4. 기존 브랜치에 force push 하기

마지막으로, 되돌리려고 하는 branch에 force push 하면 복구 완료!

git push -f origin <branch-name>
예시
git push -f origin master

발생할 수 있는 문제점

만약 이 복구 과정을 진행하는 도중에 다른 사람이 복구를 진행 중인 branch에 push를 한다면, 해당 push에 포함된 commit들이 누락되는 문제가 발생 할 수 있다.

참고자료

Recover Force Push on Github:
https://gist.github.com/agarwalparas/d355a950148702cc7ba82abc4d1943bf

Github Activity API Docs:
https://docs.github.com/en/rest/reference/activity

1개의 댓글

comment-user-thumbnail
2023년 8월 7일

감사합니다. 덕분에 살았어요....
엄청나게 감사한데 표현할 길이 없네요.

하는 일마다 다 잘 되시기를 바라겠습니다!

답글 달기