Git에서 이미 추적 중인 파일을 .gitignore에 반영하기 — rm –cached, skip-worktree, assume-unchanged 실측

빌드 산출물이나 .env 같은 파일을 실수로 커밋한 뒤 뒤늦게 .gitignore에 추가해도, git은 그 파일을 계속 추적하고 변경을 M으로 보여준다. .gitignore는 아직 추적하지 않는(untracked) 파일에만 적용되기 때문이다. 흔히 쓰는 git rm --cached는 내 작업 디렉터리에서는 안전하지만, pull하는 동료의 파일을 지운다는 점은 잘 알려져 있지 않다.

이 글에서는 원격 저장소 하나와 클론 세 개(alice, bob, carol)로 협업 상황을 재현해 git rm --cached, skip-worktree, assume-unchanged가 각각 어떻게 동작하는지 git 2.43에서 확인한다.

상황 재현

git init -q --bare -b main origin.git
git clone -q origin.git alice && cd alice
mkdir build
echo "db_password=secret" > .env
echo "debug=false"        > app.conf
echo "obj"                > build/main.o
echo "int main(void){}"   > main.c
git add . && git commit -qm "initial" && git push -q origin main
cd .. && git clone -q origin.git bob && cd alice
$ printf ".env\nbuild/\n" > .gitignore
$ echo "db_password=changed" > .env; echo "obj2" > build/main.o
$ git status --short
 M .env
 M build/main.o
?? .gitignore

무시 규칙을 추가했는데도 두 파일의 변경이 그대로 잡힌다.

추적 중인데 무시 규칙에 걸리는 파일 찾기

$ git check-ignore -v .env build/main.o; echo rc=$?
rc=1
$ git check-ignore -v --no-index .env build/main.o
.gitignore:1:.env	.env
.gitignore:2:build/	build/main.o
$ git ls-files -ci --exclude-standard
.env
build/main.o

check-ignore는 추적 중인 파일을 무시 대상이 아니라고 답하므로, 규칙이 맞는지 확인하려면 --no-index를 붙여야 한다. ls-files -ci는 “추적 중(-c)이면서 무시 규칙에 걸리는(-i)” 파일 목록을 바로 보여준다.

git rm –cached로 추적 해제

$ git rm -r --cached .env build/
rm '.env'
rm 'build/main.o'
$ git add .gitignore && git status --short
D  .env
A  .gitignore
D  build/main.o
$ git commit -qm 'stop tracking .env and build/' && git push -q origin main
$ ls -A
app.conf
build
.env
.git
.gitignore
main.c

인덱스에서만 지우므로 alice의 작업 디렉터리에는 파일이 남는다. 문제는 이 커밋을 받는 쪽이다.

$ cd ../bob && ls -A
app.conf
build
.env
.git
main.c
$ git pull -q && ls -A
app.conf
.git
.gitignore
main.c

bob에게 이 커밋은 “.env와 build/ 삭제”이므로 pull하는 순간 bob의 로컬 파일이 지워진다.

$ git log --oneline --all -- .env
038fbc3 stop tracking .env and build/
019006c initial
$ git show HEAD~1:.env
db_password=secret

추적을 해제해도 과거 커밋의 내용은 그대로 남는다.

한꺼번에 해제하기

$ echo '*.log' >> .gitignore          # debug.log가 이미 커밋된 상태
$ git ls-files -ci --exclude-standard -z | xargs -0 git rm --cached
rm 'debug.log'
$ git status --short
 M .gitignore
D  debug.log

흔히 쓰는 git rm -r --cached . && git add .도 결과는 같지만 저장소 전체를 인덱스에서 뺐다가 다시 넣으므로, 무시 대상만 골라내는 위 방식이 변경 범위를 확인하기 쉽다.

로컬 변경만 숨기기: skip-worktree와 assume-unchanged

파일은 저장소에 두고 내 수정만 커밋에서 빼고 싶을 때 쓰는 두 플래그를, upstream이 같은 파일을 바꾼 상황에서 비교했다.

### bob: skip-worktree
$ echo 'debug=true' > app.conf && git update-index --skip-worktree app.conf
$ git status --short; git ls-files -v app.conf
S app.conf
### carol: assume-unchanged
$ echo 'debug=true' > app.conf && git update-index --assume-unchanged app.conf
$ git status --short; git ls-files -v app.conf
h app.conf
### alice가 app.conf를 바꿔 push한 뒤 bob, carol 모두
$ git pull -q; echo rc=$?
error: Your local changes to the following files would be overwritten by merge:
	app.conf
Please commit your changes or stash them before you merge.
Aborting
rc=1
$ git stash; git pull -q; echo rc=$?
No local changes to save
error: Your local changes to the following files would be overwritten by merge:
	app.conf
Please commit your changes or stash them before you merge.
Aborting
rc=1
$ git update-index --no-assume-unchanged app.conf && git stash -q && git pull -q && git stash pop -q
Auto-merging app.conf
CONFLICT (content): Merge conflict in app.conf
The stash entry is kept in case you need it again.

플래그가 걸린 파일은 stash도 변경으로 보지 않아 “stash하라”는 안내를 따를 수 없다. 플래그를 풀어야 stash할 수 있고, 그 뒤에는 일반적인 충돌 해결로 이어진다.

상황별 선택

목적방법다른 사람에게 미치는 영향
실수로 커밋한 산출물·비밀 파일을 저장소에서 빼기git rm --cached + .gitignorepull하면 그 파일이 삭제된다
설정 파일의 개인 수정을 커밋에서 빼기app.conf.example을 추적하고 app.conf는 ignore없음 (각자 복사해서 사용)
위 구조로 바꾸기 전 임시 조치--skip-worktree없음. 단 upstream 변경 시 pull이 막힌다
대용량 파일의 상태 검사 생략(성능용)--assume-unchanged없음. 변경 숨김 용도로 쓰지 말 것
나만 쓰는 파일 무시.git/info/exclude없음 (커밋되지 않음)
$ echo 'notes.txt' >> .git/info/exclude && touch notes.txt && git status --short
 M app.conf

notes.txt는 목록에 나오지 않는다(app.conf는 앞 실험에서 플래그를 해제한 수정분이다).

주의사항

  • git rm --cached 커밋을 push하기 전에 동료에게 해당 파일을 백업하라고 알린다. 이미 지워졌다면 git show HEAD~1:경로 > 경로처럼 이전 커밋에서 복원할 수 있다.
  • 비밀값이 커밋됐다면 추적 해제로는 해결되지 않는다. 먼저 키·비밀번호를 폐기·교체하고, 히스토리에서 지우려면 git filter-repo로 다시 쓴 뒤 force push한다(모든 클론에 영향).
  • skip-worktree/assume-unchanged는 로컬 인덱스 플래그라 클론마다 따로 설정해야 하고 push되지 않는다. 걸려 있는 파일은 git ls-files -v | grep '^[Sh] '로 찾는다.
  • assume-unchanged는 “이 파일을 검사하지 않아도 된다”는 성능 힌트다. git 공식 문서는 두 플래그 모두 추적 파일의 로컬 변경을 무시하는 용도로 쓰지 말라고 하며, 템플릿 파일 방식을 대안으로 든다.

마무리

이미 추적 중인 파일에 .gitignore를 적용하려면 git rm --cached가 맞지만, 그 커밋은 다른 클론에서 파일 삭제로 동작한다. 개인 설정 파일이라면 템플릿 파일을 추적하는 구조로 바꾸는 것이 근본 해결이고, skip-worktree는 그 전까지의 임시 조치로만 쓴다.

참고

답글 남기기