我这儿是好的 —— 子模块钉在了旧提交上
한국어 원문으로 표시합니다.
목표
라이브러리 저장소를 부모 저장소에 서브모듈로 꽂고, 갱신을 부모에 커밋하지 않았을 때 동료가 옛 코드를 받는 사고를 직접 만들어 봅니다.
왜 중요한가
서브모듈에서 나는 사고는 거의 전부 한 가지 오해에서 나옵니다 — 부모가 브랜치를
가리킨다는 오해입니다. 부모의 트리에 들어가는 것은 모드 160000 인 항목 하나이고
값은 커밋 해시입니다. 브랜치가 아니라 커밋이므로, 서브모듈은 항상 detached HEAD 로
체크아웃되고, 라이브러리에 새 커밋이 쌓여도 부모는 저절로 따라가지 않습니다. 이
실습은 그 사실을 설명으로 듣는 대신 이력에서 직접 꺼내 보게 합니다. 그리고 git clone
이 기본으로 서브모듈을 받지 않는다는 것까지 확인하면, "CI 에서만 빌드가 깨진다" 는
흔한 증상이 왜 생기는지가 한 번에 정리됩니다.
단계
/root/gitx5/lib에 커밋 2개짜리 라이브러리 저장소를 만듭니다(version.txt가1.0.0에서1.1.0으로)./root/gitx5/app을 만들고lib를vendor/lib자리에 서브모듈로 꽂습니다. 첫 시도가 막히는 것을notes/protocol.txt에 남깁니다.- 부모가 기억하는 것이 무엇인지
notes/pointer.txt에 남깁니다. lib에1.2.0커밋을 쌓고 서브모듈을 그 커밋으로 옮긴 뒤, detached HEAD 와submodule status의 앞 글자를notes/detached.txt에 남깁니다.- 그 갱신을 부모에 커밋합니다.
lib에1.3.0을 쌓고 서브모듈만 옮긴 채 부모는 커밋하지 않은 상태에서/root/gitx5/clone을 떠, 받은 쪽이 몇 번을 보는지notes/accident.txt에 적습니다.--recurse-submodules없이/root/gitx5/clone2를 떠서 앞 글자-를 만들고, 네 가지 앞 글자를notes/status.txt에 정리합니다.notes/report.md에 서브모듈을 쓸 때의 규칙을 정리합니다.
참고
- 이 이미지에는 git 신원이 전역으로 없습니다. 저장소를 만들 때마다
git config user.email과user.name을 지정하세요. - 커밋 시각은
GIT_AUTHOR_DATE와GIT_COMMITTER_DATE로 고정합니다. - 흔한 실수: 2단계에서 첫 시도가 막히면 실습이 잘못된 줄 알고 넘어가는 것입니다. 막히는 것이 정상이고, 그 이유를 아는 것이 이 단계의 과제입니다.
- 흔한 실수: 6단계에서 부모를 커밋해 버리는 것입니다. 커밋하지 않은 상태 그대로 clone 을 떠야 사고가 재현됩니다.
공통 라이브러리 저장소 만들기
/root/gitx5/lib 에 커밋 2개짜리 라이브러리 저장소를 만듭니다(version.txt 가 1.0.0 에서 1.1.0 으로).
git init -b main 으로 만들고 저장소마다 user.email·user.name 을 정합니다. version.txt 한 줄만 바꿔 커밋 두 개를 쌓으면 됩니다 — 뒤 단계에서 이 값이 어느 커밋에 꽂혔는지를 눈으로 보는 표시가 됩니다.
로컬 경로 서브모듈이 기본으로 막힌다
/root/gitx5/app 을 만들고 lib 를 vendor/lib 자리에 서브모듈로 꽂습니다. 첫 시도가 막히는 것을 notes/protocol.txt 에 남깁니다.
git submodule add /root/gitx5/lib vendor/lib 를 그냥 하면 transport 'file' not allowed 로 막힙니다. CVE-2022-39253 이후의 기본값입니다. git -c protocol.file.allow=always submodule add ... 로 다시 하고, 꽂은 뒤에는 .gitmodules 와 vendor/lib 를 커밋까지 해야 합니다.
부모의 트리에 들어간 한 줄
부모가 기억하는 것이 무엇인지 notes/pointer.txt 에 남깁니다.
git ls-tree HEAD vendor/lib 한 줄이 서브모듈의 전부입니다. 모드가 무엇인지, 그 값이 lib 의 어느 커밋인지, 그리고 .gitmodules 에는 무엇이 적혀 있는지 세 가지를 함께 적으세요. 브랜치 이름이 어디에도 없다는 것이 요점입니다.
서브모듈은 언제나 detached HEAD 다
lib 에 1.2.0 커밋을 쌓고 서브모듈을 그 커밋으로 옮긴 뒤, detached HEAD 와 submodule status 의 앞 글자를 notes/detached.txt 에 남깁니다.
lib 에서 1.2.0 을 커밋한 뒤, app/vendor/lib 안에서 git fetch origin 하고 그 커밋으로 git checkout 합니다. 그 안에서 git status 첫 줄이 무엇인지, 그리고 부모에서 git submodule status 의 앞 글자 한 칸이 무엇으로 바뀌는지 보세요. 아직 부모를 커밋하지는 않습니다.
갱신을 부모에 커밋한다
그 갱신을 부모에 커밋합니다.
부모에서 git add vendor/lib 를 하면 gitlink 값 하나가 스테이징됩니다. 디렉터리 안의 파일들이 아니라 커밋 해시 한 줄이 바뀌는 것이라, git diff --cached 를 보면 Subproject commit 두 줄만 나옵니다.
커밋하지 않은 갱신은 나만의 것이다
lib 에 1.3.0 을 쌓고 서브모듈만 옮긴 채 부모는 커밋하지 않은 상태에서 /root/gitx5/clone 을 떠, 받은 쪽이 몇 번을 보는지 notes/accident.txt 에 적습니다.
lib 에 1.3.0 을 커밋하고 app/vendor/lib 를 그리로 옮깁니다. 부모에서 git status 를 보면 vendor/lib 가 수정된 것으로 뜨는데, 커밋하지 마세요. 그 상태로 git -c protocol.file.allow=always clone --recurse-submodules /root/gitx5/app /root/gitx5/clone 을 하고 양쪽 version.txt 를 견줍니다.
앞 글자 한 칸이 말하는 네 가지
--recurse-submodules 없이 /root/gitx5/clone2 를 떠서 앞 글자 - 를 만들고, 네 가지 앞 글자를 notes/status.txt 에 정리합니다.
git clone /root/gitx5/app /root/gitx5/clone2 는 서브모듈을 받아오지 않습니다. 그 안에서 git submodule status 를 하면 앞 글자가 무엇인지 보세요. 초기화하는 명령이 무엇인지도 함께 적으면 다음에 CI 가 깨질 때 바로 씁니다.
서브모듈을 쓸 때의 규칙
notes/report.md 에 서브모듈을 쓸 때의 규칙을 정리합니다.
이번에 직접 만든 세 가지 — 부모가 기억하는 것, detached HEAD, 갱신 누락 사고 — 를 규칙 형태로 바꿔 쓰세요. clone 할 때 무엇을 해야 하는지와, 리뷰에서 Subproject commit 한 줄짜리 diff 를 어떻게 읽을지도 함께 적습니다.