본문 바로가기

Trouble Shooting

[GIT 정리] 이미 commit/push된 파일을 .gitignore에 등록하기

이 글은 GIT의 기초를 설명하기 보다는 흔히 현장에서 발생할 수 있는 상황에 대처할 수 있는 실용적인 방법들을 설명하기 위한 두번째 글이다.


프로젝트를 진행하다보면 뒤늦게, 혹은 실수로 이미 소스저장소에 올려진 파일을 .gitignore에 등록해야 할 때가 생긴다. GIT에 대한 이해가 적을 때, 이 작업은 무지 난해한 작업이었다. 그러나 자세히 정리해보니 생각보다 간단한 작업이었다.


1. .gitignore 파일이 정확히 동작하는지 확인하기

.gitignore에 소스관리하지 않을 파일을 등록한 후 제대로 동작하는지 확인할 필요가 있다. 그러나 정확하게 명령을 내리지 않으면 정보가 누락되어 잘못된 판단을 할 수 있다. 우선 데모를 위해 이전회의 저장소 및 클론에서 몇가지 상황을 추가했다.
  1. 클론저장소는 클론-1 과 클론-2 두개가 존재한다.
  2. dir1/sub.txt 파일은 이미 등록되서 관리중인 파일로 이미 2개의 클론저장소 모두에 공유된 상태이다.
  3. dir1/sub.out 파일은 클론-1에서 새로 만들어진 파일로 등록되지 않은 상태이다.
  4. 클론-1에서  2,3항의 'sub.txt', 'sub.out'파일을 .gitignore에 등록 했다.
  5. 불행히도 클론-2가 'sub.txt'파일을 수정하고 있다.
클론-1에서 .gitignore파일에 필요한 내용을 넣은 후 'status'명령을 내리면 인덱스된 파일들은 반영되지 않고 'sub.out'에 대한 내용만 보이게 된다.

clone-1$ git status -s --ignored

?? .gitignore

!! dir1/sub.out


느낌표 두개는 소스관리 대상이 아님을 표시한다. 즉, .gitignore등에 등록된 파일이다. 그러나 'sub.txt'정보는 누락되었다.


check-ignore

새로 만든 파일인지 혹은 이미 등록된 파일인지 상관없이 모든 파일을 대상으로 .gitignore설정을 테스트해 보려면 아래의 2가지 방법중의 하나를 사용하면 된다.

clone-1$ git check-ignore -v --no-index **/*

.gitignore:2:*.out dir1/sub.out

.gitignore:1:sub.txt dir1/sub.txt

clone-1$ 

clone-1$ git ls-files -ico --exclude-from=.gitignore

dir1/sub.out

dir1/sub.txt


'check-ignore' 명령은 모든 ignore설정을 사용해서 테스트해준다. 그런데 '--no-index' 옵션에 주의하자 이 옵션의 뜻은 "인덱스처리(파일)을 반영하지 않고" 이다. 그래서 이 옵션이 포함되면 인덱스된 파일도 보여주고 빼면 새로 등록된 파일들만 보여준다. 이런 부분이 GIT을 어렵게 만든다. 전혀 직관적이지 않고 명령어마다 해석이 다른 옵션이다.  'ls-files' 명령은 늘 쓰는 명령이 될 것이니, 이 명령어만 알고 있어도 조금은 불편하지만 상관없을 듯하다. 여기서 '-ico'옵션은 "ignore AND (cached OR other)"의 의미이다. '-ic', '-io'로 구분해서 사용할 수도 있다. 다만 불편한 점은 '--exclude-from'옵션을 반드시 사용해줘야 한다.


테스트해본 결과 .gitignore파일은 잘 설정이 된 듯하다.


2. 파일의 삭제, 삭제된 파일 병합 처리

파일을 삭제한다는 의미에는 work-tree(우리가 작업하는 디렉토리)에서 파일을 삭제하는 것과 인덱스영역에서 파일에 대한 정보를 삭제하는 것이 있다. 우리는 .gitignore와 관련된 작업을 하고 있고, 우리가 할 일을 작업디렉토리의 파일은 그대로 놔두고 인덱스 영역의 정보만 삭제할 필요가 있다. 이 명령은 'rm --cached'를 사용하면 된다. 그리고 인덱스 영역에서 정보가 삭제되었는지 여부는 'status' 명령어 뿐만 아니라, 'diff-index'명령을 사용할 수도 있다.

diff-index

=== 인덱스 정보만 지운다. ===

clone-1$ git rm --cached dir1/sub.txt

rm 'dir1/sub.txt'


=== 실재파일은 그대로 있다. ====

clone-1$ ls dir1

binary.png sub.out sub.txt

clone-1$ 

clone-1$ 

clone-1$ git status -s

D  dir1/sub.txt

clone-1$ 

clone-1$ 

clone-1$ git diff-index HEAD

:100644 000000 c98fd588a704f4712904a46916d56115605f8f2e 0000000000000000000000000000000000000000 D dir1/sub.txt

clone-1$ 

clone-1$ git diff-index origin/master

:100644 000000 186f4046b662f2f091b9e6fa9021203f25421003 0000000000000000000000000000000000000000 D dir1/sub.txt

clone-1$ 



=== commit을 한다. ===

clone-1$ git commit -m "remove index of sub1.txt"

 


인덱스에서 정보가 사라진 파일은 실재로는 삭제되지 않았지만, 소스관리 정보상으로는 삭제된 것으로 보여진다. 'diff-index'명령의 결과를 보면 파일의 상태가 '100644'에서 '000000'으로 차이가 있음을 보여준다.

그럼 이 상황에서 클론-2에서 눈치없이 'sub.txt'파일을 수정하고 commit&push까지 했다고 가정하자, 이런 상황에서 병합이 되면 'sub.txt'파일에서 충돌이 발생한다. 'ls-files' 명령어를 이용해서 충돌을 조회해보면

clone-1$ git ls-files -u

100644 c98fd588a704f4712904a46916d56115605f8f2e 1 dir1/sub.txt

100644 186f4046b662f2f091b9e6fa9021203f25421003 3 dir1/sub.txt

clone-1$ 


로컬에서는 비록 인덱스상에서이지만, 파일을 삭제했기 때문에 stage번호가 '2'가 누락된 '1','3'만 보여진다. 만약 stage '3'으로 충돌을 복구하면 클론-2에서 작업한 내용이 반영된다. 그리고 삭제작업 상태 또한 그대로 유지된다. 그 이유는 충돌이 났을 때, 작업디렉토리에 있는 파일은 인덱스된 것들과 별개의 것이기 때문이다. 이런 파일을 'status'명령으로 보면 아래와 같다. 

clone-1$ git status -s

DU dir1/sub.txt

clone-1$ 


=== 그냥 'reset'명령어를 내리면 충돌이 처리된 것으로 된다. ===

clone-1$ git reset 

clone-1$


'DU'의 의미는 우리(클론-1)은 삭제를 했는데, 원격(origin의 최종은 클론-2가 제출함)에서는 수정작업을 한 상태를 나타낸다. 그리고 마지막에 충돌이 처리되었음을 알리기 위해 'reset'명령을 사용하면 된다.

reset

'rset'명령은 상당히 다양한 기능이 있지만, 지금은 주제에 한정된 기능만 설명한다. 'reset'명령에 아무런 옵션이 없으면 'reset --mixed'와 같다. 그리고 'reset --hard'의 명령어도 있습니다. 특정 <commit>을 지정하지 않으면 최종 커밋상태로 파일들이 복구되는데, --mixed는 인덱스 영역만 복구하고, --hard는  작업디렉토리와 인덱스영역 모두 복구 한다. 예를 들면, 앞서 'DU'상태에서 'reset --hard'명령을 사용하면 작업디렉토리의 파일마저 사라진다. 이는 우리가 원하던 바는 아니다. 그러나 실수로 파일을 삭제했을 때, '--hard' 옵션은 매우 유용하다.

clone-1$ ls

dir1 dir2 readme.txt

clone-1$ 

clone-1$ 

clone-1$ rm readme.txt 

clone-1$ ls

dir1 dir2

clone-1$


=== 삭제된 파일을 되돌릴 수 있다. / 필요시 특정파일만을 지정할 수 있다.===

clone-1$ git reset --hard 

HEAD is now at ab18bd2 change sub2.txt

clone-1$ ls

dir1 dir2 readme.txt




3. 상대방은 어찌 되는가?

사실 클론-1보다는 클론-2가 더 문제이다. 왜냐하면 병합을 하게 되면 실재로 파일이 삭제되기 때문이다. 따로 관련된 파일들을 다른 곳에 복사해 둔 다음에 삭제작업이 모두 병합되고 '.gitignore'파일도 완전히 연동된 후 다시 파일을 복구하는게 가장 쉽고 빠른 듯 하다. 

checkout-index

만약 파일을 미처 백업해두지 못했었다면 이전에 커밋된 곳에서 파일을 복구할 수 있다.   

clone-2$ git merge -m "resut of gitifnore"

Updating 15cf3fb..53ddffa

Fast-forward (no commit created; -m option ignored)

 .gitignore   | 2 ++

 dir1/sub.txt | 4 ----

 2 files changed, 2 insertions(+), 4 deletions(-)

 create mode 100644 .gitignore

 delete mode 100644 dir1/sub.txt

clone-2$ 


=== sub.txt파일이 작업디렉토리에서도 삭제된다. ===

clone-2$ ls dir1

binary.png


=== 당황하지 말고 최종본이 있는 <commit>에서 파일을 복구하자 ===

clone-2$ git checkout 15cf3fb -- dir1/sub.txt

clone-2$ 

clone-2$ ls dir1

binary.png sub.txt



GIT은 다양한 명령어 덕분(?)에 하나의 일을 하는데, 여러가지 방법이 있는 듯 하다. 방금의 경우도 'update-index --assume-unchanged'를 사용하는 방식도 있지만 도긴개긴인 듯하다.


마치며

'.gitignore' 파일이 꼬이면 번거로운 작업을 해야 되지만 생각보다 쉬운 작업이라고 본다. 한쪽에서는 인덱스만 삭제/commit/push를 해주고 다른 쪽에서는 병합후 삭제된 파일을 복구해주면 된다.