병합 뒤 로컬 기준 브랜치를 정해진 커밋으로 옮겨야 했습니다. 다른 작업을 열어 둔 상태라 기준 브랜치로 체크아웃하지 않고 처리했습니다. 이때 필요한 확인은 브랜치가 아직 제가 확인한 커밋에 있는지, 그리고 옮길 커밋이 현재 이력을 그대로 포함하는지였습니다.

현재 참조와 조상 관계 확인

먼저 git merge-base --is-ancestor "$old" "$new"로 이전 커밋이 새 커밋의 조상인지 확인했습니다. 종료 코드가 0이어야 빨리 감기입니다. 다음으로 git worktree list --porcelain에서 옮길 브랜치를 사용하는 체크아웃이 없는지 확인했습니다. 체크아웃한 브랜치의 참조를 직접 바꾸면 그 폴더의 인덱스와 파일이 새 커밋에 맞춰 갱신되지 않기 때문입니다.

이후 git update-ref refs/heads/develop "$new" "$old"를 실행했습니다. 세 번째 인자 때문에 실제 값이 여전히 $old일 때만 움직입니다. 확인과 실행 사이에 다른 세션이 참조를 바꿨다면 Git은 갱신을 거절합니다. 이 명령은 조상 관계나 워크트리 상태까지 검사하지 않으므로 앞선 확인이 따로 필요합니다. 마지막에 git rev-parse로 예상한 커밋에 도착한 것을 확인했습니다. 워크트리를 새로 여는 작업도 동시에 일어날 수 있는 환경이라면 그 변경과 참조 이동의 실행 순서를 조율해야 합니다.

참조가 먼저 움직이는 순서를 격리해서 확인

임시 저장소에서 한 커밋을 기준으로 두 후보를 만들었습니다. 첫 후보가 처음 읽은 참조의 자손인 것을 확인한 뒤, 별도 Git 명령으로 같은 참조를 두 번째 후보에 옮겼습니다. 그다음 처음 읽은 값을 세 번째 인자로 넣고 첫 후보로 갱신을 요청했습니다.

fatal: update_ref failed for ref 'refs/heads/audit-check':
cannot lock ref 'refs/heads/audit-check': is at <changed> but expected <old>

읽기 쉽게 한 줄을 나누고 커밋 값은 표시용 이름으로 바꿨습니다. Git은 종료 코드 128을 반환했습니다. 참조를 다시 읽으니 두 번째 후보에 그대로 있었습니다. 첫 후보가 처음 읽은 값에서 빨리 감기 가능한 커밋이어도, 실행 시점의 참조가 바뀌면 그 갱신은 거절된다는 것을 이 순서에서 확인했습니다.

푸시할 커밋과 목적지 명시

별도의 작업에서 이미 존재하는 원격 피처 브랜치에 새 커밋을 올릴 때는 원격 브랜치의 관측값이 HEAD의 조상인지 검사했습니다. 그 Push는 강제 옵션 없이 빨리 감기로 끝났습니다. 새 피처 브랜치의 첫 Push에서 사용하는 git push -u origin "HEAD:refs/heads/$branch"는 로컬에서 보낼 커밋과 원격에서 바꿀 브랜치를 함께 지정합니다. -u를 통해 이후 추적할 브랜치도 해당 원격 브랜치로 설정합니다.

로컬 참조에서 기대한 이전 커밋을 지정하는 것과 원격 Push에서 목적지를 지정하는 것은 서로 다른 확인입니다. 둘을 나누면 검토 당시 커밋과 실제로 변경할 브랜치를 명령에서 각각 확인할 수 있습니다.

MR 병합에서 원본 SHA 확인

GitLab 병합 API에는 검토한 원본 브랜치의 커밋을 sha로 넘기는 옵션이 있습니다. 검토 뒤 새 커밋이 올라와 원본 브랜치의 최신 SHA가 달라지면 문서상 병합 요청은 409로 거절됩니다. 이 값으로 대상 브랜치까지 고정되지는 않습니다. 대상 브랜치가 움직였는지는 따로 읽고, 병합 뒤에는 원격 이력에 결과 커밋이 들어갔는지 확인할 수 있습니다. 이 절은 API가 제공하는 조건을 로컬 참조 갱신과 비교한 설명입니다. GitLab 병합 API 문서