Git 브랜치 : 직관과 현실
(jvns.ca)- 많은 사람들이 Git branch의 작동 방식을 직관적이지 않다고 생각함
- Git 브랜치에 대한 일반적인 직관적 모델과 실제 Git 내부에서 브랜치가 어떻게 표현되는지에 대한 차이점을 설명함
- 직관적 모델과 실제 Git의 작동 방식이 실제로 매우 밀접하게 관련되어 있음을 보여줌
- 직관적 모델의 한계와 문제를 일으킬 수 있는 이유를 논의함
직관적인 브랜치 모델
- 많은 사람들이 브랜치를 '사과나무의 가지'에 비유하여 생각함.
- Git에서 브랜치는 '부모' 개념이 없으며, 'main'에서 분기된 것으로 생각하는 것과 다름.
Git에서 브랜치는 전체 히스토리
- Git에서 브랜치는 단순히 분기된 커밋이 아니라 모든 이전 커밋의 전체 히스토리를 포함함.
- 예제 저장소를 통해
main과mybranch모두 4개의 커밋을 가지고 있음을 보여줌.
브랜치는 커밋 ID로 저장됨
- Git 내부에서 브랜치는 커밋 ID를 포함한 작은 텍스트 파일로 저장됨.
- 각 브랜치의 최신 커밋이 해당 파일에 기록되어 있음.
- 커밋 간의 부모-자식 관계가 없기 때문에, 브랜치 간의 관계를 Git은 알지 못함.
사람들의 직관은 보통 그렇게 틀리지 않음
- Git에 대한 사람들의 직관이 '틀렸다'고 말하는 것은 다소 어리석음.
- '틀린' 모델도 실제로 유용할 수 있음.
리베이스는 '직관적' 브랜치 개념을 사용함
- 리베이스는 '직관적' 브랜치의 커밋만을
main에 재적용함. - 리베이스 결과는 직관적 모델과 일치함.
머지도 '직관적' 브랜치 개념을 사용함
- 머지는 커밋을 복사하지 않지만, 공유된 기반 커밋을 필요로 함.
- 머지 베이스는 직관적 모델에 기반한 브랜치가 분기된 커밋을 찾아줌.
GitHub 풀 리퀘스트도 직관적 아이디어를 사용함
- GitHub에서
mybranch를main에 머지하려는 풀 리퀘스트를 생성하면, 직관적 브랜치의 커밋만 보여줌.
직관은 좋지만 한계가 있음
- 직관적 브랜치 정의는 실제 Git 작업과 잘 맞지만, Git은
main과 분기된 브랜치를 다르게 인식하지 못함.
트렁크와 분기된 브랜치
- 사람들은
main과mybranch를 다르게 인식하며, 이는 Git 사용 방식에 영향을 줌. - Git은 브랜치가 다른 브랜치의 '분기'인지 구분하지 않음.
Git은 리베이스를 '역방향'으로 할 수 있음
- Git은 브랜치가 다른 브랜치의 '분기'인지 알려주지 않으므로, 언제 어떤 브랜치를 리베이스해야 하는지 사용자가 알아야 함.
git rebase main과 역방향 리베이스인git rebase mybranch가 모두 가능. 머지도 마찬가지
Git 브랜치 간의 계층 구조 부재는 다소 이상함
main브랜치가 특별하지 않다는 말은 Git이 브랜치 간의 관계를 인식하지 못하기 때문에 나온 것임.- 브랜치 사이에는 관계가 있지만, git은 아무것도 모름
Git 브랜치 UI도 이상함
- '분기된' 커밋만 보고 싶을 때
git log와git diff를 사용하는 방법이 다름.
GitHub에서 기본 브랜치는 특별함
- GitHub는 '기본 브랜치'를 가지며, 이는 특별한 역할을 함.
GN⁺의 의견
이 글에서 가장 중요한 것은 Git 브랜치에 대한 사람들의 직관적 이해와 실제 Git의 작동 방식 간의 차이를 이해하는 것임. 이 글은 초급 소프트웨어 엔지니어들이 Git 브랜치의 개념을 더 잘 이해하고 효과적으로 사용할 수 있도록 도와줄 것임. Git 브랜치의 직관적 모델이 실제 작업과 어떻게 일치하는지, 그리고 Git이 브랜치 간의 관계를 어떻게 처리하지 않는지를 알아보는 것은 흥미롭고 유익함.
Hacker News 의견
- 브랜치는 커밋을 가리키는 포인터이며, 새로운 커밋이 생성될 때마다 이 포인터가 갱신됨. 브랜치는 태그처럼 떠돌아다니는 이름이라고 볼 수 있음. 커밋 자체가 부모 커밋을 가리키기 때문에 브랜치라는 것은 관련 커밋들의 연쇄로, 명명된 진입점을 가짐. 브랜치를 삭제하면 더 이상 명명된 레이블이 없어져서, 단순히 관련 커밋들의 연쇄로만 남게 됨.
- 커밋의 계보를 '앞으로'가 아닌 '뒤로' 가리키는 포인터로 생각하면 이해하기 쉬움. 브랜치는 커밋 ID이므로 부모 링크를 거슬러 올라가면 해당 브랜치의 전체 역사를 찾을 수 있음. '브랜치 포인트'는 두 커밋 체인이 만나는 지점이며, 병합 커밋은 특별한데, 이는 두 역사가 하나로 합쳐졌음을 나타냄.
- 개인적인 프로젝트에서
git reset --hard와git stash를 사용하여 변경 사항과 브랜치 포인터를 조작하는 것을 친구들이 보면 화를 내곤 함. 잘못된 병합을 취소하려면git reset --hard <병합 전 마지막 커밋>을 사용하고, 로컬 브랜치의 작은 수정 사항을 메인 브랜치에 적용하려면git stash를 사용한 후 메인 브랜치로 체크아웃하여git stash apply를 사용함. - Git에는 'main이 특별하다'는 개념이 없지만, Gitlab과 같은 도구들은 보호된 브랜치 기능을 제공하여 실수를 줄일 수 있음. '부모'와 '자식' 브랜치 개념이 실제로 흥미로울 수 있으며, 장기 지원 브랜치를 위해 여러 '부모' 브랜치를 지원해야 함.
- 병합, 리베이스, 풀 리퀘스트를 할 때 다른 브랜치를 명시적으로 지정해야 함. Git은 사용자가 기반으로 생각하는 브랜치를 알지 못하기 때문임. 때로는 기능 브랜치를 다른 기능 브랜치에 병합하고 싶을 수 있으므로, 어떤 브랜치를 다른 브랜치에 병합할지 명확히 지정해야 함.
- 사람들이 가지고 있는 직관이 기술적으로 부분적으로 틀릴 수 있어도, 그들이 그런 직관을 가진 데는 타당한 이유가 있음.
git add와git commit사용법을 아는 사람들을 대상으로 한 동적인 튜토리얼이 있음. 이 튜토리얼은 브랜치를 시각화하며 읽을 수 있도록 도와줌.- Git 명령어를 수행할 때 '항상' 현재 브랜치를 수정한다는 것을 기억하면 Git의 문법을 '쉽게' 이해할 수 있음. 예를 들어,
git merge my-branch는 현재 브랜치에 my-branch를 병합하고,git rebase my-branch는 현재 브랜치를 my-branch 위에 리베이스함. - 브랜치(헤드)가 해당 브랜치가 시작된 기저 커밋을 가리키는 '꼬리'를 가지는 것이 좋을 것 같음. 브랜치가 자주 리베이스되기 때문에 어디서 시작하는지 생각해야 할 때가 있음. Git이 기저 커밋이
main에 속한다고 알려주면 더 편리함. - 메일링 리스트에 '패치'를 보낼 때 기저 커밋을 선택적으로 포함할 수 있음. 이는 변경 사항이 최신 릴리스, 메인 개발 브랜치, 또는 통합 브랜치 중 어디에 기반하는지 명확하지 않을 수 있기 때문임.
git range-diff를 사용할 때도 기저를 염두에 두어야 함. 이 도구는main..previous와main..current와 같은 두 범위를 비교함. - 브랜치에 대한 개인적인 견해를 다시 읽고 잊었던 몇 가지를 다시 배움.