Reddit

git worktree gotchas

git worktree의 함정들 — .git이 디렉터리가 아니라 파일일 때

git worktree가 코딩 에이전트 시대를 맞아 주류 도구가 됐지만, linked worktree에서는 .git이 디렉터리가 아니라 포인터 파일이라는 사실에서 비롯되는 함정들이 있습니다. 리포지터리 루트 탐색 로직이 어긋나고, 샌드박스 안에서는 git hooks가 끊기는 문제까지, 저자는 git rev-parse 계열 명령과 core.hooksPath 설정으로 해결책을 제시합니다.

본문

저자 Olaf Alders는 몇 년 전 '4 Strategies for Context Switching in Git'이라는 글을 썼을 때 독자 반응이 대체로 '멋지긴 한데 쓸 일은 없겠다'였다고 회고합니다. 그런데 상황이 바뀌어 이제 git worktree는 더 이상 마이너한 도구가 아니며, 특히 LLM 코딩 에이전트를 격리해 돌리는 용도가 늘면서 다시 주목받고 있습니다. 다만 여전히 몇 가지 함정이 있고, 이 글은 그 함정들이 모두 하나의 사실에서 비롯된다고 설명합니다.

용어 정리 — 모든 체크아웃은 worktree입니다

git worktree list를 실행하면 첫 번째 항목이 GitHub에서 clone한 메인 체크아웃이고, 그 뒤가 직접 만든 worktree들입니다. 글에서는 전자를 'main working tree', 후자를 'linked worktree'라고 부릅니다.

근본 원인 — linked worktree의 .git은 디렉터리가 아니라 파일입니다

main working tree에서 .git은 온갖 것이 들어 있는 디렉터리지만, linked worktree에서 file .git을 실행하면 'ASCII text'가 나옵니다. 내용은 'gitdir: /home/olaf/github/.../.git/worktrees/gh-49'처럼 이 worktree 전용 git 디렉터리의 경로이며, 실제 위치는 main working tree의 .git 안쪽입니다. 여기서 함정이 발생합니다. 현재 디렉터리에서 위로 올라가며 .git이라는 '디렉터리'를 찾는 식의 리포지터리 루트 탐지 로직은 linked worktree 안에서는 작동하지 않습니다. .git이 파일이므로 루트를 그대로 지나쳐 버리고, 만약 linked worktree가 main working tree의 하위 디렉터리에 있다면 엉뚱하게 main working tree에서 멈춥니다. 겉보기에는 비슷해 보이지만 작업 중이던 변경사항이 없는 상태라 혼란만 커집니다. 저자는 이 때문에 linked worktree를 ~/.worktree처럼 아예 다른 경로에 두라고 권하며, 직접 구현하는 대신 git rev-parse --show-toplevel을 쓰면 어느 worktree에서든 올바른 루트를 반환한다고 덧붙입니다.

main working tree 찾기 — --git-common-dir

main working tree에만 있는 gitignore 처리된 .env나 시크릿 파일을 linked worktree에서 찾아야 할 때가 있습니다. 이때 git rev-parse --git-common-dir을 쓰면 공통 .git 디렉터리의 경로가 나옵니다. 이 경로는 main working tree 자체가 아니라 그 안의 .git을 가리키므로, dirname $(git rev-parse --git-common-dir)처럼 한 단계 위로 올라가야 합니다.

샌드박스 안의 linked worktree와 git hooks

LLM 에이전트가 함부로 날뛰지 못하게 linked worktree를 nono, Docker, Linux의 Landlock, macOS의 Seatbelt 등으로 샌드박싱하는 경우를 가정합니다. main working tree 대부분의 접근을 막아 두면 linked worktree는 .git/hooks를 읽지도 쓰지도 못합니다. git rev-parse --git-path hooks가 main working tree의 .git/hooks를 가리키기 때문인데, hooks는 모든 worktree가 공유하는 자원입니다. 예전 방식은 pre-commit hook을 체크인되지 않는 .git/hooks에 심링크하는 것이었지만, 해결책은 hooks를 체크인되는 디렉터리(.githooks)에 두고 git config core.hooksPath .githooks로 '상대 경로'를 지정하는 것입니다. git은 hook을 실행하기 전에 각 worktree 자체의 루트로 chdir하므로, 상대 경로가 샌드박스 안쪽의 같은 worktree 디렉터리로 해석됩니다. 이 설정은 리포지터리의 공유 config에 저장되므로 한 번만 설정하면 현재와 미래의 모든 worktree에 적용되고, 새 체크아웃에서도 git config 실행만으로 hooks를 바로 쓸 수 있어 심링크보다 간편합니다.

정리

현재 worktree의 루트는 git rev-parse --show-toplevel, 공통 .git 디렉터리는 git rev-parse --git-common-dir, 활성 hooks 디렉터리는 git rev-parse --git-path hooks로 찾습니다. 결국 기억할 것은 하나, '.git은 디렉터리일 수도 포인터일 수도 있으며 어느 쪽인지는 내가 어디에 있느냐에 달렸다'입니다.

Reddit 반응

  • @masklinn — 제 생각에 'main working tree'를 두는 것 자체가 안티패턴입니다. 동작이 분기되고, 'worktree 하나'를 지우다가 리포지터리 전체를 날릴 위험이 생깁니다. bare repository를 만들고 거기서 worktree들을 파생시키세요. 어떤 worktree도 우선권을 가질 이유가 없고, 그러면 git rev-parse --git-common-dir이 정확히 모든 worktree의 공통 리포지터리를 가리킵니다. worktree 중첩에서 오는 경로 혼란도 피할 수 있습니다.
  • @NationalOperations — 깔끔한 글이네요. 제가 겪는 시나리오는 아니지만, 누군가의 환경이 git을 더 많이 인식하는 작업을 한다면 짚어둘 만한 내용입니다.
  • @mb862 — 저도 worktree에서 이상한 함정을 겪는 중입니다. 지금 회사의 15년 된 SVN 리포지터리를 Git+GitHub으로 마이그레이션하는 작업을 맡고 있는데, 체크아웃이 약 60-70GB이고 언제든 최소 두 개, 보통 세 개의 활성 릴리스 브랜치에 trunk까지 유지합니다. 개발자마다 각 릴리스를 로컬에 체크아웃해 두는 게 일반적이고, Windows, macOS, Linux를 모두 지원해야 해서...
  • @OffbeatDrizzle — 왜 직접 스크립트를 만드나요? 그냥 실제 git 명령어를 쓰면 되는데.

원문: Olaf Alders(olafalders.com) / 번역·요약: Trawling