들어가기에 앞서
Derrick Stolee가 작성한 Commits are snapshots, not diffs를 번역한 포스팅임을 밝힙니다. 번역에 오류가 있는 경우 댓글로 알려주시면 감사하겠습니다.
Git은 뭐가 뭔지 헷갈리기로 악명이 높다. 사용자들은 기대와 어긋나는 용어와 문구를 접하며 곤혹스러워 한다. 이러한 현상은 git cherry-pick이나 git rebase와 같이 '히스토리를 다시 작성'하는 명령에서 가장 두드러진다. 내 경험상 이러한 혼란의 근본 원인은 커밋을 여기저기에서 무엇이 어떻게 바뀌었는지 보여주는 diff로 해석하기 때문이다. 하지만 커밋은 diff가 아니라 스냅샷이다!
Git을 감싸고 있는 베일을 걷어내고 레포지토리에 데이터를 저장하는 방식을 살펴보면 Git을 이해할 수 있다고 생각한다. 이러한 관점에서 Git을 먼저 살펴본 후, 이 새로운 관점이 어떻게 git cherry-pick 및 git rebase와 같은 명령을 이해하는 데 도움을 주는지 알아보려고 한다.
조금 더 자세히 알아보고 싶다면 Git Pro book의 Git Internals 챕터를 읽어보기를 권한다.
여기에서는 v2.29.2에서 체크아웃한 git/git 레포지토리를 예제로 사용하도록 하겠다. 추가 연습을 위해 command-line 예제를 따라해 보기를 바란다.
Object ID는 해쉬
Git 객체에 대해 알아야 할 가장 중요한 부분은 Git이 object ID(줄여서 OID)로 각각을 참조하여 객체에 고유한 이름을 제공한다는 점이다. 이러한 OID를 찾기 위해 git rev-parse <ref> 명령을 사용하겠다. 각 객체는 기본적으로 일반 텍스트 파일이며, git cat-file -p <oid> 명령을 사용하여 해당 객체의 내용을 살펴볼 수 있다.
여러분은 아마도 짧은 16진수 문자열로 주어진 OID를 보는 데 익숙할 것이다. 이 문자열은 레포지토리에 있는 하나의 객체만 해당 약어와 일치하는 OID를 가질 수 있을 만큼 충분히 긴 문자열로 지정된다. 너무 짧은 길이의 축약된 OID를 사용하여 객체의 유형(type)을 요청하면 다음과 같이 일치하는 OID 목록이 표시된다.
$ git cat-file -t e0c03
error: short SHA1 e0c03 is ambiguous
hint: The candidates are:
hint: e0c03f27484 commit 2016-10-26 - contrib/buildsystems: ignore irrelevant files in Generators/
hint: e0c03653e72 tree
hint: e0c03c3eecc blob
fatal: Not a valid object name e0c03
여기에서 blob, tree, commit은 어떤 유형을 말하는 걸까? bottom-up 방식으로 아래에서부터 하나씩 살펴보도록 하자.
Blob은 파일의 내용
객체 모델 하단에 위치한 블롭(blob)에는 파일의 내용(content)이 포함되어 있다. 현재 수정 버전의 파일에 대한 OID를 찾으려면 git rev-parse HEAD:<path>를 실행한다. 그런 다음 git cat-file -p <oid>를 사용해 해당 내용을 찾는다.
$ git rev-parse HEAD:README.md
eb8115e6b04814f0c37146bbe3dbc35f3e8992e0
$ git cat-file -p eb8115e6b04814f0c37146bbe3dbc35f3e8992e0 | head -n 8
[![Build status](https://github.com/git/git/workflows/CI/PR/badge.png)](https://github.com/git/git/actions?query=branch%3Amaster+event%3Apush)
Git - fast, scalable, distributed revision control system
=========================================================
Git is a fast, scalable, distributed revision control system with an
unusually rich command set that provides both high-level operations
and full access to internals.
내 디스크에 있는 README.md 파일을 수정하면 git status는 파일에 최근 수정된 시간이 있음을 알리고 내용을 해시한다. 파일의 내용이 HEAD:README.md에 있는 현재 OID와 일치하지 않으면 git status는 해당 파일을 '디스크에서 수정됨'으로 보고한다. 이렇게 하면 현재 작업 디렉토리의 파일 내용이 HEAD에서 예상하는 내용과 일치하는지 확인할 수 있다.
Tree는 디렉토리 목록
블롭이 파일의 내용은 포함하지만 파일 이름은 포함하지 않는다는 사실에 주목하길 바란다! 파일명은 Git이 디렉토리를 표시하는 방식인 트리(tree)에서 찾아볼 수 있다. 트리는 경로 항목이 정렬된 목록으로, 객체 유형, 파일 모드 및 해당 경로에 있는 객체의 OID와 짝을 이룬다. 하위 디렉토리도 트리로 표시되므로 트리가 다른 트리를 가리킬 수도 있다!
이러한 객체들이 어떻게 연관되어 있는지 시각화해서 살펴보기 위해 다이어그램을 사용할 것이다. 빨간색 사각형은 블롭을, 파란색 삼각형은 트리를 의미한다.
$ git rev-parse HEAD^{tree}
75130889f941eceb57c6ceb95c6f28dfc83b609c
$ git cat-file -p 75130889f941eceb57c6ceb95c6f28dfc83b609c | head -n 15
100644 blob c2f5fe385af1bbc161f6c010bdcf0048ab6671ed .cirrus.yml
100644 blob c592dda681fecfaa6bf64fb3f539eafaf4123ed8 .clang-format
100644 blob f9d819623d832113014dd5d5366e8ee44ac9666a .editorconfig
100644 blob b08a1416d86012134f823fe51443f498f4911909 .gitattributes
040000 tree fbe854556a4ae3d5897e7b92a3eb8636bb08f031 .github
100644 blob 6232d339247fae5fdaeffed77ae0bbe4176ab2de .gitignore
100644 blob cbeebdab7a5e2c6afec338c3534930f569c90f63 .gitmodules
100644 blob bde7aba756ea74c3af562874ab5c81a829e43c83 .mailmap
100644 blob 05f3e3f8d79117c1d32bf5e433d0fd49de93125c .travis.yml
100644 blob 5ba86d68459e61f87dae1332c7f2402860b4280c .tsan-suppressions
100644 blob fc4645d5c08bd005238fc72cfa709495d8722e6a CODE_OF_CONDUCT.md
100644 blob 536e55524db72bd2acf175208aef4f3dfc148d42 COPYING
040000 tree a58410edddbdd133cca6b3322bebe4fb37be93fa Documentation
100755 blob ca6ccb49866c595c80718d167e40cfad1ee7f376 GIT-VERSION-GEN
100644 blob 9ba33e6a141a3906eb707dd11d1af4b0f8191a55 INSTALL
트리는 각 하위 항목의 이름을 제공한다. 또한 트리에는 각 항목에 대한 Unix 파일 권한, 객체의 유형(blob 또는 tree) 및 각 항목에 대한 OID와 같은 정보도 포함된다. 상위 15개 항목만 출력했지만, grep(입력으로 전달된 파일의 내용에서 특정 문자열을 찾고자할 때 사용하는 명령어 - 옮긴이 주)을 사용하면 이 트리에 앞에서 살펴본 blob OID를 가리키는 README.md 항목이 있다는 것을 알 수 있다.
$ git cat-file -p 75130889f941eceb57c6ceb95c6f28dfc83b609c | grep README.md
100644 blob eb8115e6b04814f0c37146bbe3dbc35f3e8992e0 README.md
트리는 이러한 경로 항목을 사용해 블롭과 다른 트리를 가리킬 수 있다. 이러한 관계는 경로의 이름과 쌍을 이룬다는 사실을 기억해 두기를 바란다. 하지만 우리가 사용할 다이어그램에 항상 이러한 이름들을 표시하지는 않을 것이다.
트리 자체는 레포지토리 내에서 어디에 존재하는지 알지 못하며, 이 사실을 알고 있는 것은 해당 트리를 가리키는 객체다. <ref>^{tree}가 참조하는 트리는 루트 트리(root tree)라는 특수한 트리이다. 이 루트 트리는 커밋과 특별한 연결고리를 가진다.
Commit은 스냅샷
커밋은 시간적으로 떨어져 있는 스냅샷이다. 각 커밋에는 해당 시점의 작업 디렉토리 상태를 나타내는 루트 트리를 가리키는 포인터가 포함되어 있다. 커밋에는 이전 스냅샷에 해당하는 부모 커밋 목록이 있다. 부모가 없는 커밋은 루트 커밋이고 부모가 여러 개 있는 커밋은 merge 커밋이다. 커밋에는 작성자(Author)와 커미터(Committer)의 정보(이름, 이메일 주소, 날짜 등), 커밋 메시지 등 스냅샷을 설명하는 메타데이터도 포함된다. 커밋 작성자는 커밋 메시지를 통해 부모 커밋과 관련하여 해당 커밋의 목적을 설명할 수 있다.
예를 들어 Git 레포지토리의 v2.29.2에 있는 커밋은 해당 release에 대해 설명하며 Git 메인테이너가 작성하고 커밋하고 있다.
$ git rev-parse HEAD
898f80736c75878acc02dc55672317fcc0e0a5a6
/c/_git/git ((v2.29.2))
$ git cat-file -p 898f80736c75878acc02dc55672317fcc0e0a5a6
tree 75130889f941eceb57c6ceb95c6f28dfc83b609c
parent a94bce62b99be35f2ee2b4c98f97c222e7dd9d82
author Junio C Hamano <gitster@pobox.com> 1604006649 -0700
committer Junio C Hamano <gitster@pobox.com> 1604006649 -0700
Git 2.29.2
Signed-off-by: Junio C Hamano <gitster@pobox.com>
git log로 히스토리를 조금 더 자세히 살펴보면 해당 커밋과 부모 커밋 사이의 변경 사항에 대해 설명하는 커밋 메시지를 볼 수 있다.
$ git cat-file -p 16b0bb99eac5ebd02a5dcabdff2cfc390e9d92ef
tree d0e42501b1cf65395e91e22e74f75fc5caa0286e
parent 56706dba33f5d4457395c651cf1cd033c6c03c7a
author Jeff King <peff@peff.net> 1603436979 -0400
committer Junio C Hamano <gitster@pobox.com> 1603466719 -0700
am: fix broken email with --committer-date-is-author-date
Commit e8cbe2118a (am: stop exporting GIT_COMMITTER_DATE, 2020-08-17)
rewrote the code for setting the committer date to use fmt_ident(),
rather than setting an environment variable and letting commit_tree()
handle it. But it introduced two bugs:
- we use the author email string instead of the committer email
- when parsing the committer ident, we used the wrong variable to
compute the length of the email, resulting in it always being a
zero-length string
This commit fixes both, which causes our test of this option via the
rebase "apply" backend to now succeed.
Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
앞으로 다이어그램에서는 커밋을 표시하기 위해 Circle을 사용하겠다. 각 다이어그램의 도식에 두운법(동일한 자음 소리를 가진 단어가 적당한 간격으로 어두에 반복되는 것 - 옮긴이 주)을 사용한다는 점을 알아챘는지 모르겠다. 지금까지 살펴본 내용을 정리하면 다음과 같다.
- Box는 blob이다. 이는 파일 내용을 나타낸다.
- Triangle은 tree다. 디렉토리를 의미한다.
- Circle은 commit이다. 시간적으로 떨어져 있는 스냅샷에 해당한다.
Branch는 포인터
Git에서는 대부분의 경우 OID를 참조하지 않고 히스토리를 이동하고 변경을 수행한다. 이는 우리의 관심의 대상인 커밋에 대한 포인터를 브랜치가 제공하기 때문이다. 브랜치 이름이 main인 브랜치는 실제로 Git에서 refs/heads/main이라는 레퍼런스다. 이 파일에는 문자 그대로 커밋의 OID를 참조하는 16진수 문자열이 포함되어 있다. 작업을 해나가면서 이러한 레퍼런스가 다른 커밋을 가리키도록 파일의 내용을 변경한다.
즉, 브랜치는 앞에서 살펴본 Git 객체와 큰 차이가 있다. 커밋, 트리, 블롭은 불변하기 때문에 그 내용을 변경할 수 없다. 내용을 변경하면 다른 해시와 새로운 객체를 참조하는 새로운 OID를 얻게 된다! 사용자는 trunk 또는 my-special-project와 같이 의미를 부여하기 위해 브랜치의 이름을 지정한다. 이로써 브랜치를 사용해 작업을 추적하고 공유한다.
특별한 레퍼런스인 HEAD는 현재 작업 중인 브랜치를 가리킨다. HEAD에 커밋을 추가하면 해당 브랜치가 자동으로 새 커밋으로 업데이트된다.
git switch -c 명령어를 사용하여 새 브랜치를 만들고 HEAD를 업데이트할 수 있다.
$ git switch -c my-branch
Switched to a new branch 'my-branch'
$ cat .git/refs/heads/my-branch
1ec19b7757a1acb11332f06e8e812b505490afc6
$ cat .git/HEAD
ref: refs/heads/my-branch
my-branch를 생성하면 현재 커밋 OID가 포함된 파일(.git/refs/heads/my-branch)이 생성되고, 이 브랜치를 가리키도록 .git/HEAD 파일이 업데이트되는 것을 볼 수 있다. 이제 새 커밋을 생성하여 HEAD를 업데이트하면 my-branch 브랜치가 새 커밋을 가리키도록 업데이트된다!
거시적인 그림
앞서 살펴본 새로운 용어들을 모두 하나의 큰 그림에 넣어 보자. 브랜치는 커밋을 가리키고, 커밋은 다른 커밋과 해당 커밋의 루트 트리를 가리키며, 트리는 블롭과 다른 트리를 가리키고, 블롭은 아무 것도 가리키지 않는다. 다음은 모든 객체가 전부 포함된 다이어그램이다.
위의 다이어그램에서 시간은 왼쪽에서 오른쪽으로 흐른다. 커밋과 그 부모 커밋 사이의 화살표는 오른쪽에서 왼쪽을 가리킨다. 각 커밋에는 루트 트리가 하나 있다. 위의 다이어그램에서 HEAD는 main 브랜치를 가리키고 있고, main 브랜치는 가장 최근 커밋을 가리킨다. 이 커밋의 루트 트리는 아래 쪽으로 전부 다 펼쳐져 있고, 나머지 트리에는 이 객체들을 가리키는 화살표가 있다. 그 이유는 여러 루트 트리에서 동일한 객체에 도달할 수 있기 때문이다! 이러한 트리는 OID(객체의 내용)로 해당 객체를 참조하기 때문에 이 스냅샷에는 동일한 데이터의 복사본이 여러 개 필요하지 않다. 이런 식으로 Git의 객체 모델은 Merkle tree를 형성한다.
이러한 방식으로 Git의 객체 모델을 보면 커밋이 스냅샷인 이유를 알 수 있다. 특정 커밋은 해당 커밋에 대해 예상되는 작업 디렉토리를 전부 볼 수 있게끔 직접 연결되어 있기 때문이다!
diff 계산
커밋은 스냅샷이지만, 히스토리 보기로 특정 커밋을 볼 때나 GitHub에서 커밋을 볼 때 diff로 보이는 경우가 많다. 실제로 커밋 메시지는 이 diff를 가리키는 경우가 많다. Diff는 커밋과 부모 커밋의 루트 트리를 비교하여 스냅샷 데이터에서 동적으로 생성된다. Git은 인접한 커밋뿐만 아니라 시간적으로 떨어져 있는 두 개의 스냅샷을 비교할 수 있다.
두 커밋을 비교하는 작업은 두 커밋의 루트 트리를 살펴보는 것에서부터 시작한다. 거의 대부분의 경우 두 루트 트리가 다를 것이다. 그런 다음 현재 트리의 경로에서 OID가 다른 경우, 두 커밋의 쌍을 따라가며 하위 트리에서 깊이 우선 탐색을 수행한다. 아래 예제에서는 docs로 인해 루트 트리의 값이 서로 다르므로 이 두 트리에서 재귀를 수행한다. 두 개의 트리는 M.md 값이 다르므로 두 블롭을 한 줄씩 비교하고 그 차이를 표시한다. docs 내에서 N.md는 여전히 동일하므로 해당 파일은 건너뛰고 루트 트리로 돌아간다. 그러면 루트 트리에서 things 디렉토리와 README.md 항목의 OID가 동일하다는 것을 알 수 있다.
위의 다이어그램에서 things 트리에는 한 번도 방문한 적이 없으므로 해당 디렉토리에 있는 도달 가능한 객체 역시 방문되지 않는다. 이렇게 하면 diff를 계산하는 비용은 내용이 다른 경로의 수에 비례한다.
이제 우리는 커밋은 스냅샷이며 어떤 커밋이든 두 개의 커밋 간의 차이를 동적으로 계산할 수 있다는 것을 이해했다. 그렇다면 왜 이러한 사실이 널리 알려지지 않은 걸까? Git을 처음 접하는 사람들은 왜 커밋이 diff라고 잘못 알고 있는 걸까?
내가 가장 좋아하는 비유 중 하나는 커밋을 어떤 경우에는 스냅샷처럼 취급하고 또 어떤 때에는 diff처럼 취급하는 등 커밋이 파동-입자 이중성을 가졌다고 생각하는 것이다.
커밋이 diff가 아니라면 git cherry-pick은 무슨 일을 하는 걸까?
git cherry-pick <oid> 명령은 현재 커밋을 부모로 하는 <oid>와 동일한 diff를 가진 새 커밋을 만든다. Git은 기본적으로 다음 단계를 따른다.
1. 커밋 <oid>와 그 부모 커밋 간의 diff를 계산한다.
2. 해당 diff를 현재 HEAD에 적용한다.
3. 루트 트리가 새 작업 디렉터리와 일치하고 HEAD에 있는 커밋을 부모로 가진 새 커밋을 만든다.
4. HEAD에 있는 커밋을 새 커밋으로 이동한다.
Git이 새 커밋을 생성한 다음, git log -1 -p HEAD의 출력은 git log -1 -p <oid>의 출력과 일치해야 한다.
커밋을 현재 HEAD의 위로 '이동'한 것이 아니라 이전 커밋과 diff가 일치하는 새 커밋을 만들었다는 점을 아는 것이 중요하다.
커밋이 diff가 아니라면 git rebase는 무슨 일을 하는 걸까?
git rebase 명령은 커밋을 옮겨서 새로운 히스토리를 만드는 방법이다. 가장 기본적인 형태를 보자면 다른 커밋 위에 diff를 재생하는 git cherry-pick 명령이 계속해서 이어진 것에 불과하다.
가장 중요한 것은 git rebase <target>이 HEAD에서는 도달할 수 있지만 <target>에서는 도달할 수 없는 커밋의 목록을 알아낸다는 점이다. git log --oneline <target>..HEAD를 사용하면 이러한 사실을 직접 살펴볼 수 있다.
다시 말해서 rebase 명령은 <target> 위치로 이동하여 해당 커밋 범위에 대해 가장 오래된 커밋부터 시작해 git chery-pick 명령을 수행하는 작업이다. 마지막에는 OID는 다르지만 원래 커밋 범위와 diff가 유사한 새로운 커밋들을 갖게 된다.
예를 들어, target 브랜치에서 갈라져서 나와서 현재 HEAD에 있는 세 개의 연속된 커밋이 있다고 생각해 보자. git rebase target을 실행하면 공통 부모인 P를 찾아서 커밋 목록 A, B, C를 알아낸다. 그 다음 target 위에서 cherry-pick을 해 새로운 커밋 A', B', C'를 구성한다.
커밋 A', B', C'는 완전히 새로운 커밋으로 A, B, C와 많은 정보를 공유하지만 별개의 새로운 객체이다. 실제로 이전 커밋들은 가비지 컬렉션이 실행될 때까지 레포지토리에 계속 존재한다.
git range-diff 명령을 사용해서 두 커밋 범위가 어떻게 다른지 확인해 볼 수도 있다! Git 레포지토리에 있는 몇 가지 예제 커밋을 사용하여 v2.29.2 태그에 리베이스한 다음 끝에 있는 커밋을 약간 수정해 보겠다.
$ git checkout -f 8e86cf65816
$ git rebase v2.29.2
$ echo extra line >>README.md
$ git commit -a --amend -m "replaced commit message"
$ git range-diff v2.29.2 8e86cf65816 HEAD
1: 17e7dbbcbc = 1: 2aa8919906 sideband: avoid reporting incomplete sideband messages
2: 8e86cf6581 ! 2: e08fff1d8b sideband: report unhandled incomplete sideband messages as bugs
@@ Metadata
Author: Johannes Schindelin <Johannes.Schindelin@gmx.de>
## Commit message ##
- sideband: report unhandled incomplete sideband messages as bugs
+ replaced commit message
- It was pretty tricky to verify that incomplete sideband messages are
- handled correctly by the `recv_sideband()`/`demultiplex_sideband()`
- code: they have to be flushed out at the end of the loop in
- `recv_sideband()`, but the actual flushing is done by the
- `demultiplex_sideband()` function (which therefore has to know somehow
- that the loop will be done after it returns).
-
- To catch future bugs where incomplete sideband messages might not be
- shown by mistake, let's catch that condition and report a bug.
-
- Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
- Signed-off-by: Junio C Hamano <gitster@pobox.com>
+ ## README.md ##
+@@ README.md: and the name as (depending on your mood):
+ [Documentation/giteveryday.txt]: Documentation/giteveryday.txt
+ [Documentation/gitcvs-migration.txt]: Documentation/gitcvs-migration.txt
+ [Documentation/SubmittingPatches]: Documentation/SubmittingPatches
++extra line
## pkt-line.c ##
@@ pkt-line.c: int recv_sideband(const char *me, int in_stream, int out)
커밋 17e7dbbcbc와 2aa8919906이 '동일'하다는 range-diff의 결과가 의미하는 바는 두 커밋이 동일한 패치를 생성한다는 뜻이다. 두 번째 커밋 쌍은 서로 다르며, 커밋 메시지가 변경되었고 원래 커밋에 없던 README.md가 수정되었음을 보여준다.
계속해서 조금 더 살펴보면 이 두 커밋에 대한 커밋 히스토리가 어떻게 여전히 존재하는지 확인할 수도 있다. 새 커밋은 히스토리에서 세 번째 커밋으로 v2.29.2 태그가 있고 이전 커밋은 세 번째 커밋으로 (이전) v2.28.0 태그가 있다.
$ git log --oneline -3 HEAD
e08fff1d8b2 (HEAD) replaced commit message
2aa89199065 sideband: avoid reporting incomplete sideband messages
898f80736c7 (tag: v2.29.2) Git 2.29.2
$ git log --oneline -3 8e86cf65816
8e86cf65816 sideband: report unhandled incomplete sideband messages as bugs
17e7dbbcbce sideband: avoid reporting incomplete sideband messages
47ae905ffb9 (tag: v2.28.0) Git 2.28
커밋이 diff가 아니라면 Git은 rename*을 어떻게 추적할까?
(*rename: 파일의 이름이나 경로 변경 - 옮긴이 주)
객체 모델을 주의 깊게 살펴본다면 Git이 저장된 객체 데이터에서 커밋들 간의 변경 사항을 추적하지 않는다는 것을 알 수 있을 것이다. "Git은 rename을 어떻게 아는 걸까?"라는 궁금증이 생길 것이다.
Git은 rename을 추적하지 않는다. Git 내부에는 커밋과 그 부모 사이에 rename이 일어났다는 기록을 저장하는 데이터 구조가 없다. 그 대신 Git은 동적으로 diff를 계산하는 중에 rename을 알아내려고 시도한다. 이러한 rename 감지 과정에는 두 가지 단계가 있는데, exact rename과 edit-rename이 그것이다.
먼저 diff를 계산한 후 Git은 해당 diff의 내부 모델을 검사하여 어떤 경로가 추가 또는 삭제되었는지 찾는다. 따라서 특정 위치에서 다른 위치로 이동한 파일은 처음 위치에서는 삭제되고, 바뀐 위치에서는 추가된 것으로 표시된다. Git은 이러한 추가와 삭제를 일치(match)시켜 추론된 rename 세트를 만들려고 시도한다.
이 매칭 알고리즘의 첫 번째 단계에서는 추가 및 삭제된 경로의 OID를 살펴보고 정확히 일치하는 항목이 있는지 확인한다. 이렇게 정확히 일치하는 파일은 서로 짝을 이룬다.
두 번째 단계는 비용이 많이 드는 부분이다. 이름 또는 경로가 변경되고 편집된 파일을 어떻게 감지할 수 있을까? Git은 추가된 각 파일을 순회하고 해당 파일을 삭제된 각 파일과 비교하여 공통된 라인의 퍼센테이지로 유사성 점수를 계산한다. 기본적으로 공통 라인의 비율이 50%를 넘으면 edit-rename 가능성이 있는 것으로 간주한다. 매칭 알고리즘은 최대 일치 항목을 찾을 때까지 이러한 쌍을 계속해서 비교한다.
이러한 과정에서 생길 수 있는 문제를 알아챘는가? 이 알고리즘은 A * D diffs를 실행하는데, 여기서 A는 추가 횟수이고 D는 삭제 횟수이다. 이것은 이차식이다! 엄청나게 긴 rename 계산을 피하기 위해 Git은 A + D가 내부 제한보다 큰 경우 edit-rename을 감지하는 부분을 건너뛴다. diff.renameLimit 설정 옵션을 사용해 이 제한을 수정할 수 있다. diff.renames 설정 옵션을 비활성화하여 해당 알고리즘을 완전히 사용하지 않을 수도 있다.
Git의 rename 감지에 대한 인식을 내 프로젝트에 직접 활용한 적이 있다. 예를 들어서 Scalar 프로젝트를 만들기 위해서 Git용 VFS 레포지토리를 포크했을 때 나는 많은 양의 코드를 재사용하면서도 파일 구조를 전반적으로 바꾸고 싶었다. 이러한 파일 히스토리를 VFS for Git 코드베이스의 버전으로 추적할 수 있기를 원했기 때문에 두 단계로 리팩토링을 구성했다.
1. blob을 변경하지 않고 모든 파일의 이름을 바꾼다.
2. 파일명을 변경하지 않고 blob을 수정하기 위해 문자열을 바꾼다.
이 두 단계를 통해 git log --follow -- <path>를 사용하여 해당 rename에 대한 파일 히스토리를 빠르게 확인할 수 있었다.
$ git log --oneline --follow -- Scalar/CommandLine/ScalarVerb.cs
4183579d console: remove progress spinners from all commands
5910f26c ScalarVerb: extract Git version check
...
9f402b5a Re-insert some important instances of GVFS
90e8c1bd [REPLACE] Replace old name in all files
fb3a2a36 [RENAME] Rename all files
cedeeaa3 Remove dead GVFSLock and GitStatusCache code
a67ca851 Remove more dead hooks code
...
위에서는 출력의 일부만 적었지만 이 마지막 두 커밋은 실제로는 Scalar/CommandLine/ScalarVerb.cs에 해당하는 경로가 없고, 대신 Git이 fb3a2a36 [RENAME] Rename all files 커밋에서 exact-content rename을 인식했기 때문에 이전 경로인 GVSF/GVFS/CommandLine/GVFSVerb.cs를 추적하고 있다.
다시는 속지 않기를!
이제 여러분은 커밋은 diff가 아니라 스냅샷이라는 사실을 잘 알게 되었으리라 믿는다! 이 사실을 잘 이해한다면 앞으로 여러분이 Git을 사용해 작업하는 데 많은 도움이 될 것이다.
여러분은 이제 Git 객체 모델에 대한 깊은 지식으로 무장했다. 이 지식을 활용하여 Git 명령어를 사용하거나 팀을 위한 워크플로우를 결정하는 데 필요한 기술을 확장시켜 나갈 수 있을 것이다. 다음 블로그 포스팅에서는 이 지식을 활용하여 다양한 Git clone 옵션과 작업을 완료하는 데 필요한 데이터의 양을 줄이는 방법에 대해 알아볼 예정이다!
'번역' 카테고리의 다른 글
Tailwind CSS 스타일 재사용하는 방법 (0) | 2024.05.19 |
---|---|
React Query로 에러 처리하기 (3) | 2024.01.21 |
React 초보부터 숙련자까지 활용할 수 있는 프로젝트 폴더 구조 (3) | 2023.06.02 |
[React] CRA(Create React App)의 시대가 저물다 (0) | 2023.05.17 |
왜 appendChild는 DOM 노드를 이 부모에서 저 부모로 이동시키는 걸까? (5) | 2022.11.16 |