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 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
를 선택해야만 아래 과정에 문제가 발생하지 않는다.
일단, 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가지 이벤트로 구성이 되어 있다.
여기서, force push전 head의 SHA 값인 afee477d063c83998a39a05f2a008a8470aed7d1
를 얻어올 수 있다.
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
HTTPS, SSH 방식 중 원하는 방법을 선택해 Repository를 clone 한다.
(두 차이점을 모르거나 SSH 방식을 써본적이 없는 사용자라면, HTTPS로 시도하면 된다.)
git clone https://github.com/<owner>/<repo>.git
git clone https://github.com/Web-Engine/recover-test.git
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
마지막으로, 되돌리려고 하는 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
감사합니다. 덕분에 살았어요....
엄청나게 감사한데 표현할 길이 없네요.
하는 일마다 다 잘 되시기를 바라겠습니다!