커밋할 경우 Git은 데이터의 스냅샷에 대한 포인터, 저자나 커밋 메시지 같은 메타데이터, 이전 커밋에 대한 포인터 등을 포함하는 커밋 개체(커밋 Object)를 저장한다. 최초 커밋을 제외한 나머지 커밋은 이전 커밋 포인터가 적어도 하나씩 있고 브랜치를 합친 Merge 커밋 같은 경우에는 이전 커밋 포인터가 여러 개 있다.

아래의 명령과 같이 디렉터리의 파일 3개를 커밋할 경우 아래의 그림과 같이 저장된다.
$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'
git commit 으로 커밋하면 먼저 루트 디렉터리와 각 하위 디렉터리의 트리 개체를 커밋 해시과 함께 저장소에 저장한다. 그다음에 커밋 개체를 만들고 메타데이터와 루트 디렉토리 트리 개체를 가리키는 포인터 정보를 커밋 개체에 넣어 저장한다. 그래서 필요하면 언제든지 스냅샷을 다시 만들 수 있다.
Git 저장소에는 각 파일에 대한 Blob 3개, 트리 개체 1개, 커밋 개체 1개로 총 5개의 데이터 개체가 생긴다.

Git의 브랜치는 커밋 사이를 이동할 수 있는 포인터 같은 것이다. 기본적으로 Git은 master 브랜치를 만드는데, master 브랜치는 자동으로 가장 마지막 커밋을 가리킨다.

git branch 명령으로 브랜치를 생성할 수 있다. git branch 명령으로 브랜치를 만들 경우 아래 그림과 같이 마지막 커밋을 가리킨다.
$ git branch testing

Git은 현재 작업 중인 브랜치가 무엇인지 'HEAD'라는 특수한 포인터로 파악한다. git branch 명령은 브랜치를 만들기만 하고 브랜치를 옮기지 않는다.

git log 명령에 --decorate 옵션을 사용하여 브랜치가 어떤 커밋을 가리키는지 확인할 수 있다.
$ git log --oneline --decorate
c821068 (HEAD -> main, testing) modified test.txt
dbab39d initial commit
git checkout 명령으로 다른 브랜치로 이동할 수 있다.
$ git checkout testing
Git 2.23.0 이후 버전은 git switch 명령을 사용하여 다른 브랜치로 이동할 수도 있다.
$ git switch testing
이렇게 하면 HEAD는 testing 브랜치를 가리킨다.

이 상태에서 커밋을 새로 작성하면 아래와 같이 HEAD가 testing 브랜치의 새 커밋을 가리키게 된다.

여기서 master 브랜치는 여전히 이전 커밋을 가리키는데 master 브랜치로 돌아갈 경우 HEAD가 master 브랜치로 이동하게 되고, 워킹 디렉터리의 파일도 해당 시점으로 돌아가게 된다.
master 브랜치로 돌아가서 파일을 변경한 후 커밋을 했을 때 아래와 같이 작업 내용이 분리되어 진행되게 된다.

git log 명령을 통해 히스토리를 확인하면 다음과 같다.
$ git log --oneline --decorate --graph --all
* 185fb7a (HEAD -> main) add context2
| * 1c78cb3 (testing) add context
|/
* c821068 modified test.txt
* dbab39d initial commit
git checkout 명령에 -b 옵션을 추가해서 브랜치 생성과 Checkout을 한 번에 할 수 있다.
$ git checkout -b hotfix
Switched to a new branch 'hotfix'
Git 2.23.0 이후 버전은 git switch 명령에 -c 옵션을 추가해서 위 기능을 동일하게 수행할 수 있다.
$ git switch -c hotfix
Switched to a new branch 'hotfix'
hotfix에서 작업을 마치고 커밋을 완료하면 아래와 그림과 같다.
$ vim newfile.txt
$ git commit -a -m 'add newfile.txt'
[hotfix ac97498] add newfile.txt
1 file changed, 5 insertions(+), 1 deletion(-)

git merge 명령으로 master 브랜치와 합칠 수 있다. hotfix 브랜치가 가리키는 C4 커밋이 C2 커밋에 기반한 브랜치이기 때문에 브랜치 포인터는 Merge 과정 없이 그저 최신 커밋으로 이동한다. 이런 Merge 방식을 “Fast forward” 라고 부른다.
$ git switch master
Switched to branch 'master'
$ git merge hotfix
Updating 185fb7a..ac97498
Fast-forward
test.txt | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)

필요 없어진 브랜치는 git branch 명령에 -d 옵션을 추가해서 삭제할 수 있다.
$ git branch -d hotfix
Deleted branch hotfix (was ac97498).
iss53 브랜치를 master 브랜치에 Merge 하면 위에서 hotfix 브랜치를 Merge 했을 때와 메시지가 다르다. 현재 브랜치가 가리키는 커밋이 Merge 할 브랜치의 조상이 아니므로 'Fast-forward'로 Merge 하지 않기 때문이다. 이 경우 Git은 각 브랜치가 가리키는 커밋 두개와 공통 조상 하나를 사용하여 3-way Merge를 한다.
$ git merge iss53
Merge made by the 'ort' strategy.
memo1.txt | 0
memo2.txt | 0
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 memo1.txt
create mode 100644 memo2.txt

아래와 같이 3-way Merge의 결과를 별도의 커밋으로 만들고나서 해당 브랜치가 그 커밋을 가리키도록 이동시킨다.

이후 필요 없어진 iss53 브랜치는 삭제하면 된다.
Merge 하는 두 브랜치에서 같은 파일의 한 부분을 동시에 수정하고 Merge 하면 Git은 해당 부분을 Merge 하지 못한다.
$ git merge iss53
Auto-merging test.txt
CONFLICT (content): Merge conflict in test.txt
Automatic merge failed; fix conflicts and then commit the result.
위와 같이 충돌이 발생해서 Merge 하지 못한 경우 git status 명령을 통해 어떤 파일을 Merge 할 수 없었는지 확인할 수 있다.
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: test.txt
no changes added to commit (use "git add" and/or "git commit -a")
충돌이 일어난 파일은 unmerged 상태로 표시된다. 충돌이 일어난 부분은 unmerged 상태인 파일을 확인해보면 알 수 있다.
<<<<<<< HEAD
text
=======
context
>>>>>>> iss53
충돌을 해결한 후 git add를 수행하고 git commit 명령으로 Merge한 것을 커밋하면 된다.
만약 충돌이 발생한 상황에서 Merge를 취소하고 싶다면 --abort 옵션을 사용하면 된다.
$ git merge --abort
git branch 명령으로 브랜치의 목록을 확인할 수 있다. * 기호가 붙어있는 브랜치는 현재 switch 해서 작업중인 브랜치를 나타낸다.
$ git branch
iss53
* master
git branch -v 명령을 실행하면 브랜치마다 마지막 커밋 메시지도 함께 확인할 수 있다.
$ git branch -v
iss53 695f133 memo2.txt
* master 541aca3 Merge branch 'iss53'
--merged 옵션을 추가하면 * 기호가 붙어있는 브랜치 기준으로 Merge 된 브랜치 목록을 확인할 수 있다.
--no-merged 옵션은 반대로 Merge 되지 않은 브랜치 목록을 확인할 수 있다.
$ git branch --merge
iss53
* main
아직 Merge 하지 않은 커밋을 담고있는 브랜치는 git branch -d 명령으로 삭제할 수 없다. 이 때 -D 옵션을 사용하면 Merge 하지 않은 브랜치를 강제로 삭제할 수 있다.
git ls-remote <remote> 명령을 사용해서 원격 저장소의 참조를 조회할 수 있다.
$ git ls-remote https://github.com/username/project_name
5d81aca7a7eea6279c84bcb88b548f8561561d63 HEAD
5d81aca7a7eea6279c84bcb88b548f8561561d63 refs/heads/main
Git 설정에 등록된 원격 저장소를 사용하여 조회할 수도 있다.
$ git ls-remote origin
5d81aca7a7eea6279c84bcb88b548f8561561d63 HEAD
5d81aca7a7eea6279c84bcb88b548f8561561d63 refs/heads/main
git remote show <remote> 명령을 사용해서 모든 원격 저장소의 브랜치와 그 정보를 조회할 수 있다.
$ git remote show https://github.com/username/project_name
* remote https://github.com/username/project_name
Fetch URL: https://github.com/username/project_name
Push URL: https://github.com/username/project_name
HEAD branch: main
Local ref configured for 'git push':
main pushes to main (local out of date)
Git 설정에 등록된 원격 저장소를 사용하여 조회할 수도 있다.
$ git remote show origin
* remote origin
Fetch URL: https://github.com/username/project_name
Push URL: https://github.com/username/project_name
HEAD branch: main
Remote branch:
main tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (fast-forwardable)
git push <remote> <branch> 명령을 사용해서 브랜치를 다른 사람과 공유할 수 있다.
$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
* [new branch] serverfix -> serverfix
예를 들어 git push origin serverfix를 실행한다면 누군가 저장소를 Fetch 하고 나서 serverfix 브랜치에 origin/serverfix라는 이름으로 접근할 수 있다.
Fetch 명령으로 원격 브랜치를 받는다고 해서 serverfix라는 브랜치가 생기는 것은 아니다. 수정 못하는 origin/serverfix 브랜치 포인터가 생기는 것이다.
$ git fetch origin
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/schacon/simplegit
* [new branch] serverfix -> origin/serverfix
새로 받은 브랜치의 내용을 Merge 하려면 git merge origin/serverfix 명령을 사용한다. 아래 명령을 사용해 원격 브랜치에서 시작하는 새 브랜치를 만들 수도 있다.
$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
Git 2.23.0 이후 버전은 git switch -c를 사용하여 위와 동일하게 사용할 수 있다.
git checkout -b <branch> <remote>/<branch> 명령으로도 간단히 추적 브랜치를 만들 수 있지만 --track 옵션을 사용해도 추적 브랜치를 만들 수 있다.
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
-b 옵션을 사용하면 브랜치 이름을 원하는 이름으로 지정한 후 생성할 수 있지만 --track 옵션은 원격 브랜치 이름으로 브랜치 이름을 지정한 후 생성한다.
Git 2.23.0 이후 버전은 git switch --track를 사용하여 위와 동일하게 사용할 수 있다.
로컬에는 해당 브랜치가 없고 원격에만 존재한다면 생략해서 사용할 수도 있다.
$ git checkout serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
이미 로컬에 존재하는 브랜치가 원격의 특정 브랜치를 추적하게 하려면 git branch 명령에 -u나 --set-upstream-to 옵션을 붙이면 된다.
$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
-vv 옵션을 사용하여 추적 브랜치가 현재 어떻게 설정되어 있는지 확인할 수 있다.
$ git branch -vv
iss53 7e424c3 [origin/iss53: ahead 2] forgot the brackets
master 1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
testing 5ea463a trying something new
여기서 중요한 점은 명령을 실행했을 때 나타나는 결과는 모두 마지막으로 서버에서 데이터를 가져온(fetch) 시점을 바탕으로 계산한다는 점이다. 현재 시점에서 진짜 최신 데이터로 추적 상황을 알아보려면 먼저 서버로부터 최신 데이터를 받아온 후에 추적 상황을 확인해야 한다.
git push 명령에 --delete 옵션을 사용하여 원격 브랜치를 삭제할 수 있다.
$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
- [deleted] serverfix

위와 같은 브랜치의 경우 merge 명령을 사용하면 아래와 같아진다.
$ git switch master
$ git merge experiment
Merge made by the 'ort' strategy.
memo2.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 memo2.txt

비슷한 결과를 만드는 다른 방식으로 Rebase가 있는데, rebase 명령을 사용한다면 한 브랜치에서 변경된 사항을 다른 브랜치에 적용할 수 있다.
$ git switch experiment
Switched to branch 'experiment'
$ git rebase master
Successfully rebased and updated refs/heads/experiment.

그리고 나서 master 브랜치를 Fast-forward 시킨다.
$ git switch master
Switched to branch 'master'
$ git merge experiment
Updating eedb678..ec38a98
Fast-forward
memo2.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 memo2.txt

Merge를 사용했을 때의 C5 커밋에서의 내용과 Rebase를 사용했을 때의 C4' 커밋에서의 내용은 동일하다. 하지만 Rebase를 사용한다면 좀 더 깨끗한 히스토리를 만들 수 있다.

위의 히스토리에서 server 브랜치는 그대로 두고 client 브랜치만 master로 합치려고 한다. 이 때 server와는 아무 관련 없는 client 커밋인 C8, C9를 master 브랜치에 적용하고 싶을 때 아래 명령을 실행하면 된다.
$ git rebase --onto master server client

그 후 master 브랜치에서 Fast-forward 시키면 아래와 같다.
$ git switch master
$ git merge client

아래 명령을 사용하면 server 브랜치의 작업이 다 끝나고 checkout 또는 switch 하지 않고 바로 server 브랜치를 master 브랜치로 Rebase 할 수 있다. 아래 명령은 server 브랜치로 이동한 후 master 브랜치에 Rebase 한다.
$ git rebase master server

그 후 master 브랜치에서 Fast-forward 시킨다.
$ git switch master
$ git merge server
이제 필요없어진 브랜치를 삭제하면 끝난다.
$ git branch -d client
$ git branch -d server

이미 공개 저장소에 Push 한 커밋을 Rebase 하면 안된다!!
이유를 알아보자
아래는 커밋 후 Merge 하고 나서 서버에 Push된 상황이다. 이 원격 저장소를 Fetch와 Merge를 하면 히스토리가 아래와 같이 된다.

그런데 서버에 Push 했던 사람이 Merge를 되돌리고 다시 Rebase 한 후 git push --force 명령을 사용해서 서버의 히스토리를 새로 덮어씌웠다.

이 상태에서 git pull로 서버의 내용을 가져와서 Merge 하면 같은 내용의 수정사항을 포함한 Merge 커밋이 아래와 같이 만들어진다.

git log로 히스토리를 확인해보면 저자, 커밋 날짜, 메시지가 같은 커밋이 두 개(C4, C4') 있다. 같은 커밋이 두 개 있기 때문에 다른 사람들이 혼란스러울 수 있다.

위 상황에서 merge 대신 git rebase teamone/master 명령을 실행하면 Git은 아래와 같은 작업을 한다.
C2, C3, C4, C6, C7)C2, C3, C4)C2, C3 (C4는 C4'와 동일한 Patch다))teamone/master 브랜치에 적용한다.
git pull --rebase 명령으로 Rebase 할 수도 있다. 또는 git fetch 와 git rebase teamone/master 명령으로도 가능하다.
git pull 명령을 실행할 때 기본적으로 --rebase 옵션이 적용되도록 pull.rebase 설정을 추가할 수 있다. git config --global pull.rebase true 명령으로 추가한다.
참고문서: https://git-scm.com/book