먹고 기도하고 코딩하라

알고도 까먹었던 Git 기록 본문

상자

알고도 까먹었던 Git 기록

사과먹는사람 2022. 11. 28. 22:23
728x90
728x90

 

나에게 Git이란 시간에 따라 변경되는 작업물에 대한 기록을 남길 수 있게 도와주는 소프트웨어였다.

그냥 무지성으로 git status로 확인하고 git add > git commit -m "message" > git push 하면 끝이었음.

그런데 이제 회사에 들어가면 브랜치를 따고 PR해서 코멘트도 받고 머지도 해야하고...

git을 다시 이해해야겠다는 생각이 들었다.

개인적으로는 git-scm 한글판 문서가 제일 좋은 것 같다. 예시도 다양해서 add commit push만 하는 나와 같은 초보자도 이해하기 쉬워, 강력 추천한다.

 

사실 팀 작업에서 브랜치를 따고 PR 날리고 머지한 적이 있긴 한데, 각자 작업하는 파일도 다르고..

충돌이 일어나지 않아서 딱히 공부를 할 필요는 없었다. 그냥 브랜치를 생성하고 checkout 해서 작업 브랜치를 바꾸고 머지하고.. 그 정도만 하면 됐으니까.

회사 가기 전 뭘 공부하면 좋을까 하다가 협업에 필요한 기본적인 git부터 다시 보고 다른 걸 차차 보기로 결정했다.

 

 

git으로 관리되는 파일/디렉터리들의 상태

git은 데이터를 파일 시스템 스냅샷의 연속으로 취급한다고 한다.

이게 무슨 말일까? subversion과 같은 다른 버전 관리 시스템에서는 데이터 변화를 시간 순으로 관리하며 파일들의 집합을 관리한다. 그림으로 보면 조금 더 이해가 쉬울 것 같다. 가령 파일 A는 버전 2에서 1번 수정되고 버전 4에서 2번째로 수정되는 등 버전(시간)에 따라 수정된 내용이 저장되는 것이다.

 

하지만 git의 경우, 파일이 달라지지 않았다면 이전 상태 파일에 대한 링크만 저장한다고 한다. 점선으로 표시된 부분이 이전 버전과 현재 버전을 비교했을 때 변하지 않은 것들을 나타내는 것이다. 파일 A는 버전 3과 버전 2가 똑같으므로 버전 3에서는 버전 2의 링크만 저장할 뿐이다.

 

 

또한 git은 파일을 committed, modified, staged 상태로 분류한다고 한다. (이건 공식문서에도 써있음)

  • modified : 수정한 파일이 아직 로컬 DB에 커밋되지 않음
  • staged : 수정한 파일을 커밋할 것이라고 표시한 상태
  • committed : 데이터가 로컬 DB에 안전하게 저장됨

이걸 이해하려면 또 tracked와 untracked를 알아야 하는데, 이건 위의 3개 상태와는 별개이다. 모니터링 대상이냐 아니냐를 의미하는 것이고, 위의 3개 상태는 tracked 파일에만 해당된다.

git이 데이터 변화를 모니터링하고 있는 파일은 tracked 상태이고, git이 변화를 추적하고 있지 않은 파일의 경우 untracked 상태다. '추적'이라는 단어의 의미를 생각해보면 쉽다.

 

다음은 git으로 관리하는 파일의 라이프사이클을 나타낸 그림이다.

 

 

 

git add가 정확히 뭘 하는 명령어인가?

git add . > git commit > git push 이 3단계는 너무나 익숙하고 자연스러운 흐름이라 딱히 생각해본 적이 없는데, git add가 정확히 뭘 위한 명령어인지를 조사해봤다. 

git add는 기본적으로 파일을 staged 상태로 만들려는 명령어이다. staged 상태란, 커밋으로 저장소에 기록할 상태를 의미한다. 즉 수정된 파일이 다음 커밋에 포함되도록 만들려면 staged 상태로 보내야 한다.

 

(1) 새로운 파일 A를 추가하고 git add

git add 전에 파일 A는 아직 untracked 상태이다. git add 이후에 파일 A는 tracked 상태가 되면서, 동시에 다음 커밋에 파일이 포함되기 때문에 staged 상태가 된다.

-> tracked + staged

(2) 커밋된 파일 A를 수정하고 git add

파일 A는 이미 tracked 상태이다.

수정 전에 파일 A는 tracked + unmodified 상태이다.

수정 후, git add 전에 파일 A는 tracked + modified 상태이다.

git add를 하게 되면 파일 A는 tracked + staged 상태가 된다.

-> modified -> staged

 

 

브랜치 사용

Git의 브랜치는 커밋 사이를 가볍게 이동할 수 있는 포인터 같은 것이라고 설명하고 있다.

git branch	# 브랜치의 목록을 보여준다. * 표가 붙은 브랜치는 현재 checkout되어 작업 중인 브랜치다
git branch -v	# 브랜치마다 마지막 커밋 메시지를 볼 수 있다

git branch testing	# testing 브랜치를 만들되, testing으로 옮기지는 않는다

git checkout testing	# testing 브랜치로 HEAD 포인터를 이동한다

git checkout -b iss53	# 53번 이슈를 처리하기 위한 브랜치를 만들고 바로 iss53으로 HEAD를 이동한다

git merge testing	# 현재 브랜치와 testing 브랜치를 병합한다

git branch -d testing	# 필요없어진 testing 브랜치를 삭제한다

git branch -D testing	# merge하지 않은 브랜치를 강제로 삭제한다

(HEAD 포인터란, 현재 작업하는 로컬 브랜치를 가리키는 포인터이다)

만약 testing에서 어떤 작업을 하고 다시 git checkout main을 해서 main 브랜치로 돌아오게 되면, 로컬 컴퓨터에서 작업하고 있는 디렉터리(워킹 디렉터리) 파일은 main에서 작업했던 내용으로 변경된다. git checkout 하면서 워킹 디렉터리 내용이 변경되는 건 예전에 조금 헷갈렸던 부분이다.

또 헷갈렸던 건 현재 디렉터리에서 커밋을 마쳐야 다른 브랜치로 checkout할 수 있다는 점이다. 그런데 완벽하지 않은 상태에서 커밋하기 꺼려질 때는 git stash를 사용할 수 있다. stash는 커밋은 하지 않고 일단 저장해뒀다가, 나중에 다시 이 브랜치로 돌아와 작업하려고 할 때 쓰기 좋은 명령어다. 

git stash

 

 

Merge 방식

  • Fast-forward : 현재 master 브랜치에서 hotfix 브랜치를 merge할 때, hotfix 브랜치가 master 브랜치 이후의 커밋을 가리키고 있다면(즉, master 브랜치의 현 커밋이 hotfix의 조상이나 부모라면) master 브랜치가 hotfix 브랜치와 동일한 커밋을 가리키도록 병합한다. 

 

C4는 C2에서 뭔가 수정한 커밋이다. C2에서 갈라져나온 것이기 때문에, merge하면 master가 C4 위치로 옮겨가게 된다.

  • Recursive : 현재 master 브랜치에서 iss53 브랜치를 merge할 때, iss53 브랜치가 가리키는 커밋이 master 브랜치의 조상(ancestor)이 아니기 때문에 Fast-forward 방식을 사용할 수 없다.
    • 그럼 recursive strategy로 병합하게 되는데, 이 경우, 3-way Merge(master 브랜치가 가리키는 커밋인 C4, iss53 브랜치가 가리키는 커밋인 C5, 공통 조상인 C2 커밋)를 하게 된다.
    • 이 결과를 별도의 커밋으로 만든다. (C6) 그리고 master 브랜치가 C6을 가리키도록 한다.

iss53가 가리키는 커밋의 조상은 현재 master 브랜치가 가리키는 커밋이 아니므로 recursive 전략을 통해 merge하게 된다. 

역시 공식 문서가 최고다. 이런 사례는 자주 일어날 수 있으므로 기억해두는 게 좋겠다.

 

 

Merge conflict

3-way merge가 실패하는 경우가 생기기도 한다. (예전에 꽤 겪었었지.. 그 때는 지혜롭게 대처하지 못 했던 기억이 난다.) 합치려는 두 브랜치가 같은 파일의 한 부분을 동시에 수정한 것이다! 이건 사람이 직접 해결해줘야 한다. 이 때는 A 브랜치의 내용과 B 브랜치의 수정 내용을 보여주면서 하나를 고르는 방식으로 해결할 수 있다. 새로 작성할 수도 있다고 한다.

 

 

Rebase

한 브랜치에서 다른 브랜치로 합치는 방법 2가지 중 첫 번째가 merge이고 두 번째가 rebase이다.

3-way merge 방식

위의 그림은 C3 커밋을 가진 master가 C4 커밋을 가진 experiment와 공통 조상인 C2로 3-way merge한 것이다.

rebase의 경우, C3 커밋 변경 사항을 patch로 만들고 이것을 C4에 적용시키는 방법이라고 한다. 

git checkout experiment
git rebase master	# 한 브랜치에서 변경된 사항을 다른 브랜치에 적용

뭔가 딱 봐도 신기한 모양이긴 하다. 여기서 이제 master로 브랜치를 바꾸고 merge해서 Fast-forward 시키면 master도 C4'를 가리키게 된다.

git checkout master
git merge experiment

Rebase와 Merge 모두 최종 결과물은 같다. 하지만 커밋 히스토리가 다르다.

Rebase는 브랜치 변경 사항을 순서대로 다른 브랜치에 적용하며 합치고(master 내용을 패치로 만들어 C4에 적용해 C4'를 만들어낸 것처럼), Merge는 C3, C4, 필요하다면 조상인 C2까지 해서 최종 결과들을 가지고 합치게 된다.

 

단 Rebase의 경우 이미 공개 저장소에 push한 커밋에는 하지 말 것을 당부하고 있다. 문서에서 더 자세히 설명하고 있는 듯해서 내 말로 바꿔 표현해보자면.. 여러 사람이 공동 작업을 하다 보면 리모트 트래킹 브랜치의 커밋에 의존을 하게 되는데(부모나 조상으로 갖고 있다던지), 만약 누군가 merge를 되돌리고 다시 rebase하면 내용이 똑같은 커밋이 2개나 생겨서 혼란스러워진다, 그러니 하지 말라고 하는 듯하다.

 

그럼 언제 Rebase를 쓰고 언제 Merge를 쓰게 될까. 무엇이 월등하다는 건 없다고 한다. 내 생각도 그렇고.. 이 역시 상황에 따라 rebase를 쓰기 좋은 때(초벌 내용을 공개하고 싶지 않음)와 merge를 쓰기 좋은 때(기록을 온전히 보존하면서 진행)가 따로 있는 듯하다. 그때그때 잘 판단하면 좋을 것 같다.

로컬에서는 rebase할 수 있지만 remote에 push로 보낸 커밋에 대해서는 절대 rebase하지 말 것을 문서에서는 권하고 있다.

 

 

리모트 저장소

인터넷상이나 네트워크에 있는 저장소를 리모트 저장소라고 한다. remote라고 해도 로컬 시스템에 존재할 수도 있다고는 한다. 프로젝트에 등록된 리모트 저장소는 여러 개가 있을 수 있다. 저장소를 github 등에서 클론하게 되면 origin이라는 이름의 리모트 저장소가 자동 등록된다. 그래서인지 origin은 리모트 저장소 이름의 근본격이 되는 이름으로 사용된다.

여기까지는 그냥 다 아는 리모트 저장소 얘기인데 협업으로 넘어가게 되면 브랜치까지 끼어들어 조금 복잡해지는 것 같다.

 

리모트 트래킹 브랜치는 우리가 보는 origin/master, origin/main 같은 것들이다. 로컬에 존재하긴 하지만 리모트 서버(git 서버라든가)에 연결할 때 브랜치 업데이트 내용에 따라 갱신될 뿐, 로컬에서 임의로 움직일 수는 없다는 특징이 있다.

음.. 그러니까, 이 경우에 master 브랜치는 git 서버에 따로 존재하며, 내가 clone을 했든지 remote add 해서 리모트 저장소를 연결했든지 해서 그 프로젝트를 로컬에서 받아서 사용하는 경우의 master 브랜치는 origin/master라는 거다. 즉 로컬 컴퓨터와 서버의 커밋 히스토리가 별개로 움직인다는 점이다.

origin/master가 서버의 master 브랜치보다 앞서있다면 push로 git 서버의 master 브랜치에 업데이트를 할 수 있고, 반대로 origin/master가 서버보다 뒤쳐졌다면 pull, fetch로 업데이트를 받아올 수 있다(즉, 서버의 최신 커밋을 받아오는 것이다).

 

 

Reset

살다 보면 나 돌아갈래!! 하고 싶은 순간이 생기는데 개발할 때도 예외는 아닌 듯하다. 이전 커밋으로 돌아가고 싶을 때가 있다. 그럴 때 reset을 사용할 수 있다.

Git은 HEAD, Index, 워킹 디렉토리 3가지를 관리하고 있다. Head는 현재 브랜치의 마지막 커밋을 가리키는 포인터다. Index는 지금 브랜치에서 다음에 커밋할 것들을 가리킨다. Staging Area라고도 이야기를 한다. 워킹 디렉토리는 Head, Index와 달리 실제 파일로 존재하고 있기 때문에 사용자가 편집하기 쉽다.

어떤 파일 C를 만들어서 untracked 상태라면, C는 현재 워킹 디렉토리에만 버전 1로 남은 상태다. 여기서 git add를 하면 인덱스에도 버전 1의 파일 C가 복사된다. 여기서 git commit까지 하고 나면 Index의 내용이 스냅샷으로 저장되고 커밋 객체가 만들어진다. 그럼 HEAD도 이제 버전 1의 파일 C를 가리키게 되는 것이다. 이 상태가 되면, 워킹 디렉터리와 인덱스, HEAD가 내용이 모두 같기 때문에 git status를 했을 때 아무 변경 사항이 없다고 나온다.

 

 

reset을 하기 위해 문서에서는 file.txt를 버전 3까지 수정해서 커밋했다. 

1단계에서는 HEAD만 변경한다. reset --soft 명령을 사용한다. 브랜치를 변경하지는 않지만, 현재 브랜치에서 HEAD가 가리키는 커밋을 변경한다. 이제 HEAD는 9e5e6a4 커밋을 가리키게 된다.

git reset --soft 9e5e6a4

--soft가 아니라 그냥 reset을 하거나 --mixed를 주게 되면 Index를 현재 HEAD가 가리키는 스냅샷으로 업데이트 가능하다.

git reset HEAD~

이렇게 되면 Index가 file.txt의 버전 2가 되는데.. 그러면 Staging Area를 비우는 것이다. 

3단계에서는 워킹 디렉터리까지 변경한다. --hard 옵션을 사용하게 되면 워킹 디렉터리가 Index 상태가 되기 때문에 file.txt가 버전 2 상태로 워킹 디렉터리에 반영된다.

 

커밋 되돌리는 revert

reset 비슷하게 revert가 있는데, revert는 모든 변경사항을 취소하는 새로운 커밋을 "만든다". 

reset은 지금까지의 커밋 이력을 남기지 않고 원하는 시점으로 돌아갈 때 사용하고, revert는 과거로 간다는 커밋을 만들면서 원하는 시점으로 돌아간다는 차이가 있다.

 

 

 

Reference

 

 

728x90
반응형
Comments