Git 뜯어먹기

Git 뜯어먹기

2024년 6월 23일 by이호연
thumbnail.png

Git을 뜯어먹어보자

내게 git이라면 푸시, 풀, 머지, 커밋만 있었다. 그러니 이번 기회에 git을 한번 뜯어먹어보자.
git이 실제로 어떻게 관리되는지, 어떻게 동작하는지 알아보겠다.

Git의 구성 요소

Git은 크게 세 가지 구성 요소로 이루어져 있다.

Git의 구성 요소

  1. Working Directory: 실제 파일이 저장되는 디렉토리
  2. Index: 스테이징 영역으로도 불리며, 변경 사항을 저장하는 공간으로, 커밋을 준비하는 단계
  3. Repository: 변경 사항이 저장되는 공간으로, 실제로 버전 관리가 이루어지는 공간

git을 사용하면서 git add, git commit, git push 등의 명령어를 사용하는데, 이 명령어들은 각각 Index, Repository에 영향을 미치는 명령어이다. 개발자는 Working Directory에서 파일을 수정하고, 수정한 파일을 Index에 추가한 후, Index에 있는 변경 사항을 Repository에 커밋한다. 이런 내용들은 .git 디렉토리에 저장되어 있다.

.git

.git 디렉토리는 git 저장소의 모든 정보를 담고 있는 디렉토리이다. 이 디렉토리는 다음과 같은 파일과 디렉토리로 이루어져 있다.

    • HEAD
    • config
    • description
    • index
  • HEAD

    HEAD 파일은 현재 작업 중인 브랜치를 가리키는 파일이다. HEAD 파일을 열어보면 다음과 같은 내용을 볼 수 있다.

    ref: refs/heads/main

    위 내용은 현재 작업 중인 브랜치가 main 브랜치라는 것을 의미한다. 만약 특정 브랜치가 아닌 특정 커밋을 가리키고 있다면, 다음과 같은 커밋의 해시값을 볼 수 있다.

    1e2b4b3d4e5b6b7c8d9e0f1a2b3c4d5e6f7a8

    objects

    objects 디렉토리는 git의 객체를 담고 있는 디렉토리이다. git의 객체는 blob, tree, commit 등이 있다.

    • blob
      파일의 내용을 담고 있는 객체로, 파일의 내용을 압축(zlib)해서 SHA-1 해시값을 구한 후, 해당 해시값의 파일로 저장한다. 파일의 메타데이터는 저장하지 않고 파일의 내용만 저장한다.
      따라서, 동일한 내용의 파일이 여러개 있더라도 한 번만 저장하게 된다.

    • tree
      디렉토리의 내용을 담고 있는 객체이다. blob과는 다르게 식별자, 파일 데이터의 해시값, 파일 이름을 저장한다. 이름에서 알 수 있듯이 tree는 blob과 또 다른 tree로 이루어져 있다.

    • commit
      커밋을 담고 있는 객체로, 각 커밋별로 하나의 커밋 객체가 생성된다. tree의 해시값, author, committer, 직전 커밋 등의 정보를 담고 있다.
      커밋 객체는 Linked List 형태로 이루어져 있어, 직전 커밋을 가리키는 포인터를 가지고 있다.

    refs

    refs 디렉토리는 브랜치, 태그 등을 담고 있는 디렉토리이다.
    로컬에서 작업 중인 브랜치는 refs/heads 디렉토리에 저장되고, 원격 저장소의 브랜치는 refs/remotes 디렉토리에 저장된다. 다음과 같은 형태를 띌 수 있다.

      • main
        • HEAD
        • main
  • hooks

    hooks 디렉토리는 git의 훅 스크립트를 담고 있는 디렉토리이다.
    훅 스크립트는 git의 특정 이벤트가 발생할 때 실행되는 스크립트로, pre-commit, post-commit 등의 훅이 있다.
    husky, lint-staged 등의 라이브러리를 사용하면 이 훅 스크립트를 쉽게 작성할 수 있다.

    자주 쓰는 명령어

    이제 git의 구성 요소와 .git 디렉토리에 대해 알아보았으니, git을 사용하면서 자주 사용하는 명령어와 그 동작 방식을 알아보자.

    원격 저장소에 변경 사항 반영하기

    git add

    git add 명령어는 Working Directory에서 변경된 파일을 Index에 추가하는 명령어이다.
    즉, 내 작업 내용을 staged 상태로 만드는 것이다.
    다만, .gitignore 파일에 등록된 파일은 git add 명령어를 사용해도 Index에 추가되지 않는다.

    git commit

    git commit 명령어는 Index에 있는 변경 사항을 Repository에 커밋하는 명령어이다.
    커밋을 하면, Repository에 커밋 객체가 생성되고, Index에 있는 변경 사항이 Repository에 저장된다.

    옵션설명
    -m커밋 메시지를 작성하는 옵션으로, 커밋 메시지를 인라인으로 작성할 수 있다.
    -a변경된 파일을 자동으로 Index에 추가하는 옵션으로, git add 명령어를 생략할 수 있다.
    -amend직전 커밋을 수정하는 옵션이다.
    --no-verify훅 스크립트를 실행하지 않는 옵션으로, 훅 스크립트를 실행하지 않고 커밋할 수 있다.
    --allow-empty변경 사항이 없어도 커밋할 수 있는 옵션으로, 커밋 메시지만 작성하면 커밋할 수 있다.
    --no-edit직전 커밋 메시지를 그대로 사용하는 옵션으로, 커밋 메시지를 수정하지 않고 커밋할 수 있다.

    git push

    git push 명령어는 Repository에 있는 변경 사항을 원격 저장소에 업로드하는 명령어이다.
    원격 저장소에는 브랜치가 있고, git push 명령어를 사용하면 해당 브랜치에 변경 사항이 업로드된다.

    옵션설명
    -u원격 저장소와 로컬 저장소를 연결하는 옵션으로, 원격 저장소를 설정할 수 있다.

    원격 저장소에서 변경 사항 가져오기

    git fetch

    git fetch 명령어는 원격 저장소의 변경 사항을 가져오는 명령어이다.
    git fetch 명령어를 사용하면, 원격 저장소의 변경 사항을 로컬 저장소로 가져올 수 있다.

    병합과 재배치

    3-way merge

    3-way merge는 merge의 기본 동작 방식으로, 두 브랜치에 신규 commit이 한개 이상 있을 때 사용된다.

    3-way merge 동작 방식

    3-way merge는 새로운 병합 커밋을 생성한다.
    아래와 같은 명령어로 브랜치를 병합할 수 있다.

    git merge feature

    fast-forward

    fast-forward는 브랜치가 한쪽으로만 이동할 때 사용된다.
    즉, 기준 브랜치에는 새로운 커밋이 없고, 다른 브랜치에만 새로운 커밋이 있을 때 사용된다.

    fast-forward 동작 방식

    기준이 되는 브랜치에 새로운 커밋이 없다면 자동으로 fast-forward 병합이 된다.
    만약 강제로 fast-forward 병합이 아닌 병합을 하고 싶다면, --no-ff 옵션을 사용하면 된다.

    git merge --no-ff feature

    rebase and merge

    rebase and merge는 우선 rebase를 진행하고 나서 merge를 진행하는 방식이다.
    rebase는 이름에서 유추할 수 있듯이 브랜치의 base를 옮기는 작업이다.

    rebase

    그리고 이걸 fast-forward 병합하는 것이 rebase and merge이다.

    git switch feature
    git rebase main
    git switch main
    git merge feature

    이렇게 하면 커밋 이력이 깔끔하게 유지된다.

    squash merge

    squash merge는 여러 커밋을 하나로 합치는 방식이다.
    여러 커밋을 하나로 합치면, 커밋 이력이 깔끔해지고, 커밋 메시지도 한 번만 작성하면 된다.

    squash merge

    git switch main
    git merge --squash feature
    git commit -m "Merge"

    이렇게 하면 여러 커밋을 하나로 합칠 수 있다.

    병합 충돌

    git merge 에서의 ours, theirs

    ourstheirs는 병합 충돌 시 사용되는 키워드이다.

    • ours: 현재 브랜치의 변경 사항
    • theirs: 병합 대상 브랜치의 변경 사항
    git merge feature
    git checkout --ours file.txt
    git checkout --theirs file.txt

    git checkout --ours 명령어는 현재 브랜치의 변경 사항을 적용하고, git checkout --theirs 명령어는 병합 대상 브랜치의 변경 사항을 적용한다.

    git rebase 에서의 ours, theirs

    그러나 rebase 상황에서 발생한 충돌은 ourstheirs가 반대로 적용된다.

    • ours: HEAD가 가리키고 있는 것
    • theirs: rebase 대상 브랜치가 가리키고 있는 것

    이렇게 되는 이유는, rebase시에 먼저 HEAD를 옮기고, 그 다음에 rebase 대상 브랜치를 옮기기 때문이다.

    기타 명령어

    git checkout

    git checkout 명령어는 브랜치를 변경하거나, 파일을 복구하는 명령어이다.
    브랜치를 변경할 때는 git checkout -b 명령어를 사용하고, 파일을 복구할 때는 git checkout 명령어를 사용한다.

    옵션설명
    -b새로운 브랜치를 생성하는 옵션으로, 브랜치를 생성하고 변경할 수 있다.
    -f변경 사항을 무시하고 브랜치를 변경하는 옵션으로, 변경 사항을 덮어쓸 수 있다.

    git reset

    git reset 명령어는 커밋을 초기화하는 명령어이다.

    옵션설명
    --soft커밋을 초기화하고, 변경 사항을 Index에 추가하는 옵션으로, 커밋을 취소할 수 있다.
    --mixed커밋을 초기화하고, 변경 사항을 Working Directory에 추가하는 옵션으로, 커밋을 취소할 수 있다.
    --hard커밋을 초기화하고, 변경 사항을 모두 삭제하는 옵션으로, 커밋을 취소할 수 있다.

    git cherry-pick

    git cherry-pick 명령어는 특정 커밋을 가져오는 명령어이다.

    옵션설명
    -n커밋을 가져오고, 커밋을 적용하지 않는 옵션으로, 커밋을 가져올 수 있다.

    git stash

    git stash 명령어는 변경 사항을 임시로 저장하는 명령어이다.

    옵션설명
    save변경 사항을 임시로 저장하는 옵션으로, 변경 사항을 저장할 수 있다.
    pop변경 사항을 복구하는 옵션으로, 변경 사항을 복구할 수 있다.
    apply변경 사항을 적용하는 옵션으로, 변경 사항을 적용할 수 있다.
    drop변경 사항을 삭제하는 옵션으로, 변경 사항을 삭제할 수 있다.

    git log

    git log 명령어는 커밋 로그를 확인하는 명령어이다.

    옵션설명
    --oneline간략하게 커밋 로그를 확인하는 옵션으로, 한 줄로 커밋 로그를 확인할 수 있다.
    --graph그래프 형태로 커밋 로그를 확인하는 옵션으로, 그래프 형태로 커밋 로그를 확인할 수 있다.
    --decorate브랜치, 태그 등을 표시하는 옵션으로, 브랜치, 태그 등을 표시할 수 있다.

    git reflog

    git reflog 명령어는 HEAD의 이동 기록을 확인하는 명령어이다.
    이것을 이용해서 git reset --hard 명령어로 되돌린 커밋을 다시 복구할 수 있다.