Search
🐙

back to the basic of git

subtitle
do you know git?
Tags
git
github
Created
2022/09/25
2 more properties
git pull, push, fetch, merge, rebase, … 등등 기초적인 명령어들만 알면 git을 사용하는 데 문제가 없습니다. 하지만 git이 돌아가는 원리에 대해서는 자세히 알고있지 못했습니다.
충돌이 생기거나 브랜치가 꼬이는 등의 문제가 생길 때 git 히스토리를 깔끔히 관리하고 git command를 활용해 훅이나 액션으로 워크플로우를 구성하는 등 git에 대해 잘 알고 있으면 이점이 많을 것 같아 git에 대해 좀 더 공부해보았습니다.

git 파일의 세가지 상태

1.
Committed: 커밋은 되어있지만 수정하지 않은 파일. (로컬에 저장되어있는 파일)
2.
Modified: 파일을 수정했지만 아직 커밋하지 않은 상태
3.
Staged: 현재 수정한 파일을 곧 커밋할 것이라고 표시한 상태

git 파일 관리

committed: .git 폴더에 스냅샷으로 저장된 파일들
modified: working tree에서 파일 수정
staged: 수정한 파일을 staging함 (git add <filename>)
committed: .git에 스냅샷으로 저장
출처: git-scm
.git
프로젝트의 메타데이터와 git object(git이 추적하는 파일들에 대한 정보)를 저장하는 곳입니다.
원격저장소에 있는 프로젝트를 clone할 때 만들어집니다.
key-value 데이터 저장소이며 추적하는 파일들의 스냅샷은 .git/objects 의 경로에 만들어집니다. key는 파일 데이터 + 헤더로 만든 sha 체크섬 값입니다.
git object key를 만드는 과정 ⇒ 데이터 또는 데이터 길이가 바뀌면 체크섬이 바뀌면서 key가 바뀜
working tree
워킹 트리는 ‘현재 작업하고 있는 폴더’를 말합니다.
staging area / index
변경내용을 로컬 저장소에 커밋하기 전 커밋할 파일이라고 표시하기 위한 공간입니다. 파일이 staging(indexing) 되면 커밋할 스냅샷을 만듭니다. staging된 파일을 커밋하면 .git 폴더에 해당 파일을 영구적인 스냅샷으로 저장합니다.
git hash-object <파일경로 + 파일이름>: .git/objects 디렉토리에 저장하고 그 데이터에 접근할 수 있는 key를 알려줌
git cat-file -p <sha-1 해시값>: sha-1 해시값으로 파일 내용 조회
# git hash-object <파일경로 + 파일이름> >> git hash-object main.js => 5e86f711a143170b58898733d079246a543a4636 # git cat-file -p <sha-1 해시값> >> git cat-file -p 5e86f711a143170b58898733d079246a543a4636 => console.log('hello world!');
Shell
git 개체 종류: 커밋 개체, 트리(디렉토리) 개체, blob(파일) 개체
git cat-file -t [sha1] : tree/blob/commit 중 하나의 타입 출력
git cat-file -p [sha1] : tree => 트리(디렉토리) 정보, blob => 실제 파일 내용, commit => 커밋정보(메세지 등등)

git hook + git status로 푸시하기 전 미리 체크하기

git status로 현재 working tree 파일들의 상태를 출력할 수 있습니다.
lint나 graphql-code-generate처럼 개발자가 실수로 빼먹고 커밋을 안할 수 있는 경우가 있습니다.
이럴 땐 푸시하기 전 lint 또는 code-generate 명령어를 실행시킨 뒤 git status 명령어를 실행시켜 변경된 파일이 있다면 push를 실패시키는 git-hook을 만들 수 있습니다.
.husky/hooks/generate-code.ts hook

rebase의 활용

다른 브랜치의 커밋들을 현재 브랜치로 당겨오기

rebase vs merge
git rebase는 브랜치 하나에서 작업한 것처럼 git history를 깔끔하게 관리할 수 있습니다.
1.
git merge <merge할 브랜치> (브랜치 시각화: git log --graph --all)
master 브랜치에서 머지할 브랜치의 커밋을 가져옵니다.
merge commit 을 새로 만듭니다.
2.
git rebase master
merge 브랜치에 checkout한 후, master로 rebase
기존 커밋이 아닌 새로운 커밋을 master 브랜치 뒤에 갖다붙입니다.
원래 커밋의 해시는 531db2, rebase 이후 커밋의 해시는 270d0f
git checkout master + git merge <merge 브랜치> : master 브랜치를 fast-forward(HEAD를 앞으로 이동) 시킵니다.

커밋 수정하기

1.
현재 commit 수정하는 법
파일 수정
git add .
git commit --amend (참고로 amend는 수정하다 라는 의미): 커밋 메시지 수정 가능
주목할 것은 기존 add merge.js 커밋의 해시와 바뀐 커밋의 해시가 다르다는 점. 즉, 기존 커밋이 유지되는 게 아니라 새로운 커밋으로 바뀐다는 것입니다.
2.
과거의 커밋 여러개를 수정하는 법
수정하고 싶은 커밋으로 HEAD를 이동
커밋 수정
원래 커밋으로 HEAD를 이동
checkout으로 수정하기
checkout이 아니라 굳이 rebase를 사용하는 이유는 git commit --amend 를 사용하면 새로운 커밋이 생성되기 때문에 커밋히스토리에서 새로운 브랜치가 생기기 때문입니다.
아래 과정을 통해 checkout으로 수정해보면 git log graph에서 updated README.md 커밋으로부터 새로운 브랜치가 생겼고 해당 브랜치에 수정된 커밋이 들어갔음을 알 수 있습니다.
update README.md로 checkout 후 파일 수정
git commit --amend
git checkout master + git merge 0714e58(amend된 커밋)
checkout으로 수정 시 git history
interactive rebase로 수정하기
interactive mode
git rebase -i HEAD~2 : 수정하려고 하는 커밋으로 이동. 수정하려는 커밋이 2개면 HEAD~2 를 입력합니다. -i 는 interactive 옵션입니다.
interactive 모드에서 commands 사용: 앞의 커맨드만 바꾸고 해시 값이나 커밋 메시지는 건들 필요 없습니다.
pick: 원래 커밋 그대로 사용
reword: 커밋 메시지만 수정
edit: 해당 커밋에서 파일 수정 및 커밋 메시지 수정 가능
…등등
reword 커밋은 커밋 메시지만 수정 후 저장
edit 커밋은 세 가지 선택이 주어집니다
git commit --amend : 파일 수정 후 staged로 올리고 amend를 하면 커밋 수정 가능
git rebase --continue : 모든 수정이 완료되면 continue 옵션으로 다음 커밋으로 이동. 만약 마지막 커밋이면 rebase가 끝납니다.
git rebase --abort : rebase를 아예 중단시킬 수 있습니다.
완료 이후의 git log입니다. checkout 때와 달리 기존 branch가 변경 전과 똑같이 유지됩니다.
rebase로 수정 시 git history
git fork(https://fork.dev/) 앱으로도 interactive rebase가 가능합니다.
git fork

커밋 옮기기

master - new - new2 와 같이 브랜치의 브랜치를 딴 경우, master 브랜치에 new-2 커밋을 붙일 때도 rebase를 활용하면 됩니다.
git rebase --onto [newbase] [upstream] [branch]
newbase: base가 될 브랜치
upstream: branch 가 원래 뻗어나온 브랜치
branch: rebase될 브랜치
git rebase --onto master new new-2

rebase를 사용하면 안되는 경우

이미 원격 저장소에 Push 한 커밋을 Rebase 하지 말 것
왜냐하면 다른 사람에게 욕을 먹을 것이기 때문입니다.. 아직 원격 저장소에 푸시하지 않은 커밋만 rebase 하세요.
rebase는 기존 커밋을 그대로 사용하지 않고 내용은 같지만 다른 커밋을 새로 만듭니다.
예를 들어, 내가 원격 저장소에 푸시한 커밋을 다른 팀원이 pull해서 작업한다고 합시다. 그런데 그 커밋을 rebase해서 다시 푸시하면 그 팀원은 다시 merge 해야 합니다. 그 팀원이 merge한 내용을 pull하면 내 코드도 엉망이 됩니다.
팀원 중 한 명이 troll 브랜치를 master에 머지해서 origin/master에 푸시했습니다. 그리고 내가 origin/master를 pull한 후 teamone 브랜치를 따서 작업합니다.
트롤 팀원이 마스터에 troll 브랜치 머지
내 로컬 브랜치 teamone에서 origin/master pull 한 후 작업
이 상태에서 트롤 팀원이 merge한 커밋을 지우고 해당 브랜치를 rebase하고 forch push로 origin/master를 덮어씌웁니다. (rebase는 새로 커밋을 만들기 때문에 다른 팀원이 의존하는 커밋이 없어지고 기존에 팀원이 작업했던 커밋도 사라집니다. 잘보면 add teamone.js commit이 master branch에서 제거되었습니다.)
내 teamone 브랜치에서 origin/master를 pull하면 기존에 troll 브랜치를 merge했던 이력과 rebase했던 이력이 겹치면서 git history가 혼란스러워집니다.
이럴 땐 내 teamone 브랜치에서 origin/master를 그냥 pull하지 않고 git pull origin master --rebase로 내 브랜치에서 한번 더 rebase하면 완전히 같은 내용의 커밋은 사라지지만, 덮어씌워진 master로 rebase하게 되어 내 기존 작업 내용(add teamone.js) 도 사라집니다.
커밋 히스토리를 깔끔히 하는 것도 중요하지만 만약 내 기존 작업 내용을 살려야 한다면 이런 케이스에서는 rebase가 아닌 merge를 해야합니다.
*같은 내용의 커밋을 구분할 수 있는 이유: git은 커밋 체크섬 sha이외에도 patch한 내용으로 sha 체크섬을 한번 더 구해서 patch-id를 만듭니다. 그래서 rebase할 때 덮어쓰이지 않은 커밋을 구분할 수 있습니다(내용이 같으면 해시값이 같으니까).

rebase vs merge

로컬 브랜치에서 작업할 때 rebase를 자유롭게 사용할 수 있지만 원격 저장소에 이미 push한 커밋은 절대 rebase하면 안됩니다. 커밋 히스토리를 깔끔하게 관리하려면 애초에 pull할 때 rebase를 한 후 작업합니다.
git config pull.rebase true 로 기본값을 설정할 수 있습니다. 하지만 여러 브랜치를 pull하는 경우 브랜치가 꼬일 수 있습니다.

알아두면 유용한 git 용어, 도구

git refs

깃은 커밋으로 코드 이력을 관리합니다. 커밋은 고유의 SHA1 해시 값을 가지고 있으며, 이 해시 값은 여러 기능에서 참조합니다. 깃에서는 참조하는 해시 값을 refs 목록으로 가지고 있습니다. (출처: git 교과서)
깃 커밋은 고유의 SHA1 해시값을 갖고 있으며 이 해시값은 여러 기능에서 사용될 수 있습니다. 때문에 해시값을 사용하는 것보다 이름으로 된 포인터가 있으면 사용하기 더 쉬울 것입니다. git에서는 외우기 쉬운 이름으로 된 파일에 SHA1 해시값을 저장해 이름과 해시값을 매핑합니다.
find .git/refs
find .git/refs .git/refs .git/refs/heads .git/refs/heads/merge .git/refs/heads/teamone .git/refs/heads/master .git/refs/heads/new .git/refs/heads/new-2 .git/refs/tags .git/refs/remotes .git/refs/remotes/origin .git/refs/remotes/origin/merge .git/refs/remotes/origin/master
Shell
refs 파일 조회
cat .git/refs/heads/new-2 38c1ccf452c3b1fd2ca688fd0172531e4bad8d96
Shell
해당 브랜치의 마지막 커밋 해시값과 매핑되어있습니다.
HEAD
.git/HEAD 파일은 다른 refs를 가리키는 간접 refs라 SHA-1 해시값이 없습니다.
cat .git/HEAD ref: refs/heads/master
Shell

Tag

1.
lightweight tag
특정 커밋에 대한 포인터. 브랜치에 최신 커밋이 존재하든 않든, 특정 커밋만 가리킵니다.
특정 커밋에 이름만 달아줍니다.
git checkout <태그달아줄 커밋>
git tag <태그이름>
2.
annotated tag
Git 데이터베이스에 태그를 만든 사람의 이름, 이메일과 태그를 만든 날짜, 그리고 태그 메시지도 저장한다. GPG(GNU Privacy Guard)로 서명할 수도 있다. 일반적으로 Annotated 태그를 만들어 이 모든 정보를 사용할 수 있도록 하는 것이 좋다 - <git-scm>
annotated tag를 만들려면 git tag -a <tag이름> -m “<tag message>”
-a 옵션을 사용한다.
tag이름은 보통 버전. 릴리즈할 때 많이 사용하기 때문
tag message에는 릴리즈 정보를 적는다.
3.
tag를 통해 커밋 정보 조회하기: git show <tag이름>
annotated tag를 조회하는 경우 커밋 메시지뿐만 아니라 태그 정보도 함께 조회됩니다.
4.
tag push하기
git push 명령어는 태그를 자동으로 푸시하지 않으니 git push origin —tags 명령어로 태그를 전송합니다.
5.
github API로 tag 생성 시 주의할 점
git tag command를 이용하면 git tag object를 생성하고 refs/tags/태그이름 이라는 reference도 생깁니다.
find .git/refs .git/refs ... .git/refs/tags .git/refs/tags/v1.0 .git/refs/tags/test-tag ...
Shell
refs/tags/태그이름 에서 해시값이 가리키는 object가 git tag object입니다.
cat .git/refs/tags/test-tag e8d827d2d151609239abba85a504dae7124e94e9 ➜ git cat-file -p e8d827d2d151609239abba85a504dae7124e94e9 tree 85591cfea927a873cee2518a52439ed8e801a421 parent 56bd3964d07ed68d06b244f1fe4eefbe64df8e91 author hj-song <hj.song@toss.im> 1664332945 +0900 committer hj-song <hj.song@toss.im> 1664344296 +0900 fix: updated README.md
Shell
그런데 github API 에서는 create git tag와 create references 가 분리되어 있습니다.
create git tag 는 annotated tag 를 위한 API입니다.
github api로 annotated tag를 만들려면 create git tag + create reference , lightweight tag로 만들려면 create reference api를 사용합니다.
Note that creating a tag object does not create the reference that makes a tag in Git. If you want to create an annotated tag in Git, you have to do this call to create the tag object, and then create the refs/tags/[tag] reference. If you want to create a lightweight tag, you only have to create the tag reference - this call would be unnecessary. - github docs

리비전 조회하기

1.
Plumbing 명령과 Porcelain 명령
저수준의 명령어는 Plumbing 명령어라고 부르고 좀 더 사용자에게 친숙한 사용자용 명령어는 Porcelain 명령어라고 부릅니다. 우리가 평소에 사용하는 명령어가 포크레인 명령어입니다.
2.
리비전이란?
git object를 가리킬 수 있는 이름은 모두 리비전이라고 합니다. sha1, tag, ref 등 특정 커밋을 가리킬 수 있으면 모두 리비전이 될 수 있습니다. (git-scm에서 리비전 종류를 확인할 수 있습니다)

리비전 조회하기: git reflog

git은 자동으로 지난 몇 달 동안(너무 오래된 건 기록 x) 브랜치와 HEAD가 가리켰던 커밋들을 모두 기록하는데 이 로그들을 RefLog라고 부릅니다.
git reflog
bc6ecce (HEAD -> master, tag: v1.0) HEAD@{0}: checkout: moving from e8d827d2d151609239abba85a504dae7124e94e9 to master e8d827d (tag: test-tag) HEAD@{1}: checkout: moving from master to e8d827d2d151609239abba85a504dae7124e94e9 bc6ecce (HEAD -> master, tag: v1.0) HEAD@{2}: checkout: moving from teamone to master 776e0e7 (teamone) HEAD@{3}: pull origin master --rebase (finish): returning to refs/heads/teamone 776e0e7 (teamone) HEAD@{4}: pull origin master --rebase (pick): feat: add teamone.js f691c6d (origin/master) HEAD@{5}: pull origin master --rebase (start): checkout f691c6d9a8d8de11fea51784f5d68b30df950613 9d0d0eb HEAD@{6}: commit: feat: add teamone.js bc6ecce (HEAD -> master, tag: v1.0) HEAD@{7}: reset: moving to HEAD~6 f691c6d (origin/master) HEAD@{8}: pull origin master --rebase: Fast-forward be1d82f HEAD@{9}: reset: moving to HEAD~6 78b585b HEAD@{10}: pull origin master: Merge made by the 'recursive' strategy. 3b721c8 HEAD@{11}: rebase (continue) (finish): returning to refs/heads/teamone 3b721c8 HEAD@{12}: rebase (continue): feat: add troll.js 4625571 HEAD@{13}: rebase (skip) (pick): fix: change master b161180 HEAD@{14}: rebase (skip): feat: add teamone.js
Shell
순서, 시간을 사용해 리비전을 조회할 수도 있습니다.
git show HEAD@{5}
Shell
git show master@{yesterday} git log [--since=<date-from>] [--until=<date-to>] git log --since='Apr 1 2021' --until='Apr 4 2021' git log --since='2 weeks ago'
Shell
git log -g 명령어로 git reflog 결과를 git log와 같은 형태로 볼 수 있습니다

리비전 조회하기: dot

1.
double dot: 범위로 커밋 가리키기
git log A..B = A에는 없지만 B에는 있는 커밋들
리모트 저장소에 푸시하기 전 현재 로컬 브랜치와 리모트 브랜치의 차이를 확인할 수 있습니다.
git log master..teamone
Shell
2.
비교할 ref가 3개 이상인 경우: ^ 또는 --not 옵션 사용
refA, refB에는 있지만 refC에는 없는 커밋
git log refA refB ^refC # master에는 없지만 topic, develop에 있는 커밋 git log topic develop ^master
Shell
3.
triple dot: 공통 커밋은 제외하고 서로 다른 커밋만 보여줍니다.
요런 느낌
git log master...teamone
Shell

리비전 조회하기: 특정 브랜치의 최신 커밋 조회하기

1.
git rev-parse <브랜치이름>
git rev-parse master bc6eccebd2775a28bf10f75f53d7a01ec4dd32b4
Shell
2.
--abbrev-ref 옵션으로 현재 브랜치의 이름을 가져올 수 있습니다.
git rev-parse --abbrev-ref HEAD
Shell

conflict 한방에 해결하기

1.
git merge(pull) -s ours/theirs <branch name>
-s, --strategy : 컨플릭트 발생 시 theirs면 pull하려는 브랜치 내용을, ours면 현재 브랜치 내용을 선택합니다.
2.
git rerere
Git은 충돌이 났을 때 각 코드 덩어리를 어떻게 해결했는지 기록을 해 두었다가 나중에 같은 충돌이 나면 기록을 참고하여 자동으로 해결한다 - git-scm
rerere: reuse recorded resolution, 기록한 해결책 재사용하기
용도
conflict를 해결하고 Merge 했는데 다시 Rebase 하려고할 때 같은 충돌을 두 번 해결할 필요 없다.
긴 호흡의 브랜치를 깔끔하게 Merge 하고 싶은데 Merge 커밋은 많이 만들고 싶지 않을 때 사용
git config rerere.enabled true 로 옵션 활성화
충돌을 해결했을 때의 diff 내용을 기록해서 같은 조건의 충돌이 다시 발생하면 기록된 내용으로 충돌을 해결합니다.
Auto-merging merge.js CONFLICT (content): Merge conflict in merge.js Resolved 'merge.js' using previous resolution. Automatic merge failed; fix conflicts and then commit the result.
Shell

깃헙 레포 클론이 오래 걸릴 때: git shallow clone

배포 파이프라인에서 깃헙 레포를 클론해 도커 이미지를 빌드하는 경우와 같이, git 히스토리가 너무 많아 클론이 오래 걸리는 경우 git clone <git url> --depth=1 과 같이 shallow clone을 통해 빠르게 클론할 수 있습니다.

git cherry-pick

git cherry-pick <start-commit>..<end-commit> : start-commit 이후 커밋부터 end-commit까지 체리픽됩니다.
충돌 시 해결하고 git cherry-pick --continue 로 다음 파일로 이동
git checkout --theirs . : --theirs--ours 사용해 해당 커밋의 충돌을 해결할 수 있습니다.

git 느려질 때

터미널에서 ls 커맨드가 느릴 땐 느린 레파지토리에서 git 관련 서포트 기능을 끄면 빨라집니다.
git config oh-my-zsh.hide-status 1
Plain Text

stages만 stash하기

git stash -- $(git diff --staged --name-only)

git switch 이전 브랜치로 이동

직전 브랜치 git switch -
전전 브랜치 git switch @{-2}

git branch 최근 커밋된 순서로 정렬 조회하기

git branch --sort=-committerdate
Plain Text

더 찾아봐야할 것들

git prune

git 검색: git grep

git replace

git submodule

references