본문 바로가기
빌드노트 검색

빌드노트

detached HEAD에서 만든 커밋, 이동 전에 브랜치로 남기기

커밋 해시를 checkout한 뒤 만든 커밋은 기존 브랜치에 자동으로 연결되지 않는다. detached 표시와 실제 브랜치를 구분하고, 이동 전에 switch -c로 같은 HEAD와 부모를 유지하며 보존하는 방법을 합성 저장소에서 확인했다.

커밋 해시를 checkout한 상태에서 새 커밋을 만들면 기존 브랜치는 그 커밋으로 따라오지 않는다. 이후 main으로 돌아갔을 때 작업 파일이 예전 모습이 되고 기본 로그에서도 새 커밋이 빠져, 작업을 잃었다고 생각하기 쉽다. 아직 새 커밋에 머물러 있다면, 이동하기 전에 git switch -c keep-detached로 현재 커밋에 브랜치 이름을 붙인다.

HEAD는 현재 checkout한 위치를 나타낸다. 보통은 브랜치 이름을 거쳐 커밋을 가리키지만, 커밋 해시로 checkout하면 커밋을 직접 가리키는 detached HEAD가 된다. 이 상태에서도 커밋은 정상적으로 만들어진다. 새 커밋을 따라 움직이는 브랜치가 없다는 점이 다르다. Git checkout의 detached HEAD 설명 (새 탭에서 열림)

괄호로 표시된 HEAD를 브랜치로 세지 않는다

Git이 설치된 PowerShell 7에서 작업 중인 저장소로 이동한다. 여기서는 일반 checkout으로 detached 상태가 됐고, rebase나 merge가 진행 중이지 않으며, 남길 변경은 이미 커밋한 경우를 다룬다. 먼저 아래 첫 명령의 경로가 의도한 저장소인지 확인한 뒤 나머지를 실행해 현재 연결 상태와 커밋을 포함하는 로컬 브랜치를 읽는다.

git rev-parse --show-toplevel
git status --short --branch
git symbolic-ref --quiet --short HEAD
$LASTEXITCODE
git branch --contains HEAD

동작을 확인하기 위해 Windows 11, Git 2.45.1.windows.1, PowerShell 7.6.6에서 새 합성 저장소를 만들었다. main에 base note 커밋을 만든 뒤 그 해시로 checkout하고, note.txt에 한 줄을 더해 detached note 커밋을 만들었다. 첫 명령의 저장소 경로를 제외한 출력은 다음과 같았다.

## HEAD (no branch)
1
* (HEAD detached from 79f2582)

status는 브랜치 이름 대신 HEAD (no branch)를 표시했고, 파일 변경 행은 없었다. 작업 트리는 깨끗하지만 브랜치에는 연결되지 않은 상태였다. symbolic-ref는 아무것도 출력하지 않고 종료 코드 1을 반환했다. 바로 다음의 $LASTEXITCODE가 그 값을 보여 준다. 브랜치에 연결돼 있으면 이 명령은 이름을 출력하고 0을 반환한다. 128처럼 다른 오류가 나오면 저장소 경로와 오류부터 확인한다. symbolic-ref의 출력과 종료 코드 (새 탭에서 열림)

마지막 행의 별표만 보고 보존할 브랜치가 있다고 판단하면 안 된다. (HEAD detached from 79f2582)는 현재 상태를 표시한 행이며, 이름 있는 브랜치가 아니다. 이 실험에서 79f2582는 처음 분리된 기준 커밋이었다. 방금 만든 커밋의 ID는 아래에서 확인할 3232839였다. 괄호 속 해시를 새 커밋의 해시로 옮겨 적으면 다른 위치를 보존하게 된다.

git branch --contains HEAD에 main 같은 실제 이름이 함께 나오면, 그 로컬 브랜치의 이력에 현재 커밋이 포함돼 있다는 뜻이다. 끝점이 현재 커밋과 같을 필요는 없고, 이후 커밋까지 진행한 브랜치도 나온다. 이 조회에는 원격 추적 브랜치와 태그가 포함되지 않는다. branch의 목록 범위와 --contains (새 탭에서 열림)

실험에서도 기준 커밋을 checkout한 직후에는 detached 표시와 main이 함께 나왔다. 새 커밋을 만든 뒤에야 main이 목록에서 빠졌다. 따라서 detached라는 상태와 현재 커밋을 포함하는 브랜치가 없는 상태를 따로 확인해야 한다.

현재 커밋에 새 브랜치를 연결한다

남길 커밋에 그대로 머문 상태에서 아직 쓰지 않는 브랜치 이름을 정한다. 다음 블록은 keep-detached를 사용한다. 첫 조회로 현재 커밋과 첫 부모의 ID를 기록하고, 브랜치를 만든 뒤 같은 두 ID를 비교한다. 이 글의 대상은 기존 커밋 위에 새 커밋을 만든 경우이므로 부모가 있다.

git rev-parse HEAD 'HEAD^'
git switch -c keep-detached
if ($LASTEXITCODE -ne 0) { throw '새 브랜치를 만들지 못했습니다.' }
git symbolic-ref --short HEAD
git rev-parse HEAD 'HEAD^'
git branch --contains HEAD

switch -c에 시작 커밋을 따로 주지 않으면 현재 HEAD에서 새 브랜치를 만든 뒤 그 브랜치로 연결한다. 여기서 main을 추가로 지정하면 남길 커밋 대신 main을 시작점으로 삼게 되므로, 위 명령은 새 이름만 받는다. Git switch의 -c와 시작점 (새 탭에서 열림)

실제 실행에서 symbolic-ref는 keep-detached를 출력했고, 마지막 브랜치 목록은 * keep-detached였다. 두 번의 rev-parse 출력은 모두 다음 두 줄로 같았다.

3232839ef05505a08e60664b884f79955f805768
79f2582ae94ef90f6380a4cc70e0f3c4910af116

첫 줄은 남길 새 커밋, 둘째 줄은 그 부모다. HEAD와 부모가 그대로였고 파일 내용도 바뀌지 않았다. 새 커밋을 다시 만들거나 다른 브랜치에 복사한 과정 없이, 같은 커밋에 이름을 붙인 결과다. 자기 저장소에서는 위 해시를 복사하지 말고 실행 전후의 두 출력을 비교한다.

이미 존재하는 이름으로 만들려 하면 실패한다. 실험에서 git switch -c main은 종료 코드 128과 a branch named 'main' already exists를 반환했고, HEAD와 기존 main은 그대로였다. 같은 오류가 나면 다른 새 이름을 고른다. 대문자 -C는 기존 브랜치의 위치를 바꿀 수 있으므로 이 이름 충돌의 해결책으로 사용하지 않는다. switch의 -C 동작 (새 탭에서 열림)

이동 뒤 파일이 달라진 것과 커밋 삭제는 구분한다

같은 두 커밋을 만든 별도 합성 저장소에서 브랜치를 붙이지 않고 main으로 이동했다. Git은 브랜치에 연결되지 않은 커밋 하나를 두고 간다는 경고를 냈다. note.txt는 처음의 base 한 줄로 돌아왔고, 기본 로그에는 79f2582 base note만 남았다. 남겨 둔 커밋의 ID로 브랜치를 조회해도 빈 출력이었다.

그 직후, 미리 기록한 커밋 ID를 git cat-file -t로 조회한 결과는 commit이었다. 로그에서 빠지고 파일이 예전 모습으로 바뀌었어도, 그 순간 커밋 객체가 삭제된 것은 아니었다. 다만 이 관찰을 계속 복구할 수 있다는 보장으로 삼을 수는 없다. 공식 checkout 문서는 참조 없이 남은 커밋이 나중에 정리될 수 있다고 설명한다. detached 커밋의 참조와 정리 (새 탭에서 열림)

반면 keep-detached를 먼저 만든 저장소에서는 main으로 이동한 뒤에도 새 커밋을 포함하는 브랜치로 keep-detached가 나왔다. 그 브랜치로 돌아오자 note.txt의 base와 detached work 두 줄도 돌아왔다. 현재 커밋에 이름을 붙였기 때문에 이후에도 브랜치로 찾아갈 수 있었다.

이미 다른 커밋으로 이동했다면 그 자리의 switch -c는 현재 위치를 보존한다. 남겨 둔 커밋은 먼저 reflog 등에서 찾아 내용을 확인해야 하며, 이 글의 이동 전 보존과는 시작 조건이 다르다. 직접 검증한 범위는 깨끗한 작업 트리에서 일반 checkout 후 만든 커밋과 로컬 브랜치 연결이다. 미커밋 수정, rebase 진행 중의 detached 상태, reflog 만료나 객체 정리는 시험하지 않았다.

← 전체 글 보기