11월 22일 포스트모템 (feat. git filter branch, rebase, allow unrelated histories) - boostcampwm-2022/web33-Mildo GitHub Wiki

포스트모템이란 무엇인가?

  • 포스트모템의 원래 의미: 사람이 죽었을 때, 사인을 분석하기 위해 시체를 해부하는 과정

실패한 근본 원인을 분석하여 문서로 남기는 것이 실수로부터 배우는 핵심이다. 이를 구글은(다른 많은 회사에서도) 포스트모템이라고 한다.

  • 포스트모템 문서에는 쓸모없는 사죄, 변명, 지적으로 채워지지 않도록 각별히 주의해야 한다.
  • 제대로 된 포스트모템에는 무엇을 배웠는지와 배운 것을 토대로 앞으로 무엇을 바꿀지가 담겨야 한다.
  • 실패를 제대로 기록해두면 다른 이들도 무슨 일이 있었는지 알 수 있고 (당장 혹은 미래에) 똑같은 실수를 반복하는 일을 피할 수 있다.

TMI: 한국에서는 게임업계에서 이 용어를 많이 사용하는 것 같은데, 잘한 점도 들어있다는 것이 차이인 것 같기도 하다.

출처: 구글 엔지니어는 이렇게 일한다 https://kukim.tistory.com/140?category=898272

사건의 발단

git에 대한 에러를 해결하는 과정에서 어떤 부분에 문제가 있었는 지, 그리고 어떻게 해결을 했는 지, 앞으로는 어떻게 대비를 하고 대책을 세워야 할 지에 대해서 기록하였다. 사건의 발단을 말하기 전에 원래 정상적으로 진행되어야 할 Git Branch 전략에 대해서, 그리고 우리 팀의 Git Branch 전략에 대해서 정리를 해보아야 할 것 같다.

https://blog.lulab.net/images/programming-tools/the-need-for-a-git-branch-strategy.png

너무나도 흔하게 보았던 Git Branch 전략이다. 일단 feature 브랜치들을 Develop 브랜치로부터 분기를 진행한다. 그 다음에는 feature 브랜치에 작업을 다 마쳤을 경우 해당 feature 브랜치를 develop 브랜치에 머지한다. feature 브랜치를 머지한 이후 새로운 기능을 만들고 싶은 경우에는 다시 develop 브랜치로부터 새로운 브랜치를 딴다.

develop 브랜치에서 배포 준비 작업에 들어가기 위해 release 브랜치를 만든다. 이 브랜치에서는 온갖 종류의 스트레스 테스트와 배포를 해도 잘 굴러갈 지에 대해 이리저리 망가뜨려보는 작업을 진행해본다. 만약에 테스트가 전부 통과된다면 이 브랜치를 develop 브랜치에 머지하고, 이를 master 브랜치에 merge한다.

만약에 배포 중에 에러가 났을 경우에는 빠르게 대처하여 해결해야 하는 문제이기에, hotfix 브랜치를 바로 master에 만들고, 에러가 해결되면 바로 master 브랜치에 넣는다. 이러한 과정을 거치는 것이 사실은 정상적이었을 텐데..

지금 생각해보면 너무 무모했다..

스크린샷 2022-11-22 오후 9.44.38.png

다음 이미지를 보면 ‘feature/connect-mongodb’ 라는 이름을 feature/map이라는 브랜치에 병합한 모습을 볼 수 있다. 도대체 왜 이런 짓을 했는 지 이해가 안 갈 수도 있을 것이다. 그런데, 저 ‘feature/map’ 브랜치는 (네이밍과 관련된 부분은 둘째 치더라도) 프론트엔드에 대한 코드를 주로 작성했고, ‘feature/connect-mongodb’는 백엔드에 대한 코드를 주로 작성했었기 때문에 코드에 대한 부분에서 충돌이 일어나지 않을 것이라는 생각이 들었었다.

그리고 네이버 지도 API의 경우에는 API를 요청하고자 할 때 서버에서 요청하는 것을 권장하고 있었고, 부스트캠프의 가이드라인에서도 API를 백엔드에서 요청해야 한다고 했었다.


# 실제 부스트캠프의 가이드라인 중 일부를 발췌
  • 웹과 앱은 반드시 각 서버에서 제공하는 공식 API를 연동해서 구현해야 합니다.
  • 외부 API를 사용할 경우 개발하는 서버를 통해서 연동해야 합니다. # -> 이 부분
  • API로 가져오지 않은 별도 리소스(이미지, 동영상 등)는 비동기로 처리해야 합니다.
  • 앱은 반드시 모든 단말 화면 크기에 대응하도록 구현해야 합니다.

그랬었기에 우리 팀은 백엔드의 코드를 받아서 진행해도 된다고 생각을 했었고, 그렇게 시작되었다.

스크린샷 2022-11-22 오후 9.51.46.png

결국 env 파일이 github의 원격 저장소에 올라가게 되었다. 브랜치가 왜 저지경이 되었는 지에 대해서는 설명을 하도록 하겠다.

어떻게 하면 env 파일을 원격 저장소에서 제거할 수 있을까?

이 때부터 팀원들끼리 수많은 고민을 하기 시작했고, 가장 먼저 시도해 본 것은 바로 git-filter-branch였다.

git-filter-branch는 간단히 말해 필터를 제공해서 필터에 적용된 파일만 가지고 히스토리를 다시 구축하는 기능을 말한다. git-filter-branch는 특정 디렉터리가 하나의 프로젝트가 되는 구조인 경우 하나만 분리하면 별도의 프로젝트를 만들 수 있을 때 사용한다.

이 때, 소스와 다른 파일들이 여러 디렉터리에 흩어져 있는 경우 원하는 파일만으로 분리를 진행하기가 쉽지 않다. 이 때 필요한 것이 바로 ‘—index-filter’였다.

# Before
$ git log --oneline | wc -l
    1547
$ git filter-branch --index-filter \\
> 'git rm --cached -qr --ignore-unmatch -- . && \\
> git reset -q $GIT_COMMIT -- \\
> locale/ \\
> README.md \\
> scripts/event-geo.js \\
> tests/build.smoketest.js' \\
> --prune-empty -- --all

Rewrite d86dcffa9c5b5ab7c89dce8620a1a4cedc05abc8 (1531/1549) (70 seconds passed, remaining 0 predicted)
Ref 'refs/heads/master' was rewritten
Ref 'refs/remotes/origin/master' was rewritten
Ref 'refs/remotes/origin/add/mailchimp-form-partial' was rewritten
WARNING: Ref 'refs/remotes/origin/master' is unchanged
Ref 'refs/remotes/origin/remove/fidelity-logo' was rewritten

# After
$ git log --oneline | wc -l
    1065
</code></pre>
<p>git filter-branch —index-filter를 이용한 뒤, 다음과 같은 명령어들을 입력한다.</p>
<pre><code class="language-bash">git rm --cached -qr --ignore-unmatch -- . &amp;&amp;
</code></pre>
<p><strong>git rm과 관련된 공식 문서:</strong> <a href="https://git-scm.com/docs/git-rm"><a href="https://git-scm.com/docs/git-rm">https://git-scm.com/docs/git-rm</a></a></p>
<ul>
<li>git rm: 현재 작업중인 트리와 인덱스에서 파일을 제거한다.
<ul>
<li>—cached: Use this option to unstage and remove paths only from the index. Working tree files, whether modified or not, will be left alone. (말이 좀 어렵지만, Git 저장소에서만 삭제하고, 로컬에서는 남기고자 하는 경우에는 —cached를 사용하면 된다.)</li>
<li>-qr
<ul>
<li>q: <code>git rm</code> normally outputs one line (in the form of an <code>rm</code>
 command) for each file removed. This option suppresses that output. (git rm은 각 파일들이 제거될 때마다 보통 한 줄만을 내보내는데, 이 옵션은 그러한 출력을 강제한다. 즉, 제거가 되는 경우에만 해당 로그를 보여준다.)</li>
<li>r: Allow recursive removal when a leading directory name is given. (재귀적으로 삭제, 즉 하위 디렉터리 및 경로까지 모두 포함하여 삭제한다.)</li>
<li>—ignore-unmatch : Exit with a zero status even if no files matched. (삭제할 파일들이 하나도 없다고 하더라도 정상적인 결과(0)을 내보내고 종료한다. 왜 zero status가 정상적인 결과인지는 <a href="https://en.wikipedia.org/wiki/Exit_status"><a href="https://en.wikipedia.org/wiki/Exit_status">https://en.wikipedia.org/wiki/Exit_status</a></a> 이 링크를 보자.</li>
<li>&amp;&amp;: bash shell에서는 연속적으로 명령어를 실행하고자 할 때 사용하는 명령어이다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>그 다음으로 진행할 부분은 git reset이다. 해당 부분에 대한 것은 <a href="https://git-scm.com/book/ko/v2/Git-%EB%8F%84%EA%B5%AC-Reset-%EB%AA%85%ED%99%95%ED%9E%88-%EC%95%8C%EA%B3%A0-%EA%B0%80%EA%B8%B0"><a href="https://git-scm.com/book/ko/v2/Git-%EB%8F%84%EA%B5%AC-Reset-%EB%AA%85%ED%99%95%ED%9E%88-%EC%95%8C%EA%B3%A0-%EA%B0%80%EA%B8%B0">https://git-scm.com/book/ko/v2/Git-도구-Reset-명확히-알고-가기</a></a> 이 공식문서에서 자세하게 살펴보자.</p>
<h2>세 개의 트리 / Reset</h2>
<p>Git을 서로 다른 세 트리를 관리하는 컨텐츠 관리자로 생각하면 <code>reset</code> 과 <code>checkout</code> 을 좀 더 쉽게 이해할 수 있다. 여기서 “트리” 란 실제로는 “파일의 묶음” 이다. 자료구조의 트리가 아니다 세 트리 중 Index는 트리도 아니지만, 이해를 쉽게 하려고 일단 트리라고 한다.</p>
<p>Git은 일반적으로 세 가지 트리를 관리하는 시스템이다.</p>


트리 | 역할
-- | --
HEAD | 마지막 커밋 스냅샷, 다음 커밋의 부모 커밋
Index | 다음에 커밋할 스냅샷
워킹 디렉토리 | 샌드박스


<p>샌드박스: <strong>외부로부터 받은 파일을 바로 실행하지 않고 보호된 영역에서 실행시켜 봄으로써 외부로부터 들어오는 파일과 프로그램이 내부 시스템에 악영향을 주는 것을 방지하는 기술</strong></p>
<p>워킹 디렉토리: 보통 현재 위치의 디렉토리, 현재 작업 중인 디렉토리</p>
<p>git reset에 대해 자세하게 말한 내용: <a href="https://www.lainyzine.com/ko/article/git-reset-and-git-revert-and-git-commit-amend/"><a href="https://www.lainyzine.com/ko/article/git-reset-and-git-revert-and-git-commit-amend/">https://www.lainyzine.com/ko/article/git-reset-and-git-revert-and-git-commit-amend/</a></a></p>
<p><code>reset</code> 명령은 정해진 순서대로 세 개의 트리를 덮어써 나가다가 옵션에 따라 지정한 곳에서 멈춘다.</p>
<ol>
<li>HEAD가 가리키는 브랜치를 옮긴다. <em><strong>(<code>--soft</code> 옵션이 붙으면 여기까지)</strong></em></li>
<li>Index를 HEAD가 가리키는 상태로 만든다. <em><strong>(<code>--hard</code> 옵션이 붙지 않았으면 여기까지)</strong></em></li>
<li>워킹 디렉토리를 Index의 상태로 만든다.</li>
</ol>
<p>사실은 이렇게 장황하게 설명을 진행했지만, 이 filter-branch를 통해서 실질적으로 얻은 이득은 없었고, 오히려 잘못된 사용으로 브랜치의 개수가 기하급수적으로 늘어나 마치 전자회로처럼 복잡해지는 문제가 발생하였다. 결국 env 내부에 들어있는 모든 데이터들을 재발급 받아 갱신했다.</p>
<p>교훈: git filter-branch는 극약처방이다. 쓸 때도 상당히 조심스럽게 사용하자.</p>
<p><strong>git filter-branch를 조심스럽게 사용해야 하는 이유:</strong> <a href="https://git-scm.com/docs/git-filter-branch"><a href="https://git-scm.com/docs/git-filter-branch">https://git-scm.com/docs/git-filter-branch</a></a></p>
<h2><strong>WARNING</strong></h2>
<p><em><strong>git filter-branch</strong></em> has a plethora of pitfalls that can produce non-obvious manglings of the intended history rewrite (and can leave you with little time to investigate such problems since it has such abysmal performance). These safety and performance issues cannot be backward compatibly fixed and as such, its use is not recommended. Please use an alternative history filtering tool such as <a href="https://github.com/newren/git-filter-repo/">git filter-repo</a>. If you still need to use <em><strong>git filter-branch</strong></em>, please carefully read <a href="https://git-scm.com/docs/git-filter-branch#SAFETY">SAFETY</a> (and <a href="https://git-scm.com/docs/git-filter-branch#PERFORMANCE">PERFORMANCE</a>) to learn about the land mines of filter-branch, and then vigilantly avoid as many of the hazards listed there as reasonably possible.</p>
<p>요약: git filter-branch는 의도한 history가 덮어씌워지면서 모호하고 뒤죽박죽이 될 수 있다. 그렇기에 리스크가 크다. 차라리 git filter-repo(<a href="https://github.com/newren/git-filter-repo/">https://github.com/newren/git-filter-repo/</a>)를 사용해보자.</p>
<h1>Rebase / Revert</h1>
<h2>Rebase</h2>
<p>그 다음으로 우리 팀에서 살펴본 것은 바로 ‘Rebase’ 명령어를 이용해서 브랜치를 되돌리고자 했었다. 일단 결론부터 말하자면 해당 명령어로 되돌리는 작업을 성공시키지는 못했다. 그러나 해당 명령어를 직접 공부해보면서 Rebase가 정확히 어떤 역할을 하는 지 이해할 수 있게 되었다.</p>
<p>Git에서 브랜치를 병합하기 위한 두 가지 방법이 있는데, 하나는 Merge이고, 나머지 하나는 바로 Rebase이다. 일단 rebase를 통해 feature에서 main으로 병합하고자 하는 경우에는 쉘에서 다음과 같은 명령어를 입력하면 된다.</p>
<pre><code class="language-bash">git checkout feature
git rebase main
</code></pre>
<p><img src="https://wac-cdn.atlassian.com/dam/jcr:3bafddf5-fd55-4320-9310-3d28f4fca3af/03%20Rebasing%20the%20feature%20branch%20into%20main.svg?cdnVersion=638" alt="https://wac-cdn.atlassian.com/dam/jcr:3bafddf5-fd55-4320-9310-3d28f4fca3af/03 Rebasing the feature branch into main.svg?cdnVersion=638"></p>
<p>그리고 rebase를 진행하게 되면 다음과 같은 결과를 볼 수 있다. rebase를 진행하게 되면 feature 브랜치의 모든 커밋 기록을 main에서도 자세하게 볼 수 있다는 장점이 존재한다. 따라서 history를 명확하게 얻을 수 있다는 장점이 존재한다. 따라서 이러한 과정은 현재 문제를 해결하고자 하는 방안으로 적합하지 못하다는 결론을 얻게 되었다.</p>
<h2>Revert</h2>
<p>Revert는 Reset과 동일하게 이전 커밋으로 되돌리는 기능을 가지고 있다. 그러나 github 같은 저장소에 올라가 다른 사람간의 코드 공유를 진행할 수 있다는 점이 Git Reset과 비교했을 때 차이점이다. (git reset은 이전 커밋으로 되돌리게 되면 그 기록을 다른 사람들과 코드 공유를 하지 못한다.)</p>
<pre><code class="language-bash">git commit -m &quot;1번 커밋&quot;
git commit -m &quot;2번 커밋&quot;
git commit -m &quot;3번 커밋&quot;

git revert [1번commit hash값]

# 결과
# Revert &quot;1번 커밋&quot;
# 3번 커밋
# 2번 커밋
# 1번 커밋
</code></pre>
<p>그러나, Revert의 경우에는 결국 Revert의 기록이 남기 때문에, env 파일 또한 각각의 커밋 기록에 남게 되어 결과적으로는 무용지물이 된다. 그리고 Reset의 경우에는 env 파일을 기록한 시점부터의 모든 커밋 기록을 날려야 하는데, 그러기에는 이미 너무 멀리 왔기 때문에 쉽사리 그 모험을 감행하는 것이 어렵다는 생각이 들었다.</p>
<h1>--allow-unrelated-histories</h1>
<p>사실 이 주제는 우리 팀에서 논의되었던 env 파일을 어떻게 지울 것인 지에 대한 주제는 아니다. 그러나, 두 브랜치를 병합하고자 할 때, 이 브랜치 각각의 커밋 기록들이 관련성 없는 history인 경우도 존재한다.</p>
<p><strong>fatal: refusing to merge unrelated histories’ Git error</strong></p>
<p>특히 이 에러를 해결하고자 할 때, 유용하게 쓰이는 방식이다. 일단은, 이 에러가 왜 나타났는 지에 대해서 살펴보기로 하겠다. 이 에러는 하나의 브랜치에 두 개의 관련성 없는 프로젝트를 병합하고자 할 때 생기는 에러이다.</p>
<p>기본적으로 merge는 원격 저장소와 로컬 저장소가 공통으로 가지고 있는 commit 지점이 존재해야 한다. 그 이유는 바로 병합을 시도하고자 하는 부분이 바로 ‘그 지점부터’이기 때문이다. 그러나, 만약에 공통되는 commit 이 없는 경우에는 pull(fetch + merge) 명령어를 사용할 수 없게 된다.</p>
<p>그러나, 이를 강제적으로라도 해결해 줄 수 있는 방법이 존재한다. 그것은 바로 ‘—allow-unrelated-histories’이다.</p>
<pre><code class="language-bash">git pull origin (branchname) --allow-unrelated-histories
</code></pre>
<p>다음과 같은 명령어를 통해 서로 공통된 commit이 없는 경우에도 병합하는 것이 가능해진다.</p>
<h1>앞으로 해야 할 일</h1>
<p>생각해보면, 이 모든 일들이 사전에 예방이 가능했다. 그러나, 사전에 브랜치를 설정하고자 했을 때, branch protection에 대해서 진행하지 않았기 때문에 이런 일이 발생했다는 생각이 들었다. 그렇다면, 지금부터라도 branch protection에 대해서 공부를 해보아야 한다.</p>
<p>그러나 안타깝게도, github을 사용하고 관리자 권한이 있다면 사실상 완벽하게 막는 방법은 없다. 는 생각이 들었다. 따라서 방법은 두 가지가 존재한다. 첫 번째 방법은 바로 ‘그냥 조심하고 이대로 진행하자’ 였고, 두 번째 방법은 바로 ‘관리자의 권한을 한 명을 제외하고 박탈’하는 방식이다.</p>
<p>이렇게 11월 22일의 포스트모템을 끝내도록 하겠다. 앞으로는 더욱 더 신중하게 접근하고, 함수로 브랜치 전략을 더럽히면 안되겠다는 생각이 들었다.</p>
# 11월 22일 포스트모템 (feat. git-filter-branch, rebase, --allow-unrelated-histories)

## 포스트모템이란 무엇인가?

- 포스트모템의 원래 의미: 사람이 죽었을 때, 사인을 분석하기 위해 시체를 해부하는 과정

**실패한 근본 원인을 분석하여 문서로 남기는 것이 실수로부터 배우는 핵심이다**. 이를 구글은(다른 많은 회사에서도) 포스트모템이라고 한다.

- 포스트모템 문서에는 쓸모없는 사죄, 변명, 지적으로 채워지지 않도록 각별히 주의해야 한다.
- 제대로 된 포스트모템에는 무엇을 배웠는지와 배운 것을 토대로 앞으로 무엇을 바꿀지가 담겨야 한다.
- 실패를 제대로 기록해두면 다른 이들도 무슨 일이 있었는지 알 수 있고 (당장 혹은 미래에) 똑같은 실수를 반복하는 일을 피할 수 있다.

**TMI:** 한국에서는 게임업계에서 이 용어를 많이 사용하는 것 같은데, 잘한 점도 들어있다는 것이 차이인 것 같기도 하다. 

**출처: 구글 엔지니어는 이렇게 일한다** [https://kukim.tistory.com/140?category=898272](https://kukim.tistory.com/140?category=898272)

## 사건의 발단

git에 대한 에러를 해결하는 과정에서 어떤 부분에 문제가 있었는 지, 그리고 어떻게 해결을 했는 지, 앞으로는 어떻게 대비를 하고 대책을 세워야 할 지에 대해서 기록하였다. 사건의 발단을 말하기 전에 원래 정상적으로 진행되어야 할 Git Branch 전략에 대해서, 그리고 우리 팀의 Git Branch 전략에 대해서 정리를 해보아야 할 것 같다. 

![https://blog.lulab.net/images/programming-tools/the-need-for-a-git-branch-strategy.png](https://blog.lulab.net/images/programming-tools/the-need-for-a-git-branch-strategy.png)

너무나도 흔하게 보았던 Git Branch 전략이다. 일단 feature 브랜치들을 Develop 브랜치로부터 분기를 진행한다. 그 다음에는 feature 브랜치에 작업을 다 마쳤을 경우 해당 feature 브랜치를 develop 브랜치에 머지한다. feature 브랜치를 머지한 이후 새로운 기능을 만들고 싶은 경우에는 다시 develop 브랜치로부터 새로운 브랜치를 딴다. 

develop 브랜치에서 배포 준비 작업에 들어가기 위해 release 브랜치를 만든다. 이 브랜치에서는 온갖 종류의 스트레스 테스트와 배포를 해도 잘 굴러갈 지에 대해 이리저리 망가뜨려보는 작업을 진행해본다. 만약에 테스트가 전부 통과된다면 이 브랜치를 develop 브랜치에 머지하고, 이를 master 브랜치에 merge한다.

만약에 배포 중에 에러가 났을 경우에는 빠르게 대처하여 해결해야 하는 문제이기에, hotfix 브랜치를 바로 master에 만들고, 에러가 해결되면 바로 master 브랜치에 넣는다. 이러한 과정을 거치는 것이 사실은 정상적이었을 텐데..

## 지금 생각해보면 너무 무모했다..

![스크린샷 2022-11-22 오후 9.44.38.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bb998774-873b-47bd-b562-2ed6ae01ee8c/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2022-11-22_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_9.44.38.png)

다음 이미지를 보면 ‘feature/connect-mongodb’ 라는 이름을 feature/map이라는 브랜치에 병합한 모습을 볼 수 있다. 도대체 왜 이런 짓을 했는 지 이해가 안 갈 수도 있을 것이다. 그런데, 저 ‘feature/map’ 브랜치는 (네이밍과 관련된 부분은 둘째 치더라도) 프론트엔드에 대한 코드를 주로 작성했고, ‘feature/connect-mongodb’는 백엔드에 대한 코드를 주로 작성했었기 때문에 코드에 대한 부분에서 충돌이 일어나지 않을 것이라는 생각이 들었었다. 

그리고 네이버 지도 API의 경우에는 API를 요청하고자 할 때 서버에서 요청하는 것을 권장하고 있었고, 부스트캠프의 가이드라인에서도 API를 백엔드에서 요청해야 한다고 했었다. 

```ruby

# 실제 부스트캠프의 가이드라인 중 일부를 발췌

- 웹과 앱은 반드시 각 서버에서 제공하는 공식 API를 연동해서 구현해야 합니다.                                             
- 외부 API를 사용할 경우 개발하는 서버를 통해서 연동해야 합니다. # -> 이 부분                                             
- API로 가져오지 않은 별도 리소스(이미지, 동영상 등)는 비동기로 처리해야 합니다.                          
- 앱은 반드시 모든 단말 화면 크기에 대응하도록 구현해야 합니다.

그랬었기에 우리 팀은 백엔드의 코드를 받아서 진행해도 된다고 생각을 했었고, 그렇게 시작되었다.

스크린샷 2022-11-22 오후 9.51.46.png

결국 env 파일이 github의 원격 저장소에 올라가게 되었다. 브랜치가 왜 저지경이 되었는 지에 대해서는 설명을 하도록 하겠다.

어떻게 하면 env 파일을 원격 저장소에서 제거할 수 있을까?

이 때부터 팀원들끼리 수많은 고민을 하기 시작했고, 가장 먼저 시도해 본 것은 바로 git-filter-branch였다.

git-filter-branch는 간단히 말해 필터를 제공해서 필터에 적용된 파일만 가지고 히스토리를 다시 구축하는 기능을 말한다. git-filter-branch는 특정 디렉터리가 하나의 프로젝트가 되는 구조인 경우 하나만 분리하면 별도의 프로젝트를 만들 수 있을 때 사용한다.

이 때, 소스와 다른 파일들이 여러 디렉터리에 흩어져 있는 경우 원하는 파일만으로 분리를 진행하기가 쉽지 않다. 이 때 필요한 것이 바로 ‘—index-filter’였다.

# Before
$ git log --oneline | wc -l
    1547

```bash
$ git filter-branch --index-filter \
> 'git rm --cached -qr --ignore-unmatch -- . && \
> git reset -q $GIT_COMMIT -- \
> locale/ \
> README.md \
> scripts/event-geo.js \
> tests/build.smoketest.js' \
> --prune-empty -- --all

Rewrite d86dcffa9c5b5ab7c89dce8620a1a4cedc05abc8 (1531/1549) (70 seconds passed, remaining 0 predicted)
Ref 'refs/heads/master' was rewritten
Ref 'refs/remotes/origin/master' was rewritten
Ref 'refs/remotes/origin/add/mailchimp-form-partial' was rewritten
WARNING: Ref 'refs/remotes/origin/master' is unchanged
Ref 'refs/remotes/origin/remove/fidelity-logo' was rewritten

# After
$ git log --oneline | wc -l
    1065

git filter-branch —index-filter를 이용한 뒤, 다음과 같은 명령어들을 입력한다.

git rm --cached -qr --ignore-unmatch -- . &&

git rm과 관련된 공식 문서: https://git-scm.com/docs/git-rm

  • git rm: 현재 작업중인 트리와 인덱스에서 파일을 제거한다.
    • —cached: Use this option to unstage and remove paths only from the index. Working tree files, whether modified or not, will be left alone. (말이 좀 어렵지만, Git 저장소에서만 삭제하고, 로컬에서는 남기고자 하는 경우에는 —cached를 사용하면 된다.)
    • -qr
      • q: git rm normally outputs one line (in the form of an rm  command) for each file removed. This option suppresses that output. (git rm은 각 파일들이 제거될 때마다 보통 한 줄만을 내보내는데, 이 옵션은 그러한 출력을 강제한다. 즉, 제거가 되는 경우에만 해당 로그를 보여준다.)
      • r: Allow recursive removal when a leading directory name is given. (재귀적으로 삭제, 즉 하위 디렉터리 및 경로까지 모두 포함하여 삭제한다.)
      • —ignore-unmatch : Exit with a zero status even if no files matched. (삭제할 파일들이 하나도 없다고 하더라도 정상적인 결과(0)을 내보내고 종료한다. 왜 zero status가 정상적인 결과인지는 https://en.wikipedia.org/wiki/Exit_status 이 링크를 보자.
      • &&: bash shell에서는 연속적으로 명령어를 실행하고자 할 때 사용하는 명령어이다.

그 다음으로 진행할 부분은 git reset이다. 해당 부분에 대한 것은 [https://git-scm.com/book/ko/v2/Git-도구-Reset-명확히-알고-가기](https://git-scm.com/book/ko/v2/Git-%EB%8F%84%EA%B5%AC-Reset-%EB%AA%85%ED%99%95%ED%9E%88-%EC%95%8C%EA%B3%A0-%EA%B0%80%EA%B8%B0) 이 공식문서에서 자세하게 살펴보자.

세 개의 트리 / Reset

Git을 서로 다른 세 트리를 관리하는 컨텐츠 관리자로 생각하면 reset 과 checkout 을 좀 더 쉽게 이해할 수 있다. 여기서 “트리” 란 실제로는 “파일의 묶음” 이다. 자료구조의 트리가 아니다 세 트리 중 Index는 트리도 아니지만, 이해를 쉽게 하려고 일단 트리라고 한다.

Git은 일반적으로 세 가지 트리를 관리하는 시스템이다.

트리 역할
HEAD 마지막 커밋 스냅샷, 다음 커밋의 부모 커밋
Index 다음에 커밋할 스냅샷
워킹 디렉토리 샌드박스

샌드박스: 외부로부터 받은 파일을 바로 실행하지 않고 보호된 영역에서 실행시켜 봄으로써 외부로부터 들어오는 파일과 프로그램이 내부 시스템에 악영향을 주는 것을 방지하는 기술

워킹 디렉토리: 보통 현재 위치의 디렉토리, 현재 작업 중인 디렉토리

git reset에 대해 자세하게 말한 내용: https://www.lainyzine.com/ko/article/git-reset-and-git-revert-and-git-commit-amend/

reset 명령은 정해진 순서대로 세 개의 트리를 덮어써 나가다가 옵션에 따라 지정한 곳에서 멈춘다.

  1. HEAD가 가리키는 브랜치를 옮긴다. (--soft 옵션이 붙으면 여기까지)
  2. Index를 HEAD가 가리키는 상태로 만든다. (--hard 옵션이 붙지 않았으면 여기까지)
  3. 워킹 디렉토리를 Index의 상태로 만든다.

사실은 이렇게 장황하게 설명을 진행했지만, 이 filter-branch를 통해서 실질적으로 얻은 이득은 없었고, 오히려 잘못된 사용으로 브랜치의 개수가 기하급수적으로 늘어나 마치 전자회로처럼 복잡해지는 문제가 발생하였다. 결국 env 내부에 들어있는 모든 데이터들을 재발급 받아 갱신했다.

교훈: git filter-branch는 극약처방이다. 쓸 때도 상당히 조심스럽게 사용하자.

git filter-branch를 조심스럽게 사용해야 하는 이유: https://git-scm.com/docs/git-filter-branch

WARNING

git filter-branch has a plethora of pitfalls that can produce non-obvious manglings of the intended history rewrite (and can leave you with little time to investigate such problems since it has such abysmal performance). These safety and performance issues cannot be backward compatibly fixed and as such, its use is not recommended. Please use an alternative history filtering tool such as [git filter-repo](https://github.com/newren/git-filter-repo/). If you still need to use git filter-branch, please carefully read [SAFETY](https://git-scm.com/docs/git-filter-branch#SAFETY) (and [[PERFORMANCE](https://git-scm.com/docs/git-filter-branch#PERFORMANCE)](https://git-scm.com/docs/git-filter-branch#PERFORMANCE)) to learn about the land mines of filter-branch, and then vigilantly avoid as many of the hazards listed there as reasonably possible.

요약: git filter-branch는 의도한 history가 덮어씌워지면서 모호하고 뒤죽박죽이 될 수 있다. 그렇기에 리스크가 크다. 차라리 git filter-repo(https://github.com/newren/git-filter-repo/)를 사용해보자.

Rebase / Revert

Rebase

그 다음으로 우리 팀에서 살펴본 것은 바로 ‘Rebase’ 명령어를 이용해서 브랜치를 되돌리고자 했었다. 일단 결론부터 말하자면 해당 명령어로 되돌리는 작업을 성공시키지는 못했다. 그러나 해당 명령어를 직접 공부해보면서 Rebase가 정확히 어떤 역할을 하는 지 이해할 수 있게 되었다.

Git에서 브랜치를 병합하기 위한 두 가지 방법이 있는데, 하나는 Merge이고, 나머지 하나는 바로 Rebase이다. 일단 rebase를 통해 feature에서 main으로 병합하고자 하는 경우에는 쉘에서 다음과 같은 명령어를 입력하면 된다.

git checkout feature
git rebase main

https://wac-cdn.atlassian.com/dam/jcr:3bafddf5-fd55-4320-9310-3d28f4fca3af/03%20Rebasing%20the%20feature%20branch%20into%20main.svg?cdnVersion=638

그리고 rebase를 진행하게 되면 다음과 같은 결과를 볼 수 있다. rebase를 진행하게 되면 feature 브랜치의 모든 커밋 기록을 main에서도 자세하게 볼 수 있다는 장점이 존재한다. 따라서 history를 명확하게 얻을 수 있다는 장점이 존재한다. 따라서 이러한 과정은 현재 문제를 해결하고자 하는 방안으로 적합하지 못하다는 결론을 얻게 되었다.

Revert

Revert는 Reset과 동일하게 이전 커밋으로 되돌리는 기능을 가지고 있다. 그러나 github 같은 저장소에 올라가 다른 사람간의 코드 공유를 진행할 수 있다는 점이 Git Reset과 비교했을 때 차이점이다. (git reset은 이전 커밋으로 되돌리게 되면 그 기록을 다른 사람들과 코드 공유를 하지 못한다.)

git commit -m "1번 커밋"
git commit -m "2번 커밋"
git commit -m "3번 커밋"

git revert [1번commit hash값]

# 결과
# Revert "1번 커밋"
# 3번 커밋
# 2번 커밋
# 1번 커밋

그러나, Revert의 경우에는 결국 Revert의 기록이 남기 때문에, env 파일 또한 각각의 커밋 기록에 남게 되어 결과적으로는 무용지물이 된다. 그리고 Reset의 경우에는 env 파일을 기록한 시점부터의 모든 커밋 기록을 날려야 하는데, 그러기에는 이미 너무 멀리 왔기 때문에 쉽사리 그 모험을 감행하는 것이 어렵다는 생각이 들었다.

--allow-unrelated-histories

사실 이 주제는 우리 팀에서 논의되었던 env 파일을 어떻게 지울 것인 지에 대한 주제는 아니다. 그러나, 두 브랜치를 병합하고자 할 때, 이 브랜치 각각의 커밋 기록들이 관련성 없는 history인 경우도 존재한다.

fatal: refusing to merge unrelated histories’ Git error

특히 이 에러를 해결하고자 할 때, 유용하게 쓰이는 방식이다. 일단은, 이 에러가 왜 나타났는 지에 대해서 살펴보기로 하겠다. 이 에러는 하나의 브랜치에 두 개의 관련성 없는 프로젝트를 병합하고자 할 때 생기는 에러이다.

기본적으로 merge는 원격 저장소와 로컬 저장소가 공통으로 가지고 있는 commit 지점이 존재해야 한다. 그 이유는 바로 병합을 시도하고자 하는 부분이 바로 ‘그 지점부터’이기 때문이다. 그러나, 만약에 공통되는 commit 이 없는 경우에는 pull(fetch + merge) 명령어를 사용할 수 없게 된다.

그러나, 이를 강제적으로라도 해결해 줄 수 있는 방법이 존재한다. 그것은 바로 ‘—allow-unrelated-histories’이다.

git pull origin (branchname) --allow-unrelated-histories

다음과 같은 명령어를 통해 서로 공통된 commit이 없는 경우에도 병합하는 것이 가능해진다.

앞으로 해야 할 일

생각해보면, 이 모든 일들이 사전에 예방이 가능했다. 그러나, 사전에 브랜치를 설정하고자 했을 때, branch protection에 대해서 진행하지 않았기 때문에 이런 일이 발생했다는 생각이 들었다. 그렇다면, 지금부터라도 branch protection에 대해서 공부를 해보아야 한다.

그러나 안타깝게도, github을 사용하고 관리자 권한이 있다면 사실상 완벽하게 막는 방법은 없다. 는 생각이 들었다. 따라서 방법은 두 가지가 존재한다. 첫 번째 방법은 바로 ‘그냥 조심하고 이대로 진행하자’ 였고, 두 번째 방법은 바로 ‘관리자의 권한을 한 명을 제외하고 박탈’하는 방식이다.

이렇게 11월 22일의 포스트모템을 끝내도록 하겠다. 앞으로는 더욱 더 신중하게 접근하고, 함수로 브랜치 전략을 더럽히면 안되겠다는 생각이 들었다.

⚠️ **GitHub.com Fallback** ⚠️