배경
새로운 팀 프로젝트를 진행하게 되면서, ktlint 등 프로젝트 유지 보수 등을 위한 여러 건설적인 이야기들이 오갔다
그러던 중 문득 이전 프로젝트를 진행하면서 Git Repository Graph가 너무 지저분해서 아쉬웠던 것이 떠올랐고,
Git Commit 등 관련해서 Graph를 깔끔하게 정리하고 싶었다
불필요한 Commit과 복잡해진 Graph
특히 아쉬웠던 것은 ~구현 중과 같은 임시 저장을 위한 commit들이 생겨나며, 나중에 cherry-pick으로 필요한 부분만 가져오기가 힘든 경우가 있었고,
feature/login에서 develop으로 merge 후 계속 작업할 때 종종 팀원들이 develop을 feature/login으로 다시 merge 시킨 후 작업을 하며 Repository Graph가
flowchart LR; A(Commit A) -->|feature/login 브랜치| B(Commit B); A(Commit A) -->|develop 브랜치| C(Commit C); B(Commit B) --> D(Commit D); D(Commit D) -->|feature/login -> develop으로 Merge| E(Commit E); D -->|쓸데 없는 흐름| F; C(Commit C) --> E(Commit E); E(Commit E) -->|develop -> feature/login으로 Merge| F(Commit F); F(Commit F) -->|이어서 다시 작업| G(Commit G); E -->|develop 브랜치| H(Commit H);이런 식으로 불필요한 commit과 쓸데 없는 흐름이 생겼다
Merge를 한 뒤 Pull을 develop으로 받게 될 경우 Graph를 따라서 이동해 develop과 같은 곳을 바라볼 수 있음에도,
불필요한 Merge 작업으로 불필요한 commit과 불필요한 Graph 흐름이 생기곤 했다.
위 Graph는 아래와 다를 것이 없다
flowchart LR; A(Commit A) -->|feature/login 브랜치| B(Commit B); A(Commit A) -->|develop 브랜치| C(Commit C); B(Commit B) --> D(Commit D); D(Commit D) -->|feature/login -> develop으로 Merge| E(Commit E); C(Commit C) --> E(Commit E); E(Commit E) -->|develop을 pull 받은 후 F 작업| F(Commit F); F(Commit F) -->|이어서 작업| G(Commit G); E -->|develop 브랜치| H(Commit H);지저분한 Commit
매일 한 곳에서만 작업을 해서 완성될 때만 commit을 남긴다면 참 깔끔하겠지만,
유감스럽게도 우리는 아침 ~ 저녁 전과 저녁 이후의 작업 공간이 달랐다
그래서 마저 다 완료하지 못한 작업이 있다면 어쩔 수 없이 commit을 Remote Branch에 올리고서 내려 받아 작업을 할 수 밖에 없었는데,
나중에 특정 기능만 가져올 필요가 있거나, 특정 지점으로 되돌리고 싶은 경우에 특정 기능에 대한 commit이 여러개로 나뉘어져 있어,
가져오기가 까다롭거나, 특정 기능 전으로 되돌리고 싶지만, commit이 복잡하게 얽혀서 어렵거나, log를 볼 때 “~ 구현 중” 과 같은
불필요한 commit들이 너무 많아 log 보기가 힘들었다
우린 기본적으로 3-way Merge 방식을 사용했었고, log를 보기가 힘들고, commit이 복잡하다면 squash merge 방법을 쓴다면 해결할 수 있지만,
이 부분은 우리 자체의 잘못인건지 또 한번 유감스럽게도 우리는 브랜치를 재활용하는 일이 잦았다
feature/login에서 기능 구현을 한 뒤 develop에 merge 했다가, 스타일을 바꾸거나, 레이아웃을 바꾼 뒤 다시 merge를 진행한다던지 등
squash merge의 경우 보통 아래와 같은 상황일 경우
flowchart LR; A(Commit A) -->|feature/login 브랜치| B(Commit B); A(Commit A) -->|develop 브랜치| C(Commit C); B(Commit B) --> D(Commit D);git switch develop git merge --squash feature/login git commit위의 코드처럼 merge를 진행하여 아래와 같은 흐름으로 가져갈 수 있다
flowchart LR; A(Commit A) -->|feature/login 브랜치| B(Commit B); A(Commit A) -->|develop 브랜치| C(Commit C); B(Commit B) --> D(Commit D); C -->|Squash Merge로 Commit B,D의 내용들이 합쳐진 하나의 커밋| E(Commit E);이렇게 될 경우 기본적인 3-way Merge와 달리 Merge를 진행한 Branch와 이어지지 않고, 보통은 Branch를 삭제하면서 깔끔하게 유지할 수 있다
다만 앞서 말했듯이 우리는 Branch를 재활용 하는 경우가 많았고 이렇게 Branch가 이어지지 않을 경우 이어서 작업을 진행하기 위해서는,
이어지지 않았으므로 다시 Merge를 역으로 한 뒤에 작업을 하거나, commit을 branch가 나뉘기 전까지 reset –hard로 밀어버린 다음에 pull을 받아서 진행하거나 하는 등의 번거로움이 생기는 문제가 있었다
해결책
불필요한 Commit과 복잡해진 Graph
해결책은 간단하다
기본적인 방식인 3-way Merge를 하게 되면 아래와 같은 흐름이 된다
flowchart LR; A(Commit A) -->|feature/login 브랜치| B(Commit B); A(Commit A) -->|develop 브랜치| C(Commit C); B(Commit B) --> D(feature/login); D(feature/login) --> F(Merge Commit, develop); C(Commit C) --> F(Merge Commit, develop);이 때 보면 알겠지만 feature/login과 develop은 간선으로 이어져 있다
PS에서 Graph 문제를 푸는 것과 유사하게, 간선이 있으므로 feature/login 위치에서 develop branch를 pull을 하게 되면
feature/login branch의 위치는 아래처럼 develop branch가 있는 위치로 갈 수 있다(내려받아진다)
flowchart LR; A(Commit A) -->|feature/login 브랜치| B(Commit B); A(Commit A) -->|develop 브랜치| C(Commit C); B(Commit B) --> D(Commit D); D(Commit D) --> F(Merge Commit, develop, feature/login); C(Commit C) --> F(Merge Commit, develop, feature/login);이렇게만 하면 굳이 불필요한 Merge 없이 이어서 작업을 이어나갈 수 있다.
지저분한 Commit
git log도 깔끔하게 가져가고 불필요한 commit도 없애면서 Branch를 재활용 하는 방법으로 생각해낸 나의 방법이 이른바 squash and merge다 (이미 존재했다면 유감, squash merge X)
이 방법은 간단하다
- Merge전에 Squash로 commit 이력을 정리한다
- Merge한다
구체적인 설명으로 들어가면 아래와 같은 흐름일 때
flowchart LR; A(Commit A) -->|feature/login 브랜치| B(Commit B); A(Commit A) -->|develop 브랜치| C(Commit C); B(Commit B) --> D(Commit D);Squash를 통해 feature/login에서 B, D Commit을 하나의 Commit으로 합친다
commit을 정리하고자 하는 branch에서
git log명령어를 통해 내가 합치고 싶은 브랜치가 어디인지를 확인한다
그러면
commit A – 현재 위치
commit B
commit C와 같은 hash가 달린 commit 내역들이 나올 것이다
여기서 B까지 합치고 싶을 때
commit C의 hash 번호를 복사한다
그리고
git rebase -i 복사한 C_commit_해시_번호를 하면 창이 뜰 것이다
그러면
pick A pick B이런 식으로 합치고자 했던 commit 내역들이 나올 텐데
맨 위를 제외한 나머지들의 pick을 전부 s or squash로 바꿔준다
pick A s B그리고 저장하면
commit 메세지를 수정할 수 있는 창이 나오고 이곳에서 commit을 작성한 뒤 저장하면 된다
다만 remote에 이미 올라가 있는 commit들을 합친 경우 remote로 push하려고 하면 graph 모양이 달라 충돌이 날 것이다
이 때는 git push -f로 squash한 graph로 강제로 반영을 해서 바꿔치기를 해줘야 한다
합치면 아래와 같이 될 것이다
flowchart LR; A(Commit A) -->|feature/login 브랜치| X(Commit X - B, D 커밋 합쳐진 것); A(Commit A) -->|develop 브랜치| C(Commit C);그 뒤 일반적인 3-way merge를 진행한다
flowchart LR; A(Commit A) -->|feature/login 브랜치| X(Commit X - B, D 커밋 합쳐진 것); A(Commit A) -->|develop 브랜치| C(Commit C); X --> Y; C --> Y(Commit Y, develop);이렇게 진행한다면 develop은 Y commit 위치에 feature/login은 X commit 위치에 있게 되고,
그럼 이후 다시 작업이 필요할 경우에는 pull을 받아서 진행하면 되고, log를 찍어도 “~ 구현 완료”와 같은 commit만 남게 될 것이다
이를 통해 branch를 재활용하면서도 commit과 log를 깔끔하게 관리하고자 한다