index

git commit

git commit 명령은 스테이징 영역에 추가된 변경 사항(파일 추가, 수정, 삭제 등)을 새 커밋으로 만들고 리포지토리에 저장한다

각 커밋은 고유한 SHA-1 해시로 식별되며 스냅샷과 작성자(이메일), 날짜, 메시지 등의 메타데이터를 포함한다

git object

how git commit works

먼저 스테이징 영역에 추가된 변경 사항을 확인한다

이를 기반으로 스테이징된 각 파일의 스냅샷을 Blob으로 저장하고 파일 구조를 Tree 객체로 구성한다

그 다음 부모 커밋, Tree, 메타데이터를 포함한 Commit 객체를 생성한다

현재 브랜치의 HEAD를 새 커밋으로 이동시키고 .git/refs/heads에 브랜치 포인터를 갱신하면 커밋 명령이 완료된다

git commit commands

git commit -m <message>: 스테이징된 변경을 메시지와 함께 커밋한다

git commit: 기본 편집기(vim, nano 등)로 커밋 메시지를 상세히 작성하고 커밋한다

git commit -a: 추적하고 있는 파일의 변경 사항을 자동 스테이징 후 커밋한다

git commit --amend: 직전 커밋을 새 커밋으로 덮어쓴다 (원격 리포지토리와 충돌 발생 가능성 있음)

git commit --amend --no-edit: 메시지 변경 없이 직전 커밋의 메시지를 그대로 사용하면서 새 커밋으로 덮어쓴다

git commit --allow-empty: 변경 사항없이 커밋한다 (기본적으로 빈 커밋을 허용하지 않음)

git commit --dry-run: 커밋 시뮬레이션

git merge

git merge 명령은 하나 이상의 브랜치 또는 커밋을 현재 브랜치에 병합하여 분기된 히스토리를 통합한다

병합 시 필요하면 새로운 병합 커밋(merge commit)을 생성할 수 있다 (병합 커밋으로 인해 커밋 히스토리가 복잡해질 수 있음)

how git merge works

구성 요소

git은 두 브랜치의 분기점(공통 조상)을 찾은 다음, 그로부터 각 브랜치의 변경 사항을 분석한다

fast forward merge

이미지 출처

이 때 소스 브랜치가 현재 브랜치보다 단순히 앞서기만 하면 현재 브랜치의 포인터를 이동시키기만 한다 (fast-forward merge)

3 way merge

그게 아니라 각 브랜치가 공통 조상에서 분기한 뒤 각자의 커밋 히스토리를 보유하고 있으면 병합 커밋을 생성한다 f(non-fast-forward, 3-way merge)

만약 두 브랜치에서 동일한 파일을 수정한 경우 충돌(conflict)이 발생하며 개발자가 직접 수동으로 충돌을 해결해야 한다

모든 충돌을 해결하고 정상적으로 병합할 수 있으면 병합 커밋을 생성하고 현재 브랜치의 포인터를 업데이트한다

git merge commands

git merge <branch>: 지정한 브랜치를 현재 브랜치에 병합한다

git merge --no-ff <branch>: fast-forward를 할 수 있어도 병합 커밋 생성

git merge --ff-only <branch>: fast-forward만 허용하며, 실패하면 병합을 중단한다

git merge --no-commit: 병합 후 자동 커밋을 하지 않는다

git merge --abort: 충돌 발생 시 병합을 취소하고 원래 상태로 돌아간다

git merge --squash: 병합 후 커밋을 하나로 압축한다 (병합 커밋 생성 X)

git rebase

git rebase 명령은 현재 브랜치의 커밋 히스토리를 재구성한다

주요 작업

git rebase <base-branch>: 현재 브랜치를 기반 브랜치 뒤로 재적용한다

git rebaes --continue: 리베이스 충돌 해결 후 남은 작업들을 진행한다

git rebase --skip: 충돌 커밋을 건너뛰고 리베이스한다

git rebase --abort: 리베이스를 취소하고 원래 상태로 돌아간다

git rebase --onto <newbase> <old-branch> <target-branch>: old-branch를 조상으로 하는 target-branch를 newbase 브랜치로 리베이스한다

git rebase --root: 리포지토리 처음부터 리베이스한다

how git rebase works

구성 요소

rebase

git은 두 브랜치의 공통 조상을 찾고 그로부터 발생한 변경 사항들을 파악한다

현재 브랜치의 모든 커밋을 복사하고 HEAD를 기반 브랜치의 최신 커밋으로 이동하여 복사한 커밋들을 순차적으로 적용한다

충돌이 발생한 경우 각 커밋마다 개발자가 수동으로 해결하고, 정상적으로 적용이 되면 현재 브랜치와 기반 브랜치의 포인터를 새 커밋으로 업데이트한다

git rebase side effect

리베이스한 브랜치의 원래 커밋은 모두 삭제되고 기반 브랜치에 적용된 커밋들은 새 SHA-1 해시로 대체된다

즉, 리베이스 이후 변경 사항은 동일하지만 “현재 브랜치”의 커밋들이 기반 브랜치에 재적용되면서 이전의 커밋과 서로 다른 SHA-1 해시를 가지게 된다

이 상태에서 리베이스하고 원격 리포지토리에 강제로 푸시하면 원격 브랜치의 기존 커밋들을 리베이스한 커밋들로 덮어쓴다

git은 SHA-1 해시를 기준으로 커밋 히스토리를 비교하는데, 기존 커밋 히스토리를 가지고 있던 다른 팀원이 원격 리포지토리에서 덮어쓴 커밋들을 다운받으면 해당 팀원의 로컬 리포지토리에서 충돌을 일으킬 수 있으므로 이 부분을 유의하며 작업을 진행해야 한다

git rebase --root command

git rebase --root 명령은 보통 -i 옵션과 함게 사용되며 리포지토리의 가장 처음 커밋부터 HEAD까지의 모든 커밋을 대상으로 히스토리를 재구성할 수 있게 해준다

git rebase <base-branch>는 특정 커밋 이후만 다루지만 --root 옵션은 전체 히스토리를 포함한다

구성 요소

git rebase -i

git rebase -i 명령은 대화형 리베이스로 대화형(interactive) 모드를 이용하여 현재 브랜치(HEAD)의 커밋 히스토리를 수정하거나 재구성한다

git rebase 명령이 단순히 브랜치의 커밋들을 새 기반 브랜치 뒤로 복사하는 것에 비해 -i 옵션은 개발자가 커밋을 편집, 압축, 삭제, 순서/이름 변경 등의 세부적인 조정을 할 수 있게 해준다

git rebase -i components

how git rebase -i works

대화형 모드로 작업된 커밋들은 새 SHA-1 해시로 대체된다

충돌이 발생하면 개발자가 수동으로 각 커밋 충돌을 해결해야 한다

백업

git rebase -i 명령은 대상 범위의 커밋 히스토리를 전부 재생성한다

리베이스 작업을 할 브랜치에 백업 브랜치를 만들면 기존 커밋 히스토리가 삭제되지 않고 보관할 수 있다

# 작업 전 커밋 백업
$ git branch rebase-backup
# 백업 브랜치는 원래 커밋을 보관하고 있다 
$ git rebase -i HEAD~5
$ git log --oneline --graph --all

* a79f4cf (HEAD -> main) add foo file
* ca53fb7 add test.py
| * 49d0141 (rebase-backup) add test.py
| * 283d2f6 add foo file
|/
* c58962b empty commit

대상 커밋 범위 지정

주로 HEAD를 기준으로 하며, 특정 브랜치 또는 커밋을 지정하기도 한다

# HEAD를 지정하면 아무 커밋도 선택되지 않는다
$ git rebase HEAD

# 가장 최근 커밋 편집 (HEAD)
$ git rebase HEAD^

# 최근 3개 커밋 편집 (HEAD 포함 이전 커밋 3개)
$ git rebase -i HEAD~3

# 특정 커밋 지정 (지정한 커밋 이후 커밋부터 HEAD까지)
$ git rebase -i b8876f1

커밋 히스토리 조작

# 최근 5개 커밋 편집 (HEAD 포함 이전 커밋 5개)
$ git rebase -i HEAD~5

pick 283d2f6 add foo file
pick e4e8e1b add kfc txt
pick 744b9c4 add test2.py
pick f68fb53 squash merge
pick 9328479 add any file

시간 순대로 정렬되어 가장 오래된 커밋이 맨 위에 위치하고, HEAD가 가리키는 가장 커밋이 맨 아래에 놓인다

주요 조작 명령어

squash

--squashgit merge 또는 git rebase -i 명령에서 사용할 수 있는 옵션으로 여러 커밋을 하나로 압축해서 병합한다

스쿼시는 주로 불필요한 중간 커밋을 정리하거나 작업 단위를 명확히 하기 위한 커밋 히스토리 단순화 작업에서 사용된다

how git merge –squash works

squash

이미지 출처

구성 요소

git merge --squash 명령의 경우 분기된 브랜치에서 다른 브랜치의 변경 사항을 반영할 때 사용한다

공통 조상으로부터 분기되어 독립적으로 커밋 히스토리를 유지하고 있는 소스 브랜치의 개별 커밋 히스토리는 무시하고, 변경 사항만 현재 브랜치의 스테이징 영역에 추가한다 (커밋을 하지 않음 --no-commit)

그 다음 커밋을 하면 분기된 시점으로부터 소스 브랜치의 모든 변경 사항을 포함한 새로운 커밋을 생성하고 현재 브랜치의 포인터를 업데이트한다

이 명령은 병합 커밋을 생성하지 않으며 마치 소스 브랜치의 가장 마지막 커밋의 상태를 현재 브랜치에 복사한 것과 같다

how git rebase -i squash works

git rebase -i 대화형 리베이스를 시작하면 현재 브랜치의 커밋 히스토리를 정리하기 위해 스쿼시 병합을 사용할 수 있다

# 최근 3개 커밋 대상
$ git rebase -i HEAD~3

pick a1b2c3d add test.py
pick b4c5d6e fix typo
pick c7d8e9f add main.py
$

스쿼시할 커밋은 s또는 squash 명령으로 지정한다

지정된 커밋은 직전 커밋에 통합된다

```shell
$ git rebase -i HEAD~3

pick a1b2c3d add test.py
s b4c5d6e fix typo         # a1b2c3d 커밋에 통합
s c7d8e9f add main.py     # b4c5d6e 커밋에 통합

이후에 메시지를 편집하면 지정한 커밋들이 모두 통합되며 히스토리가 갱신된다

git revert

git revert 명령은 실수로 반영된 커밋을 삭제하거나 과거로 되돌리는 대신(git reset) 해당 변경을 취소하는 방식으로 동작한다

과거 기록을 유지하면서 커밋을 되돌릴 수 있으며 기존 커밋 SHA-1 해시를 변경하지 않고 새 커밋을 생성하기 때문에 git rebase의 부작용처럼 원격 리포지토리에 영향을 주지 않는다

git revert commands

git revert <commit>: 단일 커밋의 변경을 취소하는 새 커밋을 생성한다

git revert -m "<message>" <commit>: 메시지를 직접 지정한다

git revert -m <parent-number> <merge-commit>: 병합 커밋의 특정 부모 기준으로 변경을 되돌린다

git revert -n <commit>: 변경을 되돌리지만 새 커밋을 생성하지 않고 스테이징만 한다

how git revert works

구성 요소

먼저 지정된 커밋의 변경 사항(추가/삭제 등)을 확인하고, 해당 변경을 반대로 적용한다 (추가된 줄 삭제, 삭제된 줄 복원)

기존 커밋은 그대로 유지한 채로 반전된 변경을 새 커밋으로 기록하여 커밋 히스토리에 추가하고 HEAD를 갱신한다

git cherry-pick

git cherry-pick 명령은 지정한 커밋의 변경 사항을 현재 브랜치에 새 커밋으로 적용한다

원본 커밋의 내용을 복사해 현재 HEAD에 추가하고 독립적인 새 커밋을 생성한다

주로 특정 변경 사항만 가져와야 되는 경우(긴급 수정 커밋 적용, 전체 병합 없이 필요한 부분만 조정하고 싶을 때 등)에 사용한다

git cherry-pick <commit>: 지정한 커밋의 변경 사항을 현재 브랜치에 새 커밋으로 적용한다

git cherry-pick <commit1> <commit2> ...: 지정한 여러 커밋의 변경 사항을 현재 브랜치에 새 커밋으로 적용한다

git cherry-pick <start>..<end>: 특정 커밋 범위의 변경 사항을 현재 브랜치에 새 커밋으로 적용한다

git cherry-pick n <commit>: 커밋의 변경 사항을 스테이징 영역에 추가하며 자동으로 커밋하지 않는다