[Git] shallow clone에서 merge-base와 태그 이력 안전하게 복원하기
Git shallow clone에서 merge-base와 git describe 실패를 진단하고, deepen, tag fetch, unshallow를 단계적으로 적용·검증하는 방법을 설명합니다.
앞선 글 브랜치·rebase·push·PR 병합 흐름은 두 branch의 공통 조상을 찾을 수 있다는 전제에서 rebase와 병합을 설명했습니다. Git maintenance 글도 이미 가진 commit graph를 빠르게 탐색하는 방법이지, CI가 내려받지 않은 부모 commit을 되살리는 절차는 아니었습니다. 이번 글은 이 두 전제가 깨지는 shallow clone을 진단하고, 전체 이력을 무조건 받기 전에 필요한 두 branch만 단계적으로 확장합니다.
대상 환경과 실패 증상
GitHub Actions의 actions/checkout@v7 또는 git clone --depth=1로 받은 Windows·Linux 작업 공간을 가정합니다. PR 변경 범위를 계산하는 git merge-base origin/main HEAD가 출력 없이 종료 코드 1로 끝나거나, git describe --tags가 태그를 찾지 못해 버전·변경 로그 생성이 실패하는 상황입니다.
먼저 비슷해 보이는 세 가지 축을 분리합니다
| 기능 | 줄이는 대상 | 대표 증상 |
|---|---|---|
| shallow clone | commit 부모 이력 | merge-base·describe·changelog 실패 |
| sparse-checkout | working tree 경로 | 필요한 디렉터리가 checkout되지 않음 |
| partial clone | blob·tree 같은 객체 | 파일 접근 때 지연 fetch 발생 |
파일이 모두 보여도 commit ancestry는 끊겨 있을 수 있습니다. 반대로 sparse-checkout이라고 해서 commit 이력이 얕은 것은 아닙니다. 이번 증상은 먼저 shallow 여부로 판정합니다.

1. 작업을 바꾸기 전에 shallow 여부와 ref를 기록합니다
git --version
git rev-parse --is-shallow-repository
git status --short
git branch -avv
git rev-list --count HEAD기대 관찰값: 두 번째 명령이 true이고 depth 1이면 HEAD에서 보이는 commit 수는 대개 1입니다. status가 비지 않으면 fetch 자체가 working tree를 덮지는 않더라도, 이후 진단 결과와 로컬 변경을 섞지 않도록 별도 작업 공간에서 재현합니다.
2. 실패를 감추지 말고 종료 코드와 경계를 봅니다
git merge-base origin/main HEAD
$mergeBaseExit = $LASTEXITCODE
git describe --tags HEAD
$describeExit = $LASTEXITCODE
git log --oneline --decorate --boundary -20진단 분기: merge-base 출력이 비고 종료 코드가 1이면 두 ref 사이에 보이는 공통 조상이 없습니다. 실제로 unrelated history일 수도 있지만 shallow가 true라면 먼저 경계 밖 조상을 의심합니다. git describe --always는 태그가 없어도 SHA로 성공하므로 문제를 숨길 수 있습니다. 태그 기반 버전이 요구사항이라면 --always 성공을 복구 증거로 쓰지 않습니다.
3. 비교 대상 ref 자체가 없으면 tip부터 명시적으로 받습니다
git show-ref --verify refs/remotes/origin/main
git fetch --depth=1 --no-tags origin `
"refs/heads/main:refs/remotes/origin/main"
git show --no-patch --oneline origin/main이 단계는 origin/main의 tip을 만드는 것뿐 부모 이력을 완성하지 않습니다. ref가 생겼다는 사실과 공통 조상이 생겼다는 사실을 구분해야 합니다. fork PR처럼 head branch가 현재 origin에 없으면 임의 SHA fetch를 반복하지 말고 CI가 제공하는 정확한 head ref와 권한을 확인합니다.
4. 전체 이력보다 먼저 필요한 두 branch만 단계적으로 깊게 만듭니다
$headBranch = 'feature/report'
git fetch --deepen=50 --no-tags origin `
"refs/heads/main:refs/remotes/origin/main" `
"refs/heads/$headBranch:refs/remotes/origin/$headBranch"
git merge-base origin/main "origin/$headBranch"
git rev-parse --is-shallow-repository기대 관찰값: 공통 조상이 최근 50 commit 안에 있다면 merge-base가 한 SHA를 출력합니다. 저장소는 여전히 shallow이므로 마지막 값은 true일 수 있습니다. 공통 조상이 없으면 50을 100처럼 관찰 가능한 단위로 늘리되, 네트워크 전송량과 job 시간을 함께 기록합니다.
중단 기준: 두 번 깊이를 늘려도 merge-base가 없고 ref 선택이 맞다는 증거가 없다면 무작정 unshallow하지 않습니다. base/head ref가 뒤바뀌었는지, fork 권한이나 force-push로 예상 이력이 사라졌는지 먼저 확인합니다.
5. tags fetch와 commit 이력 복원을 같은 것으로 보지 않습니다
git fetch --tags origin
git tag --list --sort=-version:refname | Select-Object -First 10
git rev-parse --is-shallow-repository
git describe --tags HEAD태그 ref를 받았어도 HEAD에서 그 태그까지의 부모 경로가 shallow 경계에서 끊기면 describe는 여전히 실패할 수 있습니다. 반대로 경계 안에 가까운 태그가 있으면 성공할 수 있으므로, 성공 여부만 보지 말고 예상 태그와 거리까지 검증합니다.
6. 전체 ancestry가 요구사항일 때만 unshallow합니다
if ((git rev-parse --is-shallow-repository) -eq 'true') {
git fetch --unshallow --tags origin
} else {
git fetch --tags origin
}
git rev-parse --is-shallow-repository
git merge-base origin/main HEAD
git describe --tags HEAD기대 관찰값: 완전한 source에서 가져왔다면 shallow 값은 false가 되고 merge-base와 예상 태그 기반 describe가 모두 성공합니다. source 자체가 shallow이면 공식 문서상 source가 가진 만큼만 받을 수 있으므로 false가 되지 않을 수 있습니다. 이때 다른 mirror나 upstream을 선택해야 합니다.
격리 실험 결과
Git 2.54.0.windows.1: main과 feature/report가 C3에서 갈라지고 C2에 v1.0.0 태그가 있는 로컬 bare remote를 만들었습니다. 두 tip을 depth 1로 받은 worker에서 shallow=true, HEAD commit 수 1, merge-base 종료 코드 1, describe 종료 코드 128을 관찰했습니다. 두 ref에 --deepen=2를 적용하자 C3 merge-base는 나타났지만 저장소는 shallow=true였고 태그는 여전히 도달 불가였습니다. --unshallow --tags 뒤 false와 v1.0.0-3-g<sha>를 확인했으며 working tree는 clean이었습니다. 실제 원격 전송량과 CI 시간은 대상 저장소에서 별도 측정해야 합니다.
7. GitHub Actions에서는 목적에 맞는 fetch-depth를 선언합니다
- uses: actions/checkout@v7
with:
fetch-depth: 50
fetch-tags: true최근 변경 범위만 필요하고 공통 조상이 보통 50 commit 안에 있다면 먼저 이 값을 실측합니다. 태그 기반 버전은 태그 ref뿐 아니라 도달 가능한 ancestry도 필요합니다.
- uses: actions/checkout@v7
with:
fetch-depth: 0release note, 전체 tag 거리, 장기간 갈라진 branch 비교처럼 완전한 이력이 계약이라면 0을 명시합니다. 이 0은 actions/checkout 입력에서 전체 이력을 뜻하며, Git CLI의 git clone --depth=0과 같은 문법이 아닙니다.
8. 깊어진 저장소를 reset으로 다시 얕게 만들지 않습니다
git status --short
git stash list
git for-each-ref --format="%(refname) %(objectname)" refs/heads refs/tags
git clone --depth=50 --no-single-branch <repository-url> repo-shallow-new롤백 기준: deepen과 unshallow는 commit을 재작성하지 않지만 객체와 이력을 추가합니다. git reset은 이를 이전 depth로 되돌리지 않습니다. CI처럼 버리는 작업 공간은 job 종료로 정리하고, 장기 clone을 다시 줄여야 한다면 clean 상태·stash·로컬 전용 branch·tag를 기록한 뒤 새 폴더에 원하는 depth로 재복제합니다. 새 clone의 ref와 빌드를 검증하기 전 기존 폴더를 삭제하지 않습니다.
운영 판단표
| 관찰 | 판단 | 조치 |
|---|---|---|
| shallow=false | depth 원인 아님 | ref·태그·unrelated history 재진단 |
| deepen 뒤 merge-base 발견 | 필요 이력 충족 | 해당 depth를 CI 기준선으로 검토 |
| tag ref는 있으나 describe 실패 | ancestry 경계 남음 | 추가 deepen 또는 unshallow |
| source도 shallow | 완전 복원 불가 | 완전한 upstream·mirror 선택 |
마무리
- 파일 목록보다 먼저
--is-shallow-repository로 commit 경계를 확인합니다. - base와 head ref를 고정하고 필요한 두 이력만 작은 단위로 deepen합니다.
- tags fetch 성공과 태그까지의 ancestry 복원을 별도로 검증합니다.
- 전체 이력이 작업 계약일 때만 unshallow 또는 Actions의 fetch-depth 0을 사용합니다.
- 깊어진 장기 clone을 줄일 때는 reset이 아니라 검증 가능한 새 shallow clone으로 교체합니다.
공식 출처
- Git 공식 문서: git-clone — depth, single-branch, no-single-branch와 shallow clone 범위
- Git 공식 문서: git-fetch — depth, deepen, shallow 경계, unshallow, tag fetch
- Git 공식 문서: git-rev-parse — --is-shallow-repository 판정
- Git 공식 문서: git-merge-base — 두 commit의 최적 공통 조상
- Git 공식 문서: git-describe — commit에서 도달 가능한 최신 태그 기반 이름
- Git 공식 문서: git-rev-list — commit 수와 boundary 관찰
- Git User Manual: shallow clone — merge base가 경계 밖일 때 merge 기반 작업의 한계
- GitHub 공식 저장소: actions/checkout — 기본 단일 commit, fetch-depth와 fetch-tags 입력