본문 바로가기
연구소👨‍💻/CS연구소

[GIT] 깃 특정파일을 지워야할때 필요한 작업들

by 신그자체김상범 2024. 5. 26.

 

깃은 굉장히 쓰기 쉬운 형상관리 툴이다. 내 파일 전체의 상태를 매번 기억해 놓는 것이 아니라 커밋별로 변경내역을 저장해 놓기 때문에 가볍고 편하다.

근데 이런 특성 때문에 올라가면 안되는 파일이 이미 커밋됐고 그걸 모른채로 개발을 하게되면 나중에 여러 문제가 생긴다.

크게 두가지로 나눌 수 있다

  • 필요없는 파일이 올라간 경우
  • 올라가면 안되는 파일이 올라간 경우

필요없는 파일이 올라가는 경우

필요 없는 파일이 올라가는 경우는 비교적 대처하기 쉽다.  누가 파일을 지운 커밋을 올린뒤 머지 하기만 하면 된다.

예를 들어 이런 상황이있다.

쓸데없는 파일 a가 시작지점에 모든 원격저장소 사용자들한테 뿌려졌어 내가 그걸 눈치채고 내 컴퓨터에서 내 브랜치에서 파일을 지우고 올려. 그런데 다른사람들은 그걸 눈치 못채서 다른 브랜치에 파일이 남아있는채로 커밋을 막 찍었어

 

그러다가 이제 나의 브랜치와 상대방의 브랜치를 develop에 머지했다면 그 쓸데없는 파일 a는 지워져 안지워져?

 
git을 한번 봤던 사람들이면 당연히 이 쓸데없는 파일 a는 지워져야 할 것 이라고 생각한다. 왜냐하면

git은 기본적으로 매 커밋단위로 파일의 생성, 수정, 삭제를 기록하기 때문에 파일 a가 생성됐다는 커밋은 분기된 두 브랜치의 공통조상 위에 존재할 수 밖에 없고 이후에 존재하는 a와 관련된 커밋이라곤 왼쪽의 커밋밖에 없기 때문이다. 그렇기 때문에 두 브랜치를 합친다면 

이런 모양이 될 것이고 그럼 파일 a는 삭제될 수 밖에 없다.
 
그런데 이제 막상 개발을 하다보면 내 브랜치에서 분명히 파일을 지워도 다른사람이 merge를 할 때마다 같은 파일이 계속 생성되는걸 봤다. 이게 이해가 안돼서 해결도 못하고 방치해놓곤 했는데.

한가지 가정을 한 것이 다음과 같은 시나리오였다

보다시피 브랜치3 은 파일 a를 생성했다는 이력을 가지고 있다. 브랜치 2도 파일 a 생성 이력을 가지고 갔지만 이후 삭제했다. 그런데 브랜치 3이 develop에 push하기 위해서 브랜치 2를 rebase를 True로 해놓은 상태에서 pull 받는다면 내 커밋 이력 이후에 자신의 커밋을 다른 커밋 이력으로 덮어 씌우기 하기 때문에 아예 새로운 파일 a 생성 커맨드가 생기고 그것이 push과정에서 반영되는 것이 아닐까? 하는 생각이었다.

하지만 Rebase의 기본 구조는 공통 조상에서 부터 다른 것들을 덮어 씌우기 하기 때문에 파일 a 생성 커맨드는 이미 develop에 존재하는 커밋 이력이고 따라서 파일은 삭제되어야 한다.

그런데도 만약 다른사람이 분기한 커밋에서 누가 파일을 다른 폴더로 옮기거나 수정을하면 삭제 커밋을해도 파일이 남아있을 수 있는 것 같다.

올라가면 안되는 파일이 올라가는 경우

개발초기에 실수로 많은 사람들이 aws 키 같은 암호 정보를 원격 public 저장소에 올려놓고 방치해두는 경우가있다 실제로 이것때문에 몇천만원 결제내역이 떠서 애좀 먹었던 사람을 보기도했다. 근데 문제는 깃의 특성상 파일을 삭제하고 커밋하는걸론 정보유출을 막을 수 없다. 이전 커밋으로 돌리면 바로 파일이 돌아오기 때문이다.

즉 이문제를 해결하려면 이전 모든 커밋에서 이 파일을 지워야한다는 의미가 된다. 그런데 여기서 사용하는 명령어들이 생각보다 복잡했어서 이 부분을 알기 쉽게 설명하고 싶다.

 

일단 깃허브에 올리면 안될 정보를 올리면 다음과 같은 에러가 발생한다.

remote: Resolving deltas: 100% (4683/4683), completed with 21 local objects.
remote: error: GH013: Repository rule violations found for refs/heads/develop.
remote:
remote: - GITHUB PUSH PROTECTION
remote:   —————————————————————————————————————————
remote:     Resolve the following violations before pushing again
remote:
remote:     - Push cannot contain secrets
remote:
remote:
remote:      (?) Learn how to resolve a blocked push
remote:      https://docs.github.com/code-security/secret-scanning/pushing-a-branch-blocked-by-push-protection
remote:
remote:      (?) This repository does not have Secret Scanning enabled, but is eligible. Enable Secret Scanning to view and manage detected secrets.      
remote:      Visit the repository settings page, https://github.com/TEAM-Ving/BE/settings/security_analysis
remote:
remote:
remote:       —— Amazon AWS Access Key ID ——————————————————————————
——————————————————————————
remote:        locations:
remote:          - blob id: 1783249fa008c887566e975f713a0d564d7c5a77
remote:          - blob id: a3a5d375ad03452901562ca26d920a77d3b156b3
remote:
remote:        (?) To push, remove secret from commit(s) or follow this URL to allow the secret.
remote:        https://github.com/TEAM-Ving/BE/security/secret-scanning/unblock-secret/2gthNcSZimRBHV2ZhvcY2dRx4D8
remote:
remote:
remote:       —— Amazon AWS Access Key ID ——————————————————————————

커밋에서 파일 찾기

눈에 보이지 않는 이전 커밋에서 파일을 삭제하는 작업은 당연히 어렵다. 일단 그 파일을 수정판 이력이 있는지 확인하는 명령어 부터 봐보자.

git log -- <파일경로>

이 명령어를 사용하면 위와같은 오류를 발생시키는 것으로 예상되는 파일의 이력을 확인할 수 있다.

 

깃의 BLOB ID

그런데 위의 에러 사항을 확인해보면 에러 location에서 blob id라는 것을 확인할 수 있다.

일단 blob id 자체는 커다란 이진 객체라는 뜻으로 마치 키처럼 너무 큰 데이터들을 가리키는 역할이라고 한다. 

 

이렇게 blob id로 된 객체를 받으면 그것을 기반으로 log를 뒤지면 되겠다고 생각했지만 실제로는 그렇지 않았다. 저 blob id는 특정 커밋 아이디를 가리키고 있고 그자체는 파일 경로가 아니기 때문에 어떤 파일에 문제가 생겼는지 모를때 어려움이 있었다.

 

여기서 약간 GPT의 힘을 빌려보면

git log --all --full-history -- **/* | grep -B 2 -A 2 "1783249fa008c887566e975f713a0d564d7c5a77"

이런 명령어를 사용하라고 한다. 이걸 하나하나 분리해보면

git log : 커밋이력을 보여달라는 내용

--all : 모든 선택지를 보여달라는 내용?

--full-history : 모든 히스토리를 보여준다는 내용, 보통 git log가 간소화된 히스토리를 보여주기 때문에 사용한다.

--**/* : 이 ** / * 은 깃에서 모든 파일을 의미한다고 한다 앞의 **는 디렉토리 트리, 뒤의 *는 파일이다.

| :파이프 설정, git명령어의 출력을 | 뒤의 명령어에 적용하는 것이다.                                                                                                        

grep : 리눅스에서 문자열을 검색할 때 활용하는 명령어

-B  2: 찾은 내용의 이전 두줄

-A  2 : 찾은 내용의 이후 두줄

즉 모든 로그를 출력하도록 하고 그 출력값의 일부를 grep명령어를 통해 검색해 출력하는 것으로 보인다.

좀 더 쉬운 방식을 위해선

git log -S "1783249fa008c887566e975f713a0d564d7c5a77"
git log -G "1783249fa008c887566e975f713a0d564d7c5a77"
git log --grep="fix bug"

다음과 같은 방식들이 있는데 

-S는 문자열 을 포함하는 커밋을 감시하고

-G는 정규식을 사용해 일치하는 커밋을 검색

-grep은 이전 커밋에서 문자열 검색을 하는 역할을 한다.

 

특정 파일의 blob ID를 구하는 방식으로는 다음과 같은 방식이 있다

git log --all --full-history --pretty=format:"%H" -- <파일 경로> | while read commit_hash; do
  git ls-tree -r $commit_hash | grep <blob ID>
done

마찬가지로 이해가지 않는 부분은

--pretty=format:"%H" -- <파일경로> : 출력 형식을 정하는 내용, %H는 커밋 해시만을 의미하는 것 같다.

while - do - done 으로 보이는 리눅스 CLI 반복문

commit_hash : 이전 출력값을 변수로 alias하는 과정이다. 아마 read 뒤에있기 때문에 입력값으로 들어오는 것을 Commit hash라고 부를 수 있는 것 같다.

ls-tree : log와 비슷한 명령어

-r : 하위 디렉토리까지 재귀적으로 출력

$commit_hash : 앞의 문장에서 읽어온 변수의 값

grep <blob Id> 문자열 검색

 

커밋의 내용을 바꾸는 작업

파일을 찾았다면 일단 현재 내 브랜치의 파일 지우는 일은 쉽다.

git rm --cache "파일이름"

이 코드를 실행하면 내가 설정한 파일이름을 현재 index에서 지워주면서도 --cache를 통해 내 샌드박스에서는 놔둘 수 있다.

 

다만 이전 커밋에서 파일을 지우는 것은 이것보다 어렵고 보통 라이브러리인 git filter-repo를 사용한다고한다

git filter-repo 이용법

일단 파이썬 라이브러리인 git filter-repo를 설치한다.

pip install git-filter-repo

다음엔 filter-repo 명령어를 사용해서 모든 커밋 이력에서 파일을 지운다

git filter-repo --path DJANGO/ving/ving/settings.py --invert-paths

만약 clone을 먼저 받아야 한다고 에러가 뜬다면 그 clone을 확실히 받고 작업을 수행하는것이 좋으나 귀찮을 경우엔 --force를 활용한다.

 

 

난 이런 과정을 다 겪고 나서 위에 있던 문제를 해결할 수 있었다. 한번 어긋나면 굉장히 고치기 힘든 git에 대해서 이렇게 배워가는건 항상 좋은 일 같다.