Git
고친 사람 github-actions[bot]
Git 은 파일이 바뀌어 온 이력을 남기고 필요하면 예전 모습으로 되돌려 주는 프로그램입니다. 각자 자기 컴퓨터에 설치해서 씁니다. 이력 전체가 내 컴퓨터 안에 있어서 인터넷이 없어도 쓸 수 있습니다. 여럿이 따로 고친 것을 나중에 하나로 합치는 일도 맡습니다.
쉽고 빠른 이해
Git 은 코드 폴더의 사진첩입니다. 고칠 때마다 폴더 전체의 모습을 한 장씩 찍어 둡니다. 잘못 고쳤으면 어제 찍은 장으로 돌아가면 됩니다.
이게 없으면 최종_진짜최종.zip 같은 복사본이 쌓입니다. 누가 무엇을 왜 바꿨는지도 남지 않습니다.
돌아가는 방식은 이렇습니다.
- 폴더에서 파일을 고칩니다
- 이번 장에 담을 파일을 골라 한 장으로 찍습니다
- 찍은 장들을 동료와 주고받고, 서로 다르게 고친 것은 하나로 합칩니다
대가가 있습니다. 둘이 같은 줄을 다르게 고치면 사람이 직접 골라야 합니다. 동영상처럼 큰 파일은 사진첩을 금방 무겁게 만듭니다.
상세
Git 은 버전관리를 하는 도구입니다. 버전관리는 파일이 언제 어떻게 바뀌었는지를 남겨 두는 일입니다. 이 절은 Git 이 이력을 담는 법, 가르는 법, 합치는 법을 명령과 그림으로 봅니다.
이력 전체를 각자 한 벌씩 가진다
버전관리 도구는 이력을 어디에 두느냐로 갈립니다. 이력을 서버 한 곳에만 두는 방식을 중앙집중형 버전관리라고 합니다. Subversion 이 그런 도구입니다. 이 방식에서는 서버에 닿아야 이력을 보거나 새 기록을 남길 수 있습니다.
Git 은 반대쪽인 분산 버전관리 도구입니다. 파일과 그 이력 전체를 담은 폴더를 저장소라고 부릅니다. Git 에서는 사람마다 이 저장소를 온전히 한 벌씩 가집니다.
flowchart TD
subgraph 중앙집중형
S1["서버 · 이력"] --> A1["사람 1 · 파일만"]
S1 --> A2["사람 2 · 파일만"]
S1 --> A3["사람 3 · 파일만"]
end
subgraph 분산형
S2["원격 저장소 · 이력"] --> B1["사람 1 · 이력 한 벌"]
S2 --> B2["사람 2 · 이력 한 벌"]
S2 --> B3["사람 3 · 이력 한 벌"]
end
중앙집중형 ~~~ 분산형
그래서 이력을 보는 일도, 새 기록을 남기는 일도 전부 내 컴퓨터 안에서 끝납니다. 네트워크를 타지 않으니 빠릅니다. 서버가 망가져도 누군가의 컴퓨터에 이력 한 벌이 남아 있습니다.
파일이 거쳐 가는 세 구역
Git 을 쓸 때 파일은 세 구역을 차례로 지납니다. 이 구역을 알아야 명령이 무엇을 옮기는지 읽힙니다.
첫째는 작업 디렉터리입니다. 내가 편집기로 열어 고치는 평범한 폴더입니다.
둘째는 인덱스입니다. 다음 기록에 담을 변경을 골라 올려 두는 대기 구역입니다. 스테이징 영역이라고도 합니다.
셋째는 저장소입니다. 인덱스에 골라 둔 변경이 기록 한 건으로 쌓이는 곳입니다. 이 기록 한 건을 Git 은 커밋이라고 부르며, 두 소절 뒤에서 풉니다.
인덱스가 따로 있는 까닭은 고친 것을 나눠 담기 위해서입니다. 버그 수정과 이름 바꾸기를 한꺼번에 고쳤어도 둘을 다른 기록으로 남길 수 있습니다. 나중에 한쪽만 되돌리기 쉬워집니다.
flowchart TD
W["작업 디렉터리 · 고치는 폴더"] -->|git add| I["인덱스 · 담을 변경을 고른다"]
I -->|git commit| L["내 저장소 · 기록이 쌓인다"]
L -->|git push| R["원격 저장소"]
R -->|git fetch| L
그림은 고친 파일 하나가 동료에게 닿기까지의 길입니다. 위 세 칸은 내 컴퓨터 안에 있습니다. 맨 아래 원격 저장소만 다른 컴퓨터에 있습니다. 아래에서 위로 가는 화살표는 반대 방향으로, 동료가 올린 기록을 내 저장소로 받아 오는 길입니다.
커밋은 폴더 전체의 스냅샷이다
인덱스에 올린 것을 저장소에 한 번 기록하는 일이 커밋입니다. 커밋 하나에는 그 시점 파일들의 모습, 작성자, 시각, 설명 메시지가 담깁니다. 바로 앞 커밋(부모)의 이름도 함께 담깁니다.
Git 은 커밋마다 바뀐 줄만 적지 않습니다. 그 시점 폴더 전체의 모습을 담습니다. 이것을 스냅샷이라고 합니다.
스냅샷이라고 해서 파일을 매번 새로 저장하지는 않습니다. 바뀌지 않은 파일은 앞 커밋의 것을 가리키기만 합니다. 그래서 스냅샷이어도 저장소가 크게 불어나지 않습니다. 그 모양은 다음 소절 그림에 있습니다.
커밋마다 부모의 이름이 들어 있으니, 부모를 거슬러 올라가면 처음 커밋까지 이력이 이어집니다.
git add login.py # 인덱스에 올린다
git commit -m "수정" # 커밋을 남긴다
git log --oneline # a1b2c3d 수정
마지막 줄의 a1b2c3d 는 이 커밋의 이름입니다. 사람이 붙인 번호가 아니라 내용에서 계산한
값입니다. 다음 소절에서 그 값이 어디서 오는지 봅니다.
내용에서 이름을 계산한다
Git 은 저장하는 모든 것을 객체로 담습니다. 객체는 이름이 붙은 데이터 한 덩어리입니다. 저장소 안의 파일 내용도, 폴더 구조도, 커밋도 전부 객체입니다.
객체는 세 종류가 기본입니다. 파일 한 개의 내용을 담는 것이 블롭입니다. 폴더 하나에 어떤 파일과 하위 폴더가 들었는지 담는 것이 트리입니다. 나머지 하나가 앞에서 본 커밋입니다.
객체의 이름은 내용을 해시 함수에 넣어 나온 값입니다. 해시 함수는 어떤 입력이든 짧은 고정 길이 값으로 바꾸는 함수입니다. 입력이 한 글자만 달라도 값이 크게 달라집니다.
Git 은 기본으로 SHA-1(Secure Hash Algorithm 1) 해시 함수를 써서 40자리 16진수 이름을
만듭니다. 앞의 a1b2c3d 는 그 이름의 앞 일곱 자리만 줄여 보인 것입니다.
flowchart TD
C2["커밋 2"] -.->|부모| C1["커밋 1"]
C1 --> T1["트리 · 최상위 폴더 (커밋 1)"]
C2 --> T2["트리 · 최상위 폴더 (커밋 2)"]
T1 --> B1["블롭 · login.py 고치기 전"]
T2 --> B2["블롭 · login.py 고친 뒤"]
T1 --> D["트리 · docs 폴더"]
T2 --> D
D --> R["블롭 · README.md"]
커밋은 최상위 트리를 가리킵니다. 트리는 블롭과 아래 트리를 가리킵니다. 그림에서 커밋 2 는
login.py 만 고쳤습니다. 그래서 login.py 블롭만 새로 생기고, 안 바뀐 docs 트리는 두 커밋이
함께 가리킵니다.
내용이 이름이 되므로 같은 내용은 이름도 같습니다. 두 커밋이 같은 파일을 가졌으면 객체 하나를 함께 가리키고 끝나는 까닭이 이것입니다.
이 구조는 위조도 드러냅니다. 옛 파일 한 글자를 몰래 바꾸면 그 블롭의 이름이 바뀝니다. 블롭의 이름은 트리의 내용이고, 트리의 이름은 커밋의 내용이라 바뀐 이름이 위로 번집니다.
flowchart TD
X1["블롭 내용 한 글자 바뀜 → 블롭 이름 바뀜"] --> X2["그 블롭을 담은 트리 이름 바뀜"]
X2 --> X3["그 트리를 담은 커밋 이름 바뀜"]
X3 --> X4["그 커밋을 부모로 담은 뒤 커밋 전부 이름 바뀜"]
해시가 해시를 가리켜 아래쪽 변경이 꼭대기 이름까지 바꾸는 이 모양이 머클 트리입니다.
브랜치는 커밋을 가리키는 이름표다
브랜치는 이력을 갈래로 나눠 두는 것입니다. 새 기능을 만드는 동안 기준이 되는 브랜치를 건드리지 않으려고 씁니다.
Git 에서 브랜치는 커밋 하나를 가리키는 이름표일 뿐입니다. 파일을 복사하지 않습니다. 그래서 브랜치를 만들고 지우는 일이 거의 공짜입니다. 기능 하나, 실험 하나마다 브랜치를 내는 습관이 여기서 나옵니다.
지금 내가 어느 브랜치 위에 있는지는 HEAD 라는 특별한 이름표가 알려 줍니다. HEAD 는 커밋이 아니라 브랜치 이름표를 가리킵니다. 커밋을 하면 HEAD 가 가리키는 브랜치의 이름표가 새 커밋으로 한 칸 옮겨 갑니다.
git switch -c login # 새 브랜치로 옮긴다
git branch # * login
flowchart TD
subgraph 커밋하기전["커밋하기 전"]
a4["커밋 4"] --> a2["커밋 2"]
a3["커밋 3"] --> a2
a2 --> a1["커밋 1"]
aM["main"] -.-> a3
aH["HEAD"] -.-> aL["login"]
aL -.-> a4
end
subgraph 커밋한뒤["login 에서 커밋한 뒤"]
b5["커밋 5"] --> b4["커밋 4"]
b4 --> b2["커밋 2"]
b3["커밋 3"] --> b2
b2 --> b1["커밋 1"]
bM["main"] -.-> b3
bH["HEAD"] -.-> bL["login"]
bL -.-> b5
end
커밋하기전 ~~~ 커밋한뒤
실선 화살표는 부모 쪽을 향하고, 점선은 이름표가 가리키는 곳입니다. 커밋 2 에서 이력이 갈라져
main 은 커밋 3 을, login 은 커밋 4 를 가리킵니다. 커밋 5 를 남기면 login 이름표만 커밋 5 로
옮겨 가고 main 은 그대로 있습니다.
갈라진 브랜치를 합친다
갈라 둔 브랜치는 결국 기준 브랜치로 돌아와야 합니다. 두 브랜치를 하나로 잇는 일이 병합입니다.
병합하려면 먼저 둘이 어디서 갈라졌는지 알아야 합니다. 두 브랜치가 갈라지기 직전의 커밋을 공통 조상이라고 합니다. 앞 그림에서는 커밋 2 입니다.
Git 은 두 브랜치의 끝 커밋과 공통 조상, 이렇게 세 스냅샷을 줄마다 견줍니다. 이 방식이
3방향 병합입니다. 예를 들어 공통 조상에서 retry = 3 이던 줄을 login 만 retry = 5 로
고쳤고 main 은 그대로 뒀다면, 바뀐 쪽인 retry = 5 를 가져옵니다.
결과는 부모를 둘 가진 병합 커밋으로 남습니다. 이력을 보면 언제 두 브랜치가 만났는지 알 수 있습니다.
git switch main # 기준 브랜치로 간다
git merge login # login 을 합친다
flowchart TD
m6["병합 커밋 6"] --> m3["커밋 3 · main 의 끝"]
m6 --> m5["커밋 5 · login 의 끝"]
m5 --> m4["커밋 4"]
m3 --> m2["커밋 2 · 공통 조상"]
m4 --> m2
m2 --> m1["커밋 1"]
mM["main"] -.-> m6
main 에서 병합했으므로 새 병합 커밋 6 은 main 이름표가 가리킵니다.
같은 줄을 둘이 고치면 사람이 고른다
3방향 병합은 줄마다 공통 조상과 견줘 세 갈래로 나뉩니다. 사람이 나서야 하는 것은 마지막 갈래뿐입니다.
flowchart TD
S["줄마다 공통 조상과 견준다"] --> Q{"어느 쪽이 바뀌었나"}
Q -->|양쪽 다 그대로| K["그대로 둔다"]
Q -->|한쪽만 바뀜| O["바뀐 쪽을 가져온다"]
Q -->|양쪽이 다르게 바뀜| X["충돌 · 병합이 멈춘다"]
X --> H["사람이 고르고 표시 줄을 지운다"]
H --> F["git add · git commit"]
두 브랜치가 같은 파일의 같은 줄을 다르게 고쳤으면 Git 은 어느 쪽이 맞는지 모릅니다. 이때 병합이 멈춥니다. 이 상태를 충돌이라고 합니다. 충돌을 사람이 풀어 최종본을 정하는 일이 충돌 해소입니다.
Git 은 충돌한 줄에 양쪽 내용을 표시로 감싸 파일에 남깁니다.
<<<<<<< HEAD
timeout = 30
=======
timeout = 60
>>>>>>> login
위쪽은 지금 있는 브랜치의 내용입니다. 아래쪽은 합쳐 오려는 login 브랜치의 내용입니다. 사람이
둘 중 하나를 남기거나 새로 고쳐 쓰고 표시 줄을 지웁니다. 그다음 인덱스에 올리고 커밋하면
병합이 끝납니다.
커밋을 옮겨 붙이는 리베이스
합치는 방법이 하나 더 있습니다. 리베이스는 내 브랜치의 커밋들을 떼어 기준 브랜치의 끝에 다시 붙입니다. 병합 커밋이 생기지 않아 이력이 한 줄로 곧게 남습니다.
flowchart TD
subgraph 리베이스전["리베이스 전"]
r5["커밋 5"] --> r4["커밋 4"]
r4 --> r2["커밋 2"]
r3["커밋 3"] --> r2
r2 --> r1["커밋 1"]
rM["main"] -.-> r3
rL["login"] -.-> r5
end
subgraph 리베이스후["리베이스 후"]
n5["커밋 5' · 새 이름"] --> n4["커밋 4' · 새 이름"]
n4 --> n3["커밋 3"]
n3 --> n2["커밋 2"]
n2 --> n1["커밋 1"]
nM["main"] -.-> n3
nL["login"] -.-> n5
o5["옛 커밋 4 · 5 · 동료 컴퓨터엔 남아 있음"] -.-> n2
end
리베이스전 ~~~ 리베이스후
대가는 커밋이 새로 만들어진다는 것입니다. 부모의 이름도 커밋에 담기는 내용이라, 부모가 바뀌면 커밋의 이름도 바뀝니다. 그림에서 커밋 4 · 5 가 4' · 5' 로 바뀐 까닭입니다.
이미 남과 나눈 커밋을 리베이스하면 동료는 여전히 옛 커밋 4 · 5 를 가지고 있습니다. 내 이력과 동료의 이력이 같은 작업을 서로 다른 이름으로 들고 있게 되어 어긋납니다. 그래서 흔히 내 컴퓨터에만 있는 커밋에만 씁니다.
다른 저장소와 주고받는다
내 컴퓨터 밖에 있는 저장소를 원격 저장소라고 합니다. 팀은 보통 원격 저장소 하나를 기준으로 정해 두고, 각자 거기서 받고 거기로 올립니다. GitHub 나 GitLab 같은 서비스가 이 원격 저장소를 맡아 줍니다.
주고받는 명령은 넷입니다. 표에 무엇이 어디서 어디로 가는지 모았습니다.
| 명령 | 하는 일 |
|---|---|
클론 git clone |
원격 저장소를 이력째 처음 한 벌 받아 온다 |
페치 git fetch |
원격의 새 커밋을 받아만 두고 내 브랜치는 그대로 둔다 |
풀 git pull |
페치한 뒤 내 브랜치에 바로 병합한다 |
푸시 git push |
내 새 커밋을 원격 저장소로 올린다 |
페치와 풀은 합치는 시점이 갈립니다. 페치는 받아서 보기만 하니 안전합니다. 풀은 받자마자 합치므로 충돌이 여기서 날 수 있습니다.
잘 다루지 못하는 것
Git 은 텍스트 파일의 줄 단위 변경에 맞춰 만들어졌습니다. 동영상이나 디자인 원본 같은 이진 파일은 줄 단위로 견줄 수 없어 병합도 안 됩니다.
이런 파일은 크기 자체도 큽니다. 커밋마다 바뀐 파일은 새 블롭으로 저장되니, 큰 파일을 고칠 때마다 큰 덩어리가 한 벌씩 더 쌓입니다. 텍스트 파일도 새 블롭이 생기지만 크기가 작아 부담이 덜합니다. 큰 파일은 본체를 따로 두고 가리킴만 저장하는 Git LFS(Large File Storage) 같은 확장을 붙여 씁니다.
Git 에는 파일 잠금이 없습니다. 각자 고치고 나중에 합치는 것이 전제라서, 누가 먼저 고치는 중인지 막아 주지 않습니다. 합칠 수 없는 이진 파일을 여럿이 고치면 한쪽 작업을 버려야 합니다.
이력은 기본으로 전부 복사됩니다. 오래된 저장소는 클론만으로 오래 걸립니다. 한 번 커밋한 비밀번호도 이력에 남습니다. 최신 파일에서 지워도 옛 커밋에서 꺼낼 수 있습니다.
관련 항목
Git 이 속하는 버전관리 방식
버전관리 · 분산 버전관리 · 중앙집중형 버전관리 · 소스 코드 관리
Git 과 같은 일을 하는 버전관리 도구
Subversion · Mercurial · Perforce · CVS · RCS
Git 저장소를 이루는 객체와 구조
저장소 · 객체 · 블롭 · 트리 · 커밋 객체 · 태그 객체 · 머클 트리 · 해시 함수 · SHA-1 · SHA-256
Git 에서 파일이 거치는 구역
작업 디렉터리 · 인덱스 · 스테이징 영역 · 원격 저장소 · .gitignore
Git 이력을 가리키는 이름표
커밋 · 브랜치 · 태그 · HEAD · ref · 분리된 HEAD
Git 에서 브랜치를 합치고 고치는 동작
병합 · 3방향 병합 · 공통 조상 · 병합 커밋 · 빨리 감기 병합 · 충돌 해소 · 리베이스 · 체리픽 · 되돌리기 · 스태시
Git 이 다른 저장소와 주고받는 명령
Git 저장소를 맡아 주는 호스팅 서비스
GitHub · GitLab · Bitbucket · Gitea
Git 위에서 굴러가는 협업 방식
풀 리퀘스트 · 코드 리뷰 · 브랜치 전략 · 커밋 메시지 · 커밋에서 배포까지
Git 이 약한 대목을 메우는 확장
Git LFS · 서브모듈 · 얕은 클론 · 희소 체크아웃
다른 이름: 깃 · git