패스트 포워드
고친 사람 github-actions[bot]
패스트 포워드는 두 브랜치를 합칠 때 새 커밋을 만들지 않고 끝내는 방법입니다. 한쪽이 갈라진 뒤 아무것도 안 했다면 그쪽 브랜치를 다른 쪽 끝으로 옮기기만 합니다. 합칠 것이 없으니 충돌도 나지 않습니다. 이력도 한 줄로 남습니다.
쉽고 빠른 이해
패스트 포워드는 브랜치를 합칠 때 새 커밋 없이 브랜치가 가리키는 커밋만 바꾸고 끝내는 방법입니다. main 에서 갈라 낸
기능 브랜치에만 커밋이 쌓였다면, main 을 그 브랜치의 끝으로 옮기는 것으로 합치기가 끝납니다.
이 방법이 없으면 합칠 것이 없는 때에도 합쳤다는 표시용 커밋이 하나씩 생깁니다. 이력이 괜히 갈라졌다 붙는 모양이 됩니다.
main의 끝 커밋이 기능 브랜치가 지나온 커밋 중 하나인지 봅니다- 그렇다면
main을 기능 브랜치의 끝으로 옮깁니다 - 아니면 이 방법으로는 못 합칩니다. 합친 결과를 새 커밋에 담는 방법으로 넘어갑니다
대가가 있습니다. 기능 브랜치가 어디서 갈라져 어디서 합쳐졌는지가 이력에 남지 않습니다. 합친
뒤에는 처음부터 main 에서 작업한 것처럼 보입니다.
상세
생방송을 녹화하면서 보다가 잠시 멈춰 두었습니다. 그동안에도 녹화는 계속 앞으로 나갔습니다. 따라잡으려고 새로 찍을 것은 없습니다. 빨리감기로 보던 곳을 녹화 끝까지 옮기면 됩니다.
영어 fast-forward 는 빨리감기를 뜻합니다. 버전관리 도구에서도 같은 낱말을 씁니다.
Git 같은 도구에서 커밋은 어느 시점의 파일 전체를 한 벌로 찍어 둔 것입니다. 커밋마다 바로 전 커밋을 가리키는 값을 함께 담습니다. 그 전 커밋을 부모 커밋이라고 부릅니다.
브랜치는 커밋을 따로 쌓아 가는 갈래입니다. 브랜치 이름은 그 갈래의 끝 커밋을 가리키는 포인터입니다. 커밋을 하나 쌓으면 포인터도 새 끝 커밋으로 따라 옮겨 갑니다.
어떤 커밋에서 부모를 따라 거슬러 올라가다 닿는 커밋을 그 커밋의 조상이라고 합니다. 커밋 4 의 부모가 커밋 3 이고 커밋 3 의 부모가 커밋 2 라면, 커밋 3 과 커밋 2 가 모두 커밋 4 의 조상입니다.
두 브랜치를 하나로 합치는 일을 병합이라고 합니다. 합치기를 받는 쪽을 대상 브랜치, 그 안으로 들어가는 쪽을 병합할 브랜치라고 합니다.
대상 브랜치의 끝이 병합할 브랜치 끝의 조상이면, 대상 브랜치 포인터를 병합할 브랜치 끝으로 옮기는 것만으로 병합이 끝납니다. 이것이 패스트 포워드입니다. 새 커밋은 하나도 생기지 않습니다.
main 에서 login 브랜치를 갈라 내고 login 에만 커밋 둘을 쌓았다고 해 봅시다. main 의 끝은
갈라지던 때의 커밋에 머물러 있습니다. 이 커밋은 login 끝의 조상이므로 main 을 login 끝으로
옮기면 합치기가 끝납니다.
브랜치 포인터가 옮겨 가는 모양
아래 그림은 위 예를 옮기기 전과 옮긴 뒤로 나눠 그렸습니다. 실선 화살표는 커밋이 자기 부모를 가리키는 방향입니다. 점선은 브랜치 포인터가 가리키는 커밋입니다.
flowchart TD
subgraph before["옮기기 전"]
BM["main"] -.-> B2
BL["login"] -.-> B4
B4["커밋 4"] --> B3["커밋 3"]
B3 --> B2["커밋 2"]
B2 --> B1["커밋 1"]
end
subgraph after["옮긴 뒤"]
AM["main"] -.-> A4
AL["login"] -.-> A4
A4["커밋 4"] --> A3["커밋 3"]
A3 --> A2["커밋 2"]
A2 --> A1["커밋 1"]
end
before ~~~ after
커밋 넷과 부모를 가리키는 화살표는 옮기기 전과 뒤가 똑같습니다. 달라진 것은 main 포인터가
가리키는 커밋 하나뿐입니다. 커밋 2 에서 커밋 4 로 옮겨 갔습니다.
합칠 것이 없는 까닭
대상 브랜치에 새 커밋이 없다는 것은 그쪽이 갈라진 뒤 아무것도 고치지 않았다는 뜻입니다. 고친 것은 전부 병합할 브랜치에만 있습니다. 그러니 두 쪽을 합친 결과는 병합할 브랜치 끝의 파일과 똑같습니다.
두 쪽이 갈라지기 직전의 커밋을 공통 조상이라고 합니다. 위 예에서는 커밋 2 가 main 과
login 의 공통 조상입니다.
양쪽 모두에 새 커밋이 있으면 공통 조상과 두 끝, 셋을 줄마다 비교해 합쳐야 합니다. 이 방식이 세 갈래 병합입니다. 패스트 포워드에서는 이 비교를 할 필요가 없습니다. 합친 결과가 이미 커밋으로 있으니 그 커밋을 가리키기만 하면 됩니다.
같은 까닭으로 병합 충돌도 나지 않습니다. 충돌은 양쪽이 같은 줄을 서로 다르게 고쳤을 때 생깁니다. 한쪽이 아무것도 안 고쳤으면 그런 줄이 있을 수 없습니다.
작업 트리가 따라 바뀌는 모습
지금 편집하는 파일이 펼쳐진 폴더를 작업 트리라고 합니다. 작업 트리에는 지금 브랜치가 가리키는 커밋의 파일이 펼쳐져 있습니다.
새 커밋을 만들지 않는다는 말은 파일이 그대로라는 뜻이 아닙니다. 브랜치 포인터만 옮기고 끝내면 작업 트리에는 옛 커밋의 파일이 그대로 남습니다.
그래서 패스트 포워드는 포인터를 옮기면서 작업 트리의 파일도 새 끝 커밋의 내용으로 바꿉니다. 위 예라면 커밋 3 과 커밋 4 에서 고친 파일이 이때 폴더에 나타납니다.
패스트 포워드가 안 되는 때
갈라진 뒤 양쪽 모두에 새 커밋이 쌓였으면 어느 쪽 끝도 다른 쪽의 조상이 아닙니다. 이때 대상 브랜치 포인터를 병합할 브랜치 끝으로 옮기면 대상 브랜치에만 있던 커밋이 브랜치에서 떨어져 나갑니다. 그 커밋에서 고친 것이 합친 결과에서 빠지는 셈이라 패스트 포워드로는 합칠 수 없습니다.
이때는 합친 결과를 담을 새 커밋이 필요합니다. 부모를 둘 가리키는 이 커밋을 병합 커밋이라고 합니다. 병합 커밋 말고도 이력을 한 줄로 두는 방법이 있어서, 고를 수 있는 길은 셋입니다.
| 방법 | 하는 일 | 이력에 남는 모양 |
|---|---|---|
| 세 갈래 병합 | 양쪽을 합친 결과를 병합 커밋에 담습니다 | 갈라졌다 붙은 모양 |
| 리베이스 뒤 패스트 포워드 | 병합할 브랜치의 커밋을 대상 브랜치 끝 위에 이어 다시 쌓은 다음 포인터를 옮깁니다 | 한 줄 |
| 스쿼시 | 병합할 브랜치의 커밋을 하나로 뭉쳐 대상 브랜치 끝 위에 얹습니다 | 한 줄. 갈래 안의 커밋은 사라집니다 |
표의 둘째 줄인 리베이스는 대상 브랜치 끝을 병합할 브랜치의 조상으로 만들어 주는 작업입니다. 그 뒤에는 패스트 포워드가 됩니다. 그래서 이력을 한 줄로 두려는 저장소는 리베이스와 패스트 포워드를 짝으로 씁니다.
합칠 때 고르는 세 가지 방식
패스트 포워드가 될 수 있는 때에도 그것을 쓸지 말지는 고를 수 있습니다. Git 의 git merge 는
옵션 셋으로 이것을 가릅니다. 아래 표는 패스트 포워드가 될 때와 안 될 때 각 옵션이 어떻게
움직이는지 보입니다.
| 옵션 | 패스트 포워드가 될 때 | 안 될 때 |
|---|---|---|
--ff (기본값) |
포인터만 옮깁니다 | 병합 커밋을 만듭니다 |
--no-ff |
병합 커밋을 만듭니다 | 병합 커밋을 만듭니다 |
--ff-only |
포인터만 옮깁니다 | 합치지 않고 멈춥니다 |
--no-ff 는 이음매를 일부러 남깁니다. 기능 하나가 어디서 갈라져 어디서 합쳐졌는지가 이력에서
한 덩이로 보입니다. 그 기능을 전부 되돌려야 할 때 병합 커밋 하나만 되돌리기 하면 됩니다.
--ff-only 는 반대로 뜻밖의 병합 커밋을 막습니다. 대상 브랜치에 커밋이 있는 줄 모르고 합치려 들면
합치지 않고 멈춰서 알려 줍니다. 한 줄 이력을 지키려는 저장소가 이 방식을 기본으로 두는 일이
흔합니다.
원격 저장소에 올릴 때
패스트 포워드는 합치기에만 나오지 않습니다. 내 커밋을 원격 저장소로 올리는 푸시도 원격 브랜치 포인터를 내 끝으로 옮겨 달라는 요청입니다. 원격은 이 옮김이 패스트 포워드일 때만 받아들이는 것이 기본입니다.
원격 브랜치 끝이 내 끝의 조상이 아니면 푸시는 거절됩니다. 그사이 동료가 먼저 올린 커밋이 원격에 있다는 뜻입니다. 포인터를 옮기면 그 커밋이 브랜치에서 떨어져 나가므로 원격이 막습니다.
거절되면 원격의 새 커밋을 받아 와 내 쪽에 합친 뒤 다시 올립니다. 받아 오기만 하는 일은 페치, 받아 와서 합치기까지 하는 일은 풀입니다. 합친 뒤에는 원격 끝이 내 끝의 조상이 되므로 푸시가 받아들여집니다.
조상인지 보는 확인을 건너뛰고 포인터를 억지로 옮기게 시킬 수도 있습니다. 이것이 강제 푸시입니다. 원격에만 있던 커밋이 브랜치에서 빠지므로, 여럿이 함께 쓰는 브랜치에서는 강제 푸시를 막아 두기도 합니다.
이력에 남는 것과 사라지는 것
패스트 포워드로 합치면 이력이 곧은 한 줄로 남습니다. 커밋을 오래된 것부터 차례로 읽기 쉽습니다.
대신 갈래가 있었다는 흔적이 사라집니다. 합친 뒤의 이력만 보면 login 의 커밋이 처음부터
main 에서 쌓인 것처럼 보입니다. 기능 하나를 이루던 커밋이 어디서 시작해 어디서 끝나는지를
이력만으로는 가를 수 없습니다.
그래서 저장소마다 어느 쪽을 택할지 미리 정해 둡니다. 갈래를 드러내려면 --no-ff 를 씁니다. 한
줄을 지키려면 리베이스 뒤 --ff-only 를 씁니다. 이런 약속을 묶어 정하는 것이
브랜치 전략입니다.
관련 항목
패스트 포워드가 일어나는 작업
패스트 포워드가 안 될 때 쓰는 합치기 방법
세 갈래 병합 · 병합 커밋 · 리베이스 · 스쿼시 · 체리픽
패스트 포워드 여부를 가르는 커밋 구조
커밋 · 부모 커밋 · 공통 조상 · 브랜치 · HEAD · 커밋 그래프 · DAG · 작업 트리
패스트 포워드가 막힐 때 마주치는 문제
병합 충돌 · 강제 푸시 · 이력 강제 덮어쓰기 · 분리된 HEAD · 되돌리기
패스트 포워드를 강제하거나 막는 규칙
브랜치 전략 · 선형 이력 · 트렁크 기반 개발 · 기능 브랜치 · 브랜치 보호 규칙
패스트 포워드를 쓰는 도구
다른 이름: fast-forward · fast forward · fast-forward merge · 패스트포워드