← 홈Setup Tip

Setup Tip

Setup Tip — 리베이스된 PR은 `+`를 붙여서 가져온다

작성자가 리베이스한 PR을 다시 리뷰하려고 `git fetch origin pull/N/head:pr-N`을 돌렸다. 내 로컬 `pr-N` 브랜치는 예전 커밋에 그대로 머물러 있었다. 리베이스된 head는 이전 head에서 이어지는 커밋이 아니라서, 앞에 `+`가 없는 refspec으로는 git이 브랜치를 덮어쓰지 않는다. 이 거부는 출력에 한 줄로만 나와서 놓치기 쉽다. 나는 head SHA가 GitHub과 다르다는 것을 보고서야 알아챘다. 리뷰용 fetch에는 `+`를 붙이고, 리뷰를 시작하기 전에 로컬 SHA를 GitHub이 보고하는 head와 비교한다.

무슨 일이 있었나

리뷰하던 PR의 작성자가 브랜치를 리베이스하고 다시 푸시했다. 나는 같은 명령으로 PR을 다시 가져왔다: git fetch origin pull/N/head:pr-N. 그리고 pr-N을 체크아웃해서 리뷰를 이어가려 했다. 그런데 로컬 브랜치의 커밋 SHA가 GitHub이 보여 주는 head와 달랐다. 로컬 브랜치는 리베이스 전 커밋에 그대로 있었다.

왜 갱신되지 않았나

pull/N/head:pr-N은 '원격의 이 ref를 가져와서 로컬 pr-N에 넣어라'라는 refspec이다. 로컬 pr-N이 이미 있으면, git은 새 커밋이 기존 커밋의 후손일 때, 즉 빨리 감기(fast-forward)로 갈 수 있을 때만 브랜치를 옮긴다. 리베이스는 커밋을 새로 만들기 때문에 새 head는 옛 head의 후손이 아니다. 그래서 git은 갱신을 거부한다.

거부 자체는 출력에 ! [rejected] ... (non-fast-forward) 같은 한 줄로 나오고, 종료 코드도 0이 아니다. 하지만 같은 fetch가 다른 ref들은 정상으로 가져오고, 스크립트나 자동화 안에서 출력이 접혀 있으면 이 한 줄은 쉽게 지나간다. 결과적으로 남는 것은 '가져왔다'는 느낌과 예전 커밋이다.

해결: refspec 앞에 +

refspec 앞의 +는 '빨리 감기가 아니어도 덮어써라'라는 뜻이다. git fetch origin +pull/N/head:pr-N으로 바꾸면, 리베이스나 강제 푸시 뒤에도 로컬 pr-N이 원격 head로 옮겨진다. 리뷰용 로컬 브랜치는 내 작업이 아니라 원격의 사본이므로, 덮어쓰는 것이 맞는 동작이다. 같은 이유로 git fetch --force를 써도 된다.

단, 지금 체크아웃되어 있는 브랜치에는 이렇게 가져올 수 없다. git이 작업 트리를 지키려고 거부한다. 리뷰 브랜치에서 빠져나온 뒤 가져오거나, 처음부터 detached 상태로 리뷰하는 편이 깔끔하다.

더 단순한 방법: 브랜치를 만들지 않는다

리뷰만 할 거라면 로컬 브랜치 자체가 필요 없다. git fetch origin pull/N/head를 실행하면 결과가 FETCH_HEAD에 들어가고, git switch --detach FETCH_HEAD로 그 커밋을 바로 볼 수 있다. 덮어쓸 브랜치가 없으니 빨리 감기 문제도 생기지 않는다. gh pr checkout N --force도 같은 목적에 쓸 수 있다.

그래도 SHA는 확인한다

+는 이번 원인을 막지만, '가져온 것이 정말 지금의 PR인가'를 증명해 주지는 않는다. 리뷰를 시작하기 전에 git rev-parse HEAD와 gh pr view N --json headRefOid -q .headRefOid를 비교한다. 둘이 다르면 리뷰를 시작하지 않는다. 리뷰 기록에도 이 SHA를 남긴다. 그러면 나중에 '어느 커밋을 보고 승인했나'를 바로 답할 수 있다.

정리

리베이스된 PR을 다시 가져올 때 + 없는 fetch는 아무것도 바꾸지 않을 수 있다. 리뷰용 사본은 +로 덮어쓰거나 detached로 보고, 리뷰 전에 SHA를 맞춰 본다.