Search
Duplicate

(git) rebase -i 로 커밋 가지고 놀기 & Rebasing Merge

간단소개
rebase에 대해 깊숙히 알아보자!
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
git
태그
Scrap
8 more properties
여러개의 커밋을 하나로 합치고 싶을 때,
몇몇 커밋의 메세지를 편집하고, 삭제하고 싶을 때,
복잡한 과정의 rebase가 필요할 때
이런 상황에 활용하면 좋은 명령이 바로 rebase의 interactive옵션이다!

git rebase -i 옵션 (--interactive)

git rebase는 대부분 브랜치 병합 목적으로 주로 활용되지만, 현재 브랜치의 HEAD포인터를 이동시킬 수 있는 특성을 활용해서 커밋 히스토리를 수정하는 데에도 활용한다. --interactive 옵션은 대화형으로 실행하는 옵션인데 시각적으로 커밋 로그들과 옵션을 나열해주기 때문에 실수하지 않고 수정할 수 있도록 도와준다.
본 포스팅에서는 rebase를 활용해서 커밋들을 수정, 삭제, 추가하는 내용과 Rebasing Merge에 대해서 간단하게 다뤄볼 것이다.

준비하기

우선 git 레포지토리를 준비한다. 아래 사진처럼 미리 5개의 커밋을 기록해둔 상태이다.

명령어 실행

git rebase -i 옵션 뒤에는 수정하고 싶은 커밋의 직전 커밋을 입력하면 된다. 위 사진에서 2번 커밋을 수정하고 싶다면 1번 커밋을 입력하면 된다. 혹은 HEAD를 기준으로 범위를 지정해 주는 방법도 존재한다.
# 2번 커밋을 수정하고 싶다면 git rebase -i 616ae23 # 범위를 지정하고 싶다면 git rebase -i HEAD~4
Shell

interactive editor 실행

명령어를 실행하고 나면 아래 사진처럼 interactive editor가 실행된다.
가장 아래쪽이 최신 커밋, 즉 HEAD가 가리키는 커밋이다. 커밋 목록 아래 주석을 살펴보자.
# Rebase 616ae23..536a328 onto 616ae23 (4 commands)
Shell
커밋 616ae23부터 536a328 까지를 616ae23 다음으로 Rebase한다고 되어있다. 즉 616ae23 다음 커밋부터 4개의 커밋을 616ae23커밋뒤에 작업을 해서 이어 붙인다는 뜻이다.
이상태에서 :wq 를 활용해 interactive editor를 종료시키면 주석이 되어있지 않은 라인을 한줄씩 실행시키게 된다. 즉, 위 사진을 그대로 수정없이 저장, 종료하게 된다면, 각각의 커밋에 대해 모두 pick 명령어를 실행하게 되는 것이다.

명령어 종류 및 설명

사진에서 commit을 포함하는 4줄은 각각 명령어 커밋해시 커밋메세지 순으로 구성되어있다. 사진에서의 pick도 명령어의 한 종류이다. 이제부터 여기에 사용되는 명령어의 종류와 용도를 알아보자.
pick 86a9707 commit 2 pick b3b7888 commit 3 pick 7c1d3e3 commit 4 pick 536a328 commit 5
Plain Text

1) Pick

p, pick <commit> = use commit
Shell
pick은 해당 커밋을 그대로 사용하겠다는 명령어이다. default로 지정되어있는 명령어이다. 해당 커밋을 그대로 사용할 것이기 때문에 수정없이 종료하게 된다면 기존의 log와 달라진 점이 없는 것을 확인할 수 있다.
이 때, pick을 활용해서 커밋의 순서를 바꿀 수도 있다. 만약 2번과 5번 커밋의 순서를 바꾸고 싶다면 아래와 같이 변경한 후, 저장하면 된다.
pick 536a328 commit 5 pick b3b7888 commit 3 pick 7c1d3e3 commit 4 pick 86a9707 commit 2
Plain Text
이제 git log --oneline을 통해 커밋기록을 살펴보면 정상적으로 순서가 바뀐것을 볼 수 있다.
바꾸려는 커밋이 같은 파일을 수정하지 않았는지 주의한다. 만약 같은 파일을 수정했는데 순서를 변경하는 경우 충돌이 발생할 수 있다.

2. reword

r, reword <commit> = use commit, but edit the commit message
Shell
reword는 기존의 커밋을 그대로 사용하지만, 커밋의 메세지만 변경해준다. 컨벤션에 맞지 않는 커밋, 오타가 있는 커밋들을 수정할 때 편하게 사용할 수 있다. 메세지를 수정하고 싶은 커밋의 명령어를 reword로 바꾸고 :wq 를 해주면 아래와 같은 또 하나의 editor가 등장한다. 여기서 기존의 메세지를 원하는 메세지로 변경후 다시 저장, 종료를 해주면 된다.
... reword 86a9707 commit 2
Plain Text
기존 메세지를 commit 2 (메세지 수정됨) 으로 변경해 보았다.
메세지가 정상적으로 수정된 것을 볼 수 있다.

3. edit

e, edit <commit> = use commit, but stop for amending
Shell
edit은 사용법이 조금 복잡한데 위의 명령어들 처럼 커밋을 그대로 사용하지만, 커밋메세지와 작업 내용까지 수정할 수 있다. 예제를 보면서 학습해보자.
... edit 86a9707 commit 2 (메세지 수정됨)
Plain Text
수정 후, 종료하게 되면 아래와 같은 설명이 나오게 된다. 그리고 실행 후, HEAD의 위치가 해당 커밋으로 옮겨진 것을 확인할 수 있다. 내용을 살펴보면, git commit --amend명령을 사용해 커밋 메세지를 수정할 수 있고, 모든 작업을 완료했다면 git rebase --continue 를 사용하면 된다고 한다.
git rebase -i HEAD~4 Stopped at c94b72e... commit 2 (메세지 수정됨) You can amend the commit now, with git commit --amend Once you are satisfied with your changes, run git rebase --continue
Shell
우선 git commit --amend를 활용해서 메세지를 한번 더 수정하고, 새로운 파일을 만들어 새 커밋을 추가해 보았다. 그리고 git rebase --continue 명령을 사용하였다.
최종적으로 나오는 결과를 살펴보자. 기존의 307db8c 커밋 메세지가 수정되었고, 그 다음으로 커밋이 하나 더 추가된 것을 확인할 수 있다.
edit을 활용하면 특정 커밋과 커밋 사이에 새로운 커밋을 추가할 수도 있다.

4. squash

s, squash <commit> = use commit, but meld into previous commit
Shell
squash는 지정한 커밋을 이전 커밋과 합칠 때 사용한다. 아래처럼 명령어를 수정한 뒤 저장해보자. 여기서 하나의 커밋만 이전의 커밋과 합칠 수 있는게 아니라 아래처럼 여러개의 커밋을 하나의 커밋으로 합칠 수 있다.
pick 536a328 commit 3 squash b3b7888 commit 4 squash 7c1d3e3 commit 2 (메세지 수정됨) + 한번 더 수정함 squash 86a9707 commit 5.5
Shell
interactive editor를 저장, 종료하고 나면 아래 사진과 같은 화면이 나오는데, 설명을 살펴보면 변경하고 싶은 커밋을 본 editor에서 수정하라고 적혀있다. 이 때, 주석은 무시된다. 여기서 3번 커밋에 추가로 (합쳐짐)을 적어보겠다.
마찬가지로 저장, 종료 후 결과를 살펴보면 정상적으로 커밋들이 합져진 것을 볼 수 있다. 이 때, 각 커밋들의 메세지가 모두 합쳐져서 보이게 된다.
가능하면 원격 저장소에 push하지 않은 커밋만 squash하자 이미 push 한 커밋을 squash 한다면 원격 저장소에 forced update를 해주어야 한다. 해당 내용을 forced update하기 직전에 pull한 다른 사용자가 있다면, 문제가 더 심각해 질 수 있다. ex) git push -f

5. fixup

f, fixup <commit> = like "squash", but discard this commit's log message
Shell
fixupsquash와 비슷하게 동작하지만, 지정한 커밋의 메세지는 무시한다. 즉, fixup 으로 지정한 커밋메세지는 사라지게 되고 이전의 커밋 메세지만 남게된다.
fixupsquash와 동일하게 동작하기 때문에 예제는 따로 작성하지 않았다.

6. exec

x, exec <command> = run command (the rest of the line) using shell
Shell
exec는 다른 프로그램과 마찬가지로 실행도중 외부 명령어를 수행할 수 있도록 도와주는 명령어이다. 위에서 설명했듯이 interactive editor를 저장, 종료하게 되면 주석을 작성하지 않은 부분을 순서대로 한줄 씩 실행시키는데, 이때 원하는 위치에서 exec를 활용하면 쉘 명령어를 사용할 수 있다.
아래처럼 수정한 후 저장, 종료 해보자.
pick 536a328 commit 1 exec echo hello world pick b3b7888 commit 5 exec git log -1 --oneline pick 7c1d3e3 commit 3 (합쳐짐)
Shell
이렇게 원하는 위치에서 원하는 명령어를 사용할 수 있다.

7. break

b, break = stop here (continue rebase later with 'git rebase --continue')
Shell
break는 해당 시점에서 Rebase를 중지시킨다. git rebase - -continue를 통해서 다시 시작할 수 있다.

8. drop

d, drop <commit> = remove commit
Shell
drop은 지정한 커밋을 삭제한다. 아예 해당 커밋 라인을 삭제하는 것과 동일하다.
# 7c1d3e3 커밋 삭제하는 법 1 pick b3b7888 commit 5 drop 7c1d3e3 commit 3 (합쳐짐) # 7c1d3e3 커밋 삭제하는 법 2 pick b3b7888 commit 5
Shell

9. Rebasing Merge

l, label <label> = label current HEAD with a name t, reset <label> = reset HEAD to a label m, merge [-C <commit> | -c <commit>] <label> [# <oneline>] . create a merge commit using the original merge commit's . message (or the oneline, if no original merge commit was . specified). Use -c <commit> to reword the commit message.
Shell
마지막으로 세개의 옵션이 남았다. 이 세개의 옵션은 Rebasing Merge를 위한 옵션이다. Rebasing Merge에 대해 간단히 알아보자.
아래 내용부터는 rebase, cherry-pick에 대한 이해가 있다고 가정하고 설명합니다.
아래와 같은 브랜치 구조를 가진 작업영역이 있다.
I--J / \ H M <-- feature (HEAD) / \ / / K--L / ...--G-------N--O--P <-- main
Plain Text
우리는 위 구조를 아래와 같이 만들고 싶다.
I'-J' / \ H' M' <-- feature (HEAD) / \ / / K'-L' / ...--G-------N--O--P <-- main
Plain Text
우리가 만약 여기서 일반적인 rebase를 사용한다면 아래와 같을 것이다. 위 구조에서 merge된 커밋 M’은 rebase된 후 필요없어지기 때문에(선형 병합이기 때문에) drop된다.
...--G-------N--O--P <-- main \ H'-I'-J'-K'-L' <-- feature (HEAD)
Plain Text
아래와 같은 구조를 만들기 위해서는 rebase과정에서 J’과 L’의 merge를 보존해야한다. 따라서 우리는 아래와 같은 과정을 순차적으로 진행해야할 필요가 있다.
I'-J' / \ H' M' <-- feature (HEAD) / \ / / K'-L' / ...--G-------N--O--P <-- main
Plain Text
여기서부터 나오는 복사는 cherry-pick을 의미한다
1.
main에서 H를 복사해서 H’을 만든다 → H’에 marker1를 지정해둔다
2.
H’으로 pointer(git switch)를 옮긴 후, I - J를 복사해 I’ - J’을 만든다 → J’ 에 marker2를 지정해둔다
3.
다시 H’으로 pointer이동한다(이전에 만든 marker를 활용)
4.
K - L을 복사해 K’ - L’을 만든다 → K’에 marker3을 지정해둔다.
이 과정을 거치면 아래와 같은 형태가 된다.
I'-J' <-- marker2 / H' <-- marker1 / \ / K'-L' <-- marker3 / ...--G-------N--O--P <-- main
Plain Text
이제 우리는 marker2marker3를 활용해서 merge를 진행할 수 있다. 예를들어 아래와 같은 명령어를 활용한다.
git switch marker2 git merge marker3
Shell
여기서 우리가 조금 더 쉽고 멋지게 해결할 수 있다. 이것을 하나하나 작성하는 것이 아니라 바로 Rebasing Merge를 활용하는 것이다. 위의 구조를 코드로 작성해보자.
pick hhhhhhh commit H label marker1 pick iiiiiii commit I pick jjjjjjj commit J label marker2 reset marker1 pick kkkkkkk commit K pick lllllll commit L reset marker2 # merge -C <commit> : create a merge commit # using the original merge commit's message # merge -c <commit> : to reword the commit message. merge marker3
Shell
이때 label은 현재 rebase가 동작하는 시점의 HEAD 포인터가 가리키는 위치에 label을 만든다. 이때 생성된 label들은 worktree-local refs(refs/rewritten/<label>)를 생성하는데 만약 rebase가 끝나게 되면 생성된 모든 label이 삭제된다.
reset은 HEAD 포인터 및 worktree를 특정한 지점으로 바꿔준다. 하지만 만약 reset을 실행하는데 실패하게 된다면 아래와 같이 즉시 수정하는 방법에 대한 가이드를 출력된다. rebase가 동작하는 런타임에 지정한 label이 없는 경우에 실패할 수 있다.
merge는 git merge와 거의 동일하게 동작한다. 만약 merge 도중 충돌이 발생한다면, merge에 실패할 수 있다. Auto-mergeing에 실패하게 된다면 아래와 같은 에러를 출력한다.
실제 충돌난 부분은 원래 conflict 난 파일을 처리하는 것과 동일한 형태로 되어있다.

명령 취소하기

위의 경우처럼 중간에 rebase를 실패하거나 중단된 경우, 아래 명령어를 활용해서 실행한 rebase를 되돌릴 수 있다. 명령을 실행하면 rebase를 실행하기 전으로 되돌아간 것을 볼 수 있다.
git rebase --abort
Shell

부가설명

These lines can be re-ordered; they are executed from top to bottom. If you remove a line here THAT COMMIT WILL BE LOST. However, if you remove everything, the rebase will be aborted.
Plain Text
마지막으로 모든 옵션에 대한 설명이 나온 후, 몇가지의 추가 설명이 나온다. 그대로 해석하면 아래와 같다.
각 라인들은 재배열 될 수 있으며, top → bottom으로 실행된다
만약 특정 라인을 지운다면 해당 커밋을 잃을 것이다(삭제)
하지만, 모든 내용을 지운다면, 이 rebase는 중단될 것이다.

Reference