删掉不等于消失
한국어 원문으로 표시합니다.
한 줄 요약
파일을 지우고 커밋하는 것은 앞으로 그 파일이 없다 는 뜻일 뿐, 이력에 남은 객체는
그대로입니다. 실제로 없애려면 이력을 재작성하고(filter-branch), 백업 참조를 걷어내고,
reflog 를 비우고, gc --prune=now 까지 해야 합니다. 그래도 이미 퍼진 사본은 남습니다.
왜 이게 필요했나
두 가지 사고가 같은 모양입니다.
하나는 비밀입니다. 운영 토큰이 든 .env 를 실수로 커밋했다가 알아채고 지웠습니다.
저장소를 열어 보면 그 파일은 없습니다. 그런데 git log -- conf/prod.env 는 여전히
커밋 두 개를 내고, git cat-file -p <커밋>:conf/prod.env 는 토큰 원문을 그대로
찍어 줍니다. git 은 내용 주소 저장소라, 한 번 들어온 blob 은 그것을 가리키는 커밋이
남아 있는 한 사라지지 않습니다.
다른 하나는 크기입니다. 수백 MB짜리 데이터 파일이나 빌드 산출물을 한 번 커밋하면, 그 뒤에 지워도 clone 받는 사람마다 그 객체를 계속 내려받습니다. "왜 우리 저장소는 clone 에 10분이 걸리지" 의 답이 대개 여기 있습니다.
둘 다 앞으로 가 아니라 지난 이력을 고쳐야 풀립니다.
어떻게 동작하나
git filter-branch 는 지정한 ref 들의 커밋을 하나씩 다시 만들면서, 각 커밋마다 필터를
적용합니다. 파일을 들어낼 때는 인덱스만 고치는 --index-filter 가 가장 빠릅니다 —
작업 트리를 펼치지 않기 때문입니다.
FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --index-filter \
'git rm -r --cached --ignore-unmatch conf/prod.env assets/blob.bin' \
--prune-empty --tag-name-filter cat -- --all
옵션 하나하나가 이유를 갖습니다.
| 옵션 | 왜 필요한가 |
|---|---|
--ignore-unmatch |
그 파일이 없는 커밋에서 git rm 이 실패해 전체가 멈추지 않게 |
--prune-empty |
그 파일만 추가하던 커밋이 빈 커밋으로 남지 않게 |
--tag-name-filter cat |
태그가 옛 커밋을 가리킨 채 남지 않게 |
-- --all |
브랜치 하나가 아니라 모든 ref 를 재작성 |
FILTER_BRANCH_SQUELCH_WARNING |
git 이 띄우는 10초 경고를 넘기기 위해 |
여기서 대부분이 멈춥니다. 명령이 끝나고 git log 를 보면 파일이 없는데,
git rev-list --objects --all 에는 그 파일이 여전히 나옵니다. 이유는
refs/original/ 입니다. filter-branch 는 되돌릴 수 있도록 원래 ref 를 전부 그 아래
백업해 두는데, 그것도 ref 라서 옛 커밋과 옛 blob 을 붙잡고 있습니다.
그래서 순서가 있습니다.
refs/original/을 지운다 —git for-each-ref --format='%(refname)' refs/original로 목록을 뽑아git update-ref -d를 돌린다.- reflog 를 비운다 —
git reflog expire --expire=now --expire-unreachable=now --all. reflog 도 참조다. HEAD 가 지나간 자리를 전부 기억한다. git gc --prune=now로 도달 불가 객체를 지운다.--prune=now가 없으면 기본 유예 기간(2주)이 지나야 지워진다.
git count-objects -vH 로 전후를 재면 이 과정이 실제로 무엇을 했는지 숫자로 보입니다.
현장에서 만나는 모습
재작성이 끝나면 모든 커밋 해시가 바뀝니다. 커밋 해시는 부모와 트리를 포함해 계산되므로, 이력 맨 앞에서 무엇을 하나 고치면 그 뒤가 전부 달라집니다. 그래서 다음 일들이 따라옵니다.
- 팀 전원이 강제 푸시된 브랜치를 받아야 합니다. 각자가
git pull을 하면 옛 이력과 새 이력이 섞여 커밋이 두 벌이 됩니다. 재작성 직후에는 "각자 새로 clone 하라" 가 가장 안전한 안내입니다. - 열려 있던 PR 과 CI 파이프라인이 전부 깨집니다. 지목하던 커밋이 사라졌기 때문입니다.
- 서브모듈로 이 저장소를 꽂아 둔 부모가 있다면 그 핀이 끊어집니다.
그리고 가장 중요한 것 — 이미 clone 을 떠 간 사본에는 그 객체가 그대로 남습니다. 동료의 노트북, CI 캐시, 포크한 저장소, 백업에 모두 남아 있습니다. 그래서 비밀이 이력에 들어갔을 때 첫 번째로 할 일은 이력 청소가 아니라 그 비밀을 폐기하고 새로 발급하는 것입니다. 이력 청소는 두 번째입니다. 순서를 바꾸면 청소하는 며칠 동안 살아 있는 자격 증명이 밖에 있게 됩니다.
큰 저장소에서는 git filter-branch 가 느립니다. 공식 문서도 git filter-repo 나
BFG 같은 도구를 권합니다. 다만 그 도구들은 따로 설치해야 하고, 하는 일과 뒤처리
순서는 똑같습니다 — 재작성, 백업 참조 제거, reflog 만료, gc.
다음 실습에서 할 것
토큰이 든 설정 파일과 900KB짜리 덩어리가 섞인 이력을 만들고, 워킹 트리에서 지워도
git cat-file 로 원문이 나오는 것부터 확인합니다. 그다음 재작성을 돌리고, 아직
남아 있는 이유(refs/original)를 직접 찾아 걷어내고, 크기가 실제로 줄어드는 것을
git count-objects -vH 로 잽니다. 마지막으로 미리 떠 둔 동료의 사본에 그 비밀이
그대로 살아 있는 것을 보여 주고, 그래서 무엇을 먼저 해야 하는지로 끝냅니다.
공식 문서는 git-filter-branch, git-reflog, git-gc 입니다.