브랜치와 수술

히스토리에서 파일 제거

git rm은 파일이 새 커밋에 나타나는 것을 멈춰요. 이미 만들어진 커밋에는 아무 일도 하지 않아요. 블롭은 여전히 객체 데이터베이스 안에, 여전히 모든 클론 안에, 여전히 git show 한 번 거리에 있어요.

그게 두 경우에 문제가 돼요. 그 파일이 자격 증명이었을 때, 그리고 400 MB였을 때.

⌘K → 히스토리에서 파일 제거, 또는 파일 우클릭 — 프로젝트 트리에서, 커밋의 파일 목록에서, 또는 커밋 작성기에서. 어떤 파일을 삭제한 커밋이야말로 그것이 아직 히스토리에 남아 있다는 사실을 누군가 깨닫는 자리이므로, 빠져나갈 길도 그 메뉴 안에 있어요.

경로 찾기

들어가는 길은 두 개예요. 서로 다른 질문에 답하기 때문이죠.

직접 입력하기 — 저장소 기준 상대 경로, 앞에 슬래시 없이 — 무엇을 지우러 왔는지 이미 아는 경우예요.

히스토리 훑어보기는 모를 때 써요. 지금까지 커밋된 적이 있는 모든 경로를 무거운 순으로, 각각 몇 개의 버전이 있는지와 아직 추적되고 있는지를 함께 나열해요. 삭제된 경로는 그렇게 표시되며, 보통 그것이 여러분이 찾는 대상이에요. 작업 트리에서는 사라졌지만 모든 클론에는 남아 있는 파일이야말로 평범한 파일 대화 상자가 보여 줄 수 없는 경우거든요. 고를 파일이 거기 없으니까요.

같은 목록은 사람들이 여기 오는 다른 이유 — 왜 이 클론이 2기가바이트지 — 에도 답해 줘요. 각 경로의 블롭이 실제로 차지하는 바이트 순으로 정렬되어 있기 때문이에요. 행을 하나 고르면 곧바로 측정해요.

지금까지 커밋된 모든 경로를 무거운 순으로, 삭제된 것은 표시해서

동의하기 전에 측정하기

측정을 누르세요(또는 행을 하나 고르세요). 아직 아무것도 쓰이지 않아요. 다음을 얻어요.

재작성될 커밋 그 파일을 담고 있던 첫 커밋부터 그 이후의 모든 커밋
브랜치 / 태그 움직이게 될 ref
블롭이 차지한 용량 그 버전들이 실제로 차지하는 바이트
첫 커밋 재작성이 시작되는 지점 — 그 뒤의 모든 것이 새 해시를 받아요

측정 결과: 재작성될 커밋, 영향받는 ref, 차지한 바이트, 그리고 어차피 비밀 키를 교체하라는 경고

개수가 0이라면 경로가 틀린 것이에요. 보통은 없어서가 아니라 철자나 디렉터리 접두사 문제예요.

재작성이 실제로 하는 일

Gitcito는 모든 브랜치와 태그를 refs/gitcito/pre-purge/<timestamp>/…로 복사한 다음, 이렇게 실행해요.

git filter-branch --force \
  --index-filter 'git rm --cached --ignore-unmatch -- <path>' \
  --prune-empty --tag-name-filter cat -- --branches --tags

--index-filter는 커밋마다 체크아웃하는 대신 인덱스를 직접 재작성해요. 몇 분과 몇 시간의 차이죠. --all이 아니라 --branches --tags인 것은 의도적이에요. --all은 백업 ref까지 포함하게 되어, 재작성이 자기 안전망을 삼켜 버릴 테니까요.

제거된 파일 말고는 아무것도 담고 있지 않던 커밋은 버려져요(--prune-empty). 태그는 재작성된 커밋을 다시 가리키게 돼요.

백업, 그리고 왜 공간이 아직 돌아오지 않는가

퍼지는 되돌릴 수 있고, 그 대가는 여러분이 말하기 전까지는 디스크 공간이 회수되지 않는다는 것이에요. 백업이 존재하는 동안에는 옛 커밋들이 여전히 도달 가능하므로, git은 그것들을 수집하지 않아요.

동작 효과
복원 모든 브랜치와 태그가 퍼지 이전 커밋으로 돌아가고, 파일도 그와 함께 돌아와요
백업 버리기 백업 ref를 지우고, reflog를 만료시키고, git gc --prune=now를 실행해요 — 공간이 돌아오고, 퍼지는 이제 영구적이에요

한 단계가 아니라 두 단계인 이유는, 첫 번째는 회복 가능한 쪽이고 두 번째는 그렇지 않기 때문이에요.

어차피 비밀 키는 교체하세요

자격 증명이 한 번이라도 푸시되었다면, 히스토리를 재작성해도 유출이 없던 일이 되지는 않아요. 누군가 이미 가져갔을 수 있고, 포지 서버는 참조되지 않는 객체를 한동안 들고 있으며, CI 로그가 그것을 출력했을 수도 있어요. 재작성은 더 퍼지는 것을 막을 뿐, 노출을 되돌리지는 않아요.

키를 교체하세요. 그런 다음 퍼지해서, 다음에 클론하는 사람이 그것을 발견하지 않게 하세요.

하지 않는 일

  • 푸시하지 않아요. 재작성은 로컬 작업이에요. 결과를 공개한다는 것은 영향받은 모든 브랜치에 강제 푸시한다는 뜻이고, 나머지 사람들은 모두 다시 클론하거나 하드 리셋해야 해요 — 그 결정이 사는 곳은 강제 푸시 가드이에요.
  • 작업 트리가 더럽거나 머지/리베이스 중이면 거부해요. 재작성은 HEAD를 여러 번 움직이는데, 커밋되지 않은 작업을 두고 그러는 것이 바로 그 작업을 잃는 방법이거든요.
  • 내용이 아니라 경로 기준으로 재작성해요. 자기 파일에 들어 있는 대신 소스 파일 안에 붙여 넣어진 비밀 키를 제거하려면 내용 필터가 필요해요. 그건 git filter-repo --replace-text의 영역이고, Gitcito는 그것을 감싸지 않아요.
  • filter-branch는 아주 큰 히스토리에서는 느려요. 하지만 어디서나 git과 함께 딸려 오는 도구이고, 그래서 Gitcito가 그것을 써요. 커밋이 수만 개인 저장소라면 터미널에서 쓰는 git filter-repo가 더 빠른 도구예요.
  • 다른 사람의 클론은 여러분의 저장소가 아니에요. 그들은 다시 클론하기 전까지 옛 히스토리를 그대로 갖고 있어요.

GitHub에서 이 번역 개선하기