“이른 시기에 자주 커밋하라”는 Git를 사용할 때 소프트웨어 개발에서 널리 알려진 말입니다. 이렇게 하면 각 변경 사항이 잘 문서화되고 협업이 향상되며 프로젝트의 진화를 쉽게 추적할 수 있습니다. 하지만 이는 커밋이 과도하게 많아질 수 있는 결과를 초래할 수도 있습니다.
이때 커밋을 병합하는 것의 중요성이 부각됩니다. 커밋을 병합하는 것은 여러 개의 커밋 엔트리를 단일, 일관된 커밋으로 결합하는 과정입니다.
예를 들어, 로그인 폼을 구현하는 기능에 작업을 하고 다음과 같은 네 개의 커밋을 만들어보겠습니다:
기능이 완료되면 전체 프로젝트에 대해 이러한 커밋은 너무 상세합니다. 우리는 미래에 개발 중에 수정된 버그를 겪었다는 사실을 알 필요가 없습니다. 메인 브랜치에서 깨끗한 기록을 보장하기 위해 이러한 커밋을 하나의 커밋으로 압축합니다:
Git에서 커밋 압축하기: 인터랙티브 리베이스
커밋을 압축하는 가장 일반적인 방법은 인터랙티브 리베이스를 사용하는 것입니다. 우리는 다음 명령어로 시작합니다:
git rebase -i HEAD~<number_of_commits>
<number_of_commits>
를 압축하고자 하는 커밋 수로 대체하세요.
우리의 경우 커밋이 네 개 있으므로 명령어는 다음과 같습니다:
git rebase -i HEAD~4
이 명령어를 실행하면 인터랙티브 커맨드라인 편집기가 열립니다:
상단 섹션에는 커밋이 표시되고 하단에는 커밋을 스쿼시하는 방법에 대한 댓글이 포함되어 있습니다.
네 개의 커밋이 보입니다. 각 커밋에 대해 어떤 명령을 실행할지 결정해야 합니다. 우리는 pick
(p
) 및 squash
(s
) 명령에 관심이 있습니다. 이 네 개의 커밋을 하나의 커밋으로 스쿼시하기 위해, 첫 번째 커밋을 선택하고 나머지 세 개를 스쿼시할 수 있습니다.
각 커밋 앞의 텍스트를 수정하여 명령을 적용합니다. 특히 두 번째, 세 번째, 네 번째 커밋에 대해 pick
을 s
또는 squash
로 변경합니다. 이러한 편집을 하려면 키보드에서 i
키를 눌러 명령줄 텍스트 편집기의 “INSERT” 모드에 들어가야 합니다:
i
를 누르면 하단에 -- INSERT --
텍스트가 나타나며, 이는 우리가 삽입 모드에 들어갔음을 나타냅니다. 이제 화살표 키로 커서를 이동하고, 문자를 삭제하고, 일반 텍스트 편집기처럼 입력할 수 있습니다:
변경 사항에 만족하면, 키보드의 Esc
키를 눌러 삽입 모드를 종료해야 합니다. 다음 단계는 변경 사항을 저장하고 편집기를 종료하는 것입니다. 이를 위해 먼저 :
키를 눌러 편집기에 명령을 실행할 것임을 알립니다:
편집기 하단에 이제 명령을 입력하라는 세미콜론 :
이 표시됩니다. 변경 사항을 저장하려면 “쓰기”를 의미하는 w
명령을 사용합니다. 편집기를 닫으려면 “종료”를 의미하는 q
를 사용합니다. 이 명령은 함께 결합하여 wq
로 입력할 수 있습니다:
명령을 실행하려면 Enter
키를 누릅니다. 이 작업은 현재 편집기를 닫고 새로운 편집기를 열어 새로 병합된 커밋의 커밋 메시지를 입력할 수 있게 합니다. 편집기는 우리가 병합하는 네 개의 커밋 메시지로 구성된 기본 메시지를 표시합니다:
이 결합된 커밋으로 구현된 변경 사항을 정확하게 반영하도록 메시지를 수정하는 것을 추천합니다. 결국 스쿼시의 목적은 깔끔하고 읽기 쉬운 기록을 유지하는 것입니다.
편집기와 상호작용하고 메시지를 수정하기 위해 i
를 다시 눌러 편집 모드에 들어가고 원하는 대로 메시지를 수정합니다.
이 경우 커밋 메시지를 “로그인 폼 구현.”으로 교체합니다. 편집 모드를 종료하려면 Esc
를 누릅니다. 그런 다음 :
를 눌러 변경 사항을 저장하고 wq
명령을 입력한 후 Enter
를 누릅니다.
커밋 기록 보기
일반적으로 전체 커밋 기록을 기억하는 것은 어려울 수 있습니다. 커밋 기록을 보려면 git log
명령을 사용할 수 있습니다. 언급된 예에서 스쿼시를 수행하기 전에 git log
명령을 실행하면 다음과 같은 결과가 표시됩니다:
커밋 목록을 탐색하려면 위 아래 화살표 키를 사용하세요. 종료하려면 q
를 누르세요.
우리는 스쿼시의 성공을 확인하기 위해 git log
을 사용할 수 있습니다. 스쿼시 이후에 실행하면 새 메시지가 있는 단일 커밋이 표시됩니다:
스쿼시한 커밋 푸시
위의 명령은 로컬 저장소에 작용합니다. 원격 저장소를 업데이트하려면 변경 사항을 푸시해야 합니다. 그러나 커밋 기록을 변경했기 때문에 --force
옵션을 사용하여 강제 푸시해야 합니다:
git push --force origin feature/login-form
강제 푸시하면 원격 브랜치의 커밋 기록이 덮어쓰여 다른 사람들이 해당 브랜치에서 작업하는 것을 방해할 수 있습니다. 이 작업을 하기 전에 팀과 소통하는 것이 좋은 실천 방법입니다.
협업자를 방해할 위험을 줄이는 더 안전한 강제 푸시 방법은 대신 --force-with-lease
옵션을 사용하는 것입니다:
git push --force-with-lease origin feature/login-form
이 옵션은 원격 브랜치가 마지막으로 가져온(fetch) 또는 풀(pull)한 이후로 업데이트되지 않은 경우에만 강제 푸시를 수행하도록 보장합니다.
특정 커밋 스쿼싱
다섯 개의 커밋이 있다고 가정해 봅시다:
커밋 1, 2, 5는 유지하고 커밋 3과 4는 스쿼시(squash)하고 싶다고 가정해 보겠습니다.
인터랙티브 리베이스를 사용할 때 스쿼싱을 위해 표시된 커밋은 바로 앞의 커밋과 결합됩니다. 이 경우, Commit4
를 스쿼시하여 Commit3
에 병합하고 싶다는 의미입니다.
이를 위해, 우리는 이 두 개의 커밋을 포함하는 인터랙티브 리베이스를 시작해야 합니다. 이 경우, 세 개의 커밋이면 충분하므로 다음 명령어를 사용합니다:
git rebase -i HEAD~3
그런 다음 Commit4
를 s
로 설정하여 Commit3
과 합쳐지도록 합니다:
이 명령어를 실행하고 커밋 목록을 확인한 후, 커밋 3과 4가 함께 합쳐진 것을 확인할 수 있으며 나머지는 변경되지 않았습니다.
특정 커밋에서 스쿼싱하기
명령어 git rebase -i HEAD~3
에서 HEAD
부분은 가장 최근의 커밋을 나타내는 약칭입니다. ~3
구문은 커밋의 조상을 지정하는 데 사용됩니다. 예를 들어, HEAD~1
은 HEAD
커밋의 부모를 가리킵니다.
대화식 리베이스에서 고려되는 커밋에는 명령어에서 지정된 커밋까지 이어지는 모든 조상 커밋이 포함됩니다. 지정된 커밋 자체는 포함되지 않습니다:
HEAD 대신에 직접 커밋 해시를 지정할 수 있습니다. 예를 들어, Commit2
의 해시는 dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
이므로 다음 명령어:
git rebase -i dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
는 Commit2
이후에 만들어진 모든 커밋을 고려하여 리베이스를 시작할 것입니다. 따라서 특정 커밋에서 리베이스를 시작하고 해당 커밋을 포함하려면 다음 명령어를 사용할 수 있습니다:
git rebase -i <commit-hash>~1
커밋을 통합할 때 충돌 해결하기
커밋을 통합하면 여러 커밋 변경 사항을 단일 커밋으로 결합하게 되는데, 변경 사항이 겹치거나 크게 다를 경우 충돌이 발생할 수 있습니다. 충돌이 발생할 수 있는 일반적인 시나리오는 다음과 같습니다:
- 중첩 변경: 파일의 동일한 행 또는 밀접한 관련 행을 수정한 두 개 이상의 커밋이 병합되는 경우, Git은 이러한 변경 사항을 자동으로 조화시키지 못할 수 있습니다.
- 다른 변경 사항 상태: 한 커밋이 특정 코드 조각을 추가하고 다른 커밋이 해당 동일한 코드 조각을 수정하거나 삭제하는 경우, 이러한 커밋을 병합하면 해결해야 할 충돌이 발생할 수 있습니다.
- 이름 변경 및 수정: 파일 이름을 바꾸는 커밋과 그 후 커밋들이 이전 이름을 변경하는 경우, 이러한 커밋을 병합하면 Git이 혼란스러워져 충돌을 일으킬 수 있습니다.
- 이진 파일에 대한 변경 사항: 이진 파일은 텍스트 기반의 diff 도구를 사용하여 잘 병합되지 않습니다. 여러 커밋이 동일한 이진 파일을 변경하고 이를 스쿼시하려고 하면 Git이 이러한 변경 사항을 자동으로 조정할 수 없기 때문에 충돌이 발생할 수 있습니다.
- 복잡한 히스토리: 커밋에 여러 병합, 분기 또는 리베이스가 포함된 복잡한 히스토리가 있는 경우, 이를 스쿼시하면 변경 사항의 비선형적 특성으로 인해 충돌이 발생할 수 있습니다.
스쿼시할 때 Git은 각 변경 사항을 하나씩 적용하려고 시도합니다. 이 과정에서 충돌이 발생하면 일시 중지하고 이를 해결할 수 있도록 합니다.
충돌은 충돌 마커 <<<<<<
와 >>>>>>
로 표시됩니다. 충돌을 처리하기 위해 파일을 열고 어떤 코드 부분을 유지할지 선택하여 수동으로 해결해야 합니다.
충돌을 해결한 후에는 git add
명령어를 사용하여 해결된 파일을 스테이징해야 합니다. 그런 다음 다음 명령어를 사용하여 리베이스를 계속할 수 있습니다:
git rebase --continue
Git 충돌에 대한 더 많은 정보는 Git에서 병합 충돌을 해결하는 방법에 대한 이 튜토리얼을 확인하세요.
리베이스를 사용한 스쿼시의 대안
git merge –squash 명령어는 여러 커밋을 하나의 커밋으로 합치는 대안적인 방법으로 git rebase -i
와 같습니다. 이 명령어는 개별 커밋을 모두 하나로 압축하여 브랜치에서 메인 브랜치로 변경 사항을 병합하고 싶을 때 특히 유용합니다. 다음은 git merge
를 사용하여 스쿼시하는 방법에 대한 개요입니다:
- 우리는 변경 사항을 통합하고자 하는 대상 브랜치로 이동합니다.
git merge --squash <branch-name>
명령어를 실행하며,<branch-name>
을 브랜치의 이름으로 교체합니다.- 우리는
git commit
을 사용하여 변경 사항을 커밋하여 기능 브랜치의 모든 변경 사항을 나타내는 단일 커밋을 생성합니다.
예를 들어, 우리가 feature/login-form
브랜치의 변경 사항을 하나의 커밋으로 main
에 통합하고 싶다고 가정해보겠습니다:
git checkout main git merge --squash feature-branch git commit -m "Implement login form"
이 접근 방식의 한계는 git rebase -i
와 비교했을 때 다음과 같습니다:
- 제어의 정밀도: 개별 커밋에 대한 제어가 부족합니다. 리베이스를 사용하면 병합할 커밋을 선택할 수 있지만, 병합은 모든 변경 사항을 하나의 커밋으로 결합하도록 강제합니다.
- 중간 히스토리: merge를 사용할 때, 기능 브랜치에서의 개별 커밋 히스토리가 메인 브랜치에서 소실됩니다. 이는 기능 개발 중에 이루어진 증분 변경 사항을 추적하기 어렵게 만들 수 있습니다.
- 커밋 전 리뷰: 모든 변경 사항을 단일 변경 세트로 스테이징하기 때문에, 각 커밋을 개별적으로 리뷰하거나 테스트할 수 없습니다. 대화식 리베이스에서는 각 커밋을 순차적으로 리뷰하고 테스트할 수 있지만, 스쿼싱 전에는 그렇지 않습니다.
결론
개발 워크플로에 자주하고 작은 커밋을 통합하는 것은 협업과 명확한 문서화를 촉진하지만, 프로젝트의 히스토리를 혼란스럽게 만들 수 있습니다. 커밋을 스쿼싱하는 것은 중요한 이정표를 보존하면서 작은 반복적인 변경 사항의 잡음을 제거하는 균형을 이룹니다.
Git에 대해 더 알아보려면 다음 자료를 추천합니다:
Source:
https://www.datacamp.com/tutorial/git-squash-commits