← posts/b.log()

blog92@web:~$ cat posts/github-04-branch-merge-and-rebase.md

GIT3 min read

GitHub 완전 정복 4편 — 브랜치의 실체와 merge·rebase·충돌 해결

브랜치가 커밋 하나를 가리키는 파일일 뿐이라는 사실에서 출발해 -d와 -D의 안전 설계, fast-forward·3-way·squash 세 가지 병합, rebase가 히스토리를 다시 쓰는 원리와 황금률, 충돌 마커 해결과 rerere까지 다룹니다.

이전 편: 3편 — 원격 저장소 인증(SSH·PAT)과 push·fetch·pull의 차이

3.1 브랜치의 실체

브랜치는 커밋 하나를 가리키는 41바이트 텍스트 파일입니다. .git/refs/heads/main을 열어보면 커밋 해시 한 줄이 전부입니다.

그래서 브랜치 생성은 O(1)이고, 100개를 만들어도 용량이 안 늘고, 삭제해도 커밋 자체는 사라지지 않습니다(도달 경로만 사라짐).

text
        ┌── feature (포인터)
        ▼
A ─ B ─ C
    ▲
    └── main (포인터)
 
HEAD → 현재 체크아웃된 브랜치를 가리키는 포인터

3.2 브랜치 기본 명령

bash
git branch                       # 로컬 목록 (안전)
git branch -a                    # 원격 포함 전체
git branch -v                    # 마지막 커밋까지
git branch --merged              # 현재 브랜치에 병합 완료된 것 ★
git branch --no-merged           # 아직 병합 안 된 것 ★
 
git switch -c feature/login      # 생성 + 이동 (Git 2.23+, 권장)
git switch main                  # 이동
git switch -                     # 직전 브랜치로 토글
git checkout -b feature/login    # 구버전 방식 (동일 동작)
 
git branch -d feature/login      # 삭제 — 병합 안 됐으면 거부함 ★ 안전
git branch -D feature/login      # 강제 삭제 ⚠ 병합 여부 무시
git push origin --delete feature/login   # 원격 브랜치 삭제 ⚠

-d와 -D의 차이가 곧 안전 설계입니다. -d는 "이 브랜치의 작업이 어딘가에 병합되었는가?"를 검사하고, 아니면 거부합니다. 거부당했다는 것은 "아직 살릴 작업이 남아있다"는 Git의 경고입니다. 습관적으로 -D를 치면 이 안전장치가 통째로 무력화됩니다. -d가 거부하면 --no-merged로 확인부터 하세요.

bash
git branch --no-merged main      # main에 안 들어간 브랜치들
git log main..feature/login      # 이 브랜치에만 있는 커밋 확인

switch/restore가 checkout을 대체한 이유도 같은 맥락입니다. checkout은 "브랜치 이동", "파일 복원", "임의 커밋으로 이동" 세 가지 전혀 다른 일을 하나의 명령에 몰아넣어서, 오타 하나로 의도치 않은 파일 덮어쓰기가 일어났습니다. Git 2.23부터 switch(브랜치)와 restore(파일)로 책임을 분리했습니다.

3.3 브랜치 네이밍 컨벤션

text
main                    # 프로덕션
develop                 # 개발 통합
feature/login-oauth     # 기능
fix/header-overflow     # 버그
hotfix/payment-500      # 긴급 수정
release/1.4.0           # 릴리스 준비
chore/upgrade-deps      # 잡무
docs/api-guide          # 문서

이슈 번호를 붙이면 추적이 쉬워집니다: feature/42-login-oauth

3.4 merge — 세 가지 방식

(1) Fast-forward — 갈라진 게 없을 때

text
병합 전                         병합 후
main ──▶ A ─ B                  main ──────────▶ A ─ B ─ C ─ D
              └─ C ─ D ◀feature                          ▲feature

main이 그냥 앞으로 이동합니다. 새 커밋이 생기지 않고 히스토리가 일직선입니다.

(2) 3-way merge — 양쪽 다 진행됐을 때

text
A ─ B ─ E ─ F ──── M ◀main      M = 병합 커밋 (부모가 2개)
     └─ C ─ D ─────┘

(3) Squash merge — 여러 커밋을 하나로

text
A ─ B ─ E ─ F ─ S ◀main         S = C,D의 변경을 합친 새 커밋 1개
     └─ C ─ D  (연결되지 않음)
bash
git merge feature                # 기본 (가능하면 FF)
git merge --no-ff feature        # 항상 병합 커밋 생성 ★
git merge --squash feature       # 변경만 가져와 스테이징 (커밋은 수동)
git merge --abort                # 충돌 중 원상복구 ★ 안전

--no-ff를 권장하는 이유: fast-forward는 "이 기능이 언제 어디서 시작해서 언제 합쳐졌는지"라는 정보를 지웁니다. --no-ff로 병합 커밋을 남기면 그래프에서 기능 단위가 시각적으로 보이고, 문제가 생겼을 때 병합 커밋 하나만 revert하면 기능 전체를 되돌릴 수 있습니다.

3.5 rebase — 히스토리를 다시 쓰기

text
rebase 전                        git rebase main 후
A ─ B ─ E ─ F ◀main              A ─ B ─ E ─ F ◀main
     └─ C ─ D ◀feature                        └─ C' ─ D' ◀feature

feature의 커밋들을 떼어내서 main 끝에 새로 만들어 붙입니다. C와 C'는 내용은 같지만 해시가 다른 별개의 커밋입니다. 이것이 rebase가 "히스토리 변경"인 이유입니다.

bash
git switch feature
git rebase main               # feature를 main 위로 재배치
git rebase --continue         # 충돌 해결 후 진행
git rebase --skip             # 이 커밋 건너뛰기
git rebase --abort            # 전체 취소 ★ 안전
mergerebase
히스토리실제 발생 순서 보존일직선으로 정리
커밋 해시유지변경됨
병합 커밋생김안 생김
추적성높음낮음 (실제 시점 소실)
안전성높음공유 브랜치에선 위험

rebase 황금률 — 원격에 push한 브랜치는 rebase 하지 않는다.

왜? 동료가 이미 내 feature 브랜치를 받아갔다고 합시다. 내가 rebase하면 C는 C'가 되고, 둘은 Git 입장에서 완전히 다른 커밋입니다. 동료가 다음에 pull 하면 C와 C'가 둘 다 존재하는 엉망진창 히스토리가 되고, 심하면 동료의 후속 커밋이 고아가 됩니다.

예외는 "나만 쓰는 브랜치" 입니다. 내 PR 브랜치를 정리하려고 rebase하는 건 일반적인 관행입니다. 이때 push는 반드시 --force-with-lease를 씁니다(5편의 force-with-lease 참고).

3.6 충돌 해결

bash
git merge feature
# CONFLICT (content): Merge conflict in src/app.ts
 
git status        # 어떤 파일이 충돌했는지 (Unmerged paths)
ts
<<<<<<< HEAD
const timeout = 3000;     // 현재 브랜치(내 쪽)
=======
const timeout = 5000;     // 들어오는 브랜치(상대 쪽)
>>>>>>> feature
bash
# 1. 파일을 열어 <<<<<<<, =======, >>>>>>> 마커를 지우고 최종 코드로 정리
# 2. 해결 표시
git add src/app.ts
# 3. 완료
git commit            # merge인 경우
git rebase --continue # rebase인 경우

유용한 도구들:

bash
git diff --name-only --diff-filter=U   # 충돌 파일 목록만
git restore --ours   file.ts           # 내 쪽 통째로 채택
git restore --theirs file.ts           # 상대 쪽 통째로 채택
git mergetool                          # 시각적 병합 도구
git merge --abort                      # 포기하고 원래대로 ★

주의: rebase 중에는 --ours와 --theirs의 의미가 뒤집힙니다. rebase는 "내 커밋을 상대 위에 다시 올리는" 작업이라 기준(ours)이 재배치 대상 브랜치가 됩니다. 헷갈리면 수동으로 편집하세요.

반복되는 충돌이 지겹다면:

bash
git config --global rerere.enabled true
# rerere = REuse REcorded REsolution
# 같은 충돌을 다시 만나면 이전 해결 방식을 자동 적용
COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..