Search
💻

Git 조금 더 잘 쓰기: 커밋을 기능별로 '효율적으로' 합치자

Date
2021/05/13
Tags
Git
Tutorial
Created by
커밋을 기능별로 효율적으로 합치는 여러 방법들을 정리해 보았다.
Table of Contents

문제 상황

하나의 PR 내에서 작업하다 보면 PR 내에 여러 커밋들이 쌓인다. 보통 커밋들이 하나의 Feature에 대한 것이라면 Rebase로 하나의 커밋으로 Squash해서 Force push하는 식으로 작업하겠지만, PR 내에서 기능 여러 개를 구현했다면 각 기능별로 커밋을 나누는 것이 바람직하다. 무조건 PR 하나에는 하나의 커밋만 있어야 하는 것도 아니고...
그러나, PR을 올리고 코드 리뷰 및 QA를 받다 보면 고쳐야 할 부분이 필연적으로 생긴다. 당연히 이에 대한 Fix commit도 쌓인다. 코드 리뷰를 잘 따라서 Fix commit을 추가했다면 머지 전까지는 가령 이런 식으로 커밋이 쌓였을 것이다.
편의상 커밋 해시는 알파벳 한 글자로 대체한다.
$ git log --pretty=oneline e (HEAD -> various-feat, origin/various-feat) PR fix2 d PR fix c ADD: feature 3 b ADD: feature 2 a ADD: feature 1
Shell
Feature 1, 2, 3에 대한 커밋 a, b, c가 각각 쌓여 있고, 코드 리뷰를 받으면서 고친 부분을 커밋한 Fix commit 2개 d, e가 쌓여 있다.
위에서도 말했듯, 이 커밋들을 모두 하나의 커밋으로 Squash하는 건 그다지 좋은 생각이 아니다. 가장 간단한 방법은 Interactive rebase(git rebase -i) 를 응용해 Feature commit에 해당하는 Fix commit을 끼워넣는 것이다.

Interactive rebase로 해결해보기

매우 간단하다. Rebase로 a~e 커밋을 하나의 커밋으로 Squash하듯이 git rebase -i <a 커밋 바로 이전 커밋 해시>로 시작하면 다음과 같이 기본 에디터가 뜰 것이다.
pick a ADD: feature 1 pick b ADD: feature 2 pick c ADD: feature 3 pick d PR fix pick e PR fix2
Shell
예를 들어 d 커밋이 a 커밋과 관련된 Fix commit이고, e 커밋이 b 커밋과 관련된 Fix commit이라면, 다음과 같이 고치고 저장하면 Interactive rebase가 완료된다.
pick a ADD: feature 1 s d PR fix pick b ADD: feature 2 s e PR fix2 pick c ADD: feature 3
Shell
그냥 순서를 바꿔서 바로 위의 커밋과 Squash해 주는 것이라고 생각하면 된다. 결과적으로 Git log를 보면 다음과 같은 식으로 커밋이 남아있을 것이다.
c ADD: feature 3 b ADD: feature 2 a ADD: feature 1
Shell
d 커밋은 a 커밋과 합쳐졌고, e 커밋은 b 커밋과 합쳐져서 총 3개의 커밋만 남게 된다.
그러나, 이 방법에는 한계가 있다. 만약 Fix commit이 각 Feature commit 하나에 대응하지 않는다면? 하나의 Fix commit에서 feature 1, feature 2의 문제점을 둘 다 수정했다면? Feature commit과 관련되지 않은 파일이 추가되었다면?
이런 상황에서 위처럼 Squash하려 하면 답이 없다. conflict의 향연을 보게 될 것이다.
이러한 경우, (Rebase로 수술하듯이 해 가는 것보다는) 첫 커밋 이전으로 Soft reset한 다음 필요한 파일들만 git add해서 커밋을 새로 만들어가는 편이 더 낫다.
가령 위의 5개 커밋을 통해 수정된 파일이 3개일 경우, 그리고 각 파일이 각 Feature에 대응할 경우, 첫 커밋 이전으로 Soft reset하면 다음과 같은 상태가 될 것이다.
$ git status On branch various-feat Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: feature1.ts modified: feature2.ts modified: feature3.ts
Shell
이 상태에서 git add feature1.tsgit commit -m "ADD: feature 1", git add feature2.tsgit commit -m "ADD: feature 2", ... 이런 식으로 커밋 3개를 깔끔하게 만들 수 있다.
그러나, 이렇게 해도 안 되는 경우가 있다! 한 파일에 있는 Change를 여러 개의 커밋으로 나눠야 할 경우에는 어떻게 할 것인가? 가령 various-feat-implementation.ts 파일의 여러 Change들을 두 개의 커밋으로 나누고 싶을 땐 어떻게 해야 할까?

git add patch (git add -p)

git add -p 옵션을 사용하면, 수정된 파일의 Change들을 묶음별로 나눠서 Stage할지 하지 않을지를 Interactive하게 정할 수 있다. 여기서 이 Change들의 묶음을 'hunk'라 한다.
예시를 하나 가져왔다.
git diff가 위와 같은 상황이라고 하자. 한 파일에 3개의 Change가 있지만, 이 파일을 git add해서 하나의 커밋으로 만드는 것은 그다지 좋은 선택이 아닌 것 같다. 3개의 커밋으로 나누고 싶다!
git add -p를 사용해보자.
다음과 같이 Change를 hunk 단위로 보여주고, 이 hunk에 대해 취할 수 있는 Option들을 보여준다. 각 Option 알파벳들은 다음과 같은 의미를 지닌다.
y - stage this hunk n - do not stage this hunk q - quit; do not stage this hunk or any of the remaining ones a - stage this hunk and all later hunks in the file d - do not stage this hunk or any of the later hunks in the file g - select a hunk to go to / - search for a hunk matching the given regex j - leave this hunk undecided, see next undecided hunk J - leave this hunk undecided, see next hunk k - leave this hunk undecided, see previous undecided hunk K - leave this hunk undecided, see previous hunk s - split the current hunk into smaller hunks e - manually edit the current hunk ? - print help
Plain Text
위의 예시는 3개의 Change를 하나의 hunk로 인식해서, 이 hunk를 Stage할 것인지 아닌지를 물어보고 있다. 우리는 각 Change를 하나씩 Stage해서 총 3개의 커밋으로 만들 예정이므로, hunk를 더 작게 나누어야 한다. s 옵션을 선택하자.
3개의 hunk로 쪼개졌다는 메시지가 나오고, 다시 1번째 hunk에 대해 이 hunk를 Stage할 것인지를 물어보고 있다.
첫 번째 커밋에서는 이 hunk만 Stage하고 나머지 hunk들은 Stage하지 않을 것이다. y, n, n 옵션을 선택하면 되겠다. 이후 git add -p가 종료되고 커밋하고 나면 다음과 같은 상황이 된다.
"기능 1" 부분의 Change만 하나의 커밋으로 빠졌고, 다른 Change들은 아직 Unstaged 상태이다.
이제 나머지 Change들도 git add -p를 이용해 hunk 단위로 Stage시켜 커밋할 수 있다.
마지막 남은 Change는 그냥 git add -a 해 줘도 상관없다.
git log를 보면 우리가 원했던 대로 하나의 파일에 대한 3개의 커밋이 생성된 것을 볼 수 있다.

VSCode에서 편리하게 사용하기 (Stage selected ranges)

위에서 설명한 git add patch를 VSCode에서는 GUI로 간편하게 사용할 수 있다.
간단하게 이야기하면, Soft reset한 상태에서 Unstaged file을 선택하면 diff가 뜰 텐데, 이 상태에서 따로 커밋하고 싶은 부분만 드래그해서 Cmd+K, Cmd+Alt+S 하면 해당 줄만 Staged 상태로 빠진다.
자세한 설명은 'VSCode State selected ranges' 키워드로 찾아보자.