'~에 대한 모든 것'이란 말은 너무 오만하다. 하이에크의 '구조적 무지'를 빌리자면, 나는 무엇을 모르는지 조차 모르기 때문이다. 그러니 오늘 작성하게 될 내용은 '내가 아는 git에 대한 모든 것'이다. 아무튼 글을 좀 작성해보자.
git을 정의하자면 '파일의 버전관리 시스템' 정도로 얘기할 수 있다. 말 그대로 현재 내 파일/작업의 상태를 꾸준히 기록하기 위한 시스템 정도로 생각할 수 있다.
위의 정의에 초점을 두어 이야기를 전개해 나가고자 한다.(~한 명령어가 있다라는 소개보다는 어떤 맥락에서 우리가 명령어를 사용하게 되는가에 초점을 두어 글을 전개해 나가고자 한다. 전자의 내용은 이미 수백개의 글에 또옥같이 있을테다.)
우리가 파일을 어딘가에 기록하고자 한다. 우리는 이 말에서 두개의 공간을 떠올릴 수 있다. 작업 공간 과 저장 공간 이다. 하지만 실제로 깃은 세 개의 공간으로 이루어져 있다. 그럼 하나의 공간은 무엇을 위한 공간인가?
나의 오늘 일과에 빗대어 생각해보자. 나는 오늘 빨래, 설거지, 청소, 독서, 영화 보기 총 5개의 일을 했다. 이를 내 일기장에 기록하고자 한다. 막상 기록하려 하니 5개의 일을 모두 나누어 기록하기 보단 같은 성질의 기록은 묶어서 기록하고자 한다. 그래서 나는 빨래, 설거지, 청소를 묶어 '집안일' 로 기록하였고, 독서와 영화보기는 '여가 생활' 로 기록 하였다.
위의 예시에서 작업 공간과 저장 공간 사이에 공간이 하나 더 필요함이 느껴지길 바란다. 말하고자 하는 바는 요리하기 전 도마와 같은 역할을 하는 공간이 필요하다는 것이다. 정리하자면 깃은
로 구성되어 내가 작업하는 공간, 1차 파일 기록 공간, 최종 저장 공간으로 구분 된다.
이제 파일을 기록해보자.
나는 Working Directory 에서 한 묶음으로 올리고자 하는 파일들을 staging area 로 넘겨주어야 한다. 이 때 사용하는 명령어가 git add (파일 이름) 이다. Staging area 에 기록하고자 하는 파일을 전부 올렸다면 이제 스냅샷을 남기기 위해 Local Repository 최종적으로 넘겨주어야 한다. 이 때 사용하는 명령어가 git commit 이다. 이 때 보통 git commit -m "남기고자 하는 메세지" 로 넘겨 어떤 작업이 이루어졌는가를 올려준다.
조금 더 구체적으로 이야기를 해보자.
git 에 올라간 파일은 크게 Tracked 와 Untracked 의 두 가지의 상태로 관리가 된다. 두 상태는 git add 를 해주었는가 아닌가에 따라 달라지는데, 의미는 staging area 에서 'git add' 명령어를 통해 파일이 들어온 순간부터, 그 파일의 변화를 계속 추적하기 때문에 나누어진 상태이다. 그리고 Tracked 상태는 Unmodified, Modified, Staged 로 구분된다.
현재 커밋이 어떻게 저장이 되어 있는가를 먼저 보자
위 그림은 현재 내 프로젝트의 커밋 상태이다. 총 5개의 커밋 메세지를 볼 수 있고 각 커밋에 id 로 해시값을 부여하여 관리되고 있음을 볼 수 있다.
혹시 우리가 작업 중에 현 작업이 마음에 들지 않거나, 모종의 이유로 이 전의 버전을 사용해야 한다면 이 커밋 id 를 통해 그 때의 작업상태로 돌아갈 수 있다.
이떄 사용하는 명령어가 git reset 과 git revert 이다.
기본적인 명령 형태는 git reset (option) commit-id 이다. 동작의 의미는 'commmit-id 를 가진 commit 의 버전 상태로 돌아가라' 이다. 이 떄 option 이 무엇인가에 따라 버전 상태로 어떻게 돌아 갈 것인가가 결정된다.
공통적으로는 과거의 커밋으로 돌아가게 되면 그 이후의 커밋들은 모두 사라지게 된다. 정리하자면 아래와 같다.(사실 커밋이 사라지는 건 아니다. 이는 뒤의 브랜치와 함께 다시 설명하겠다.)
git revert 의 경우 위의 명령과 달리 현재 있는 커밋들에는 영향을 미치지 않고, 과거의 특정 커밋을 취소하는(즉 정반대의 작업을 하는) 커밋을 새로 넣게 된다.
여기까지 하면 기본동작은 다 익혔다. 이제 협업을 위한 명령어인 branch 에 대해 적어보겠다.
우리가 프로젝트를 진행하다보면 하나의 흐름으로만 작업하는 경우는 흔치 않을테다. 예를 들어 유료버전과 무료버전을 구분해서 작업을 하거나 다크모드 설정에 대한 페이지를 따로 구현할 수도 있다. 이런 경우가 한 나무에서 여러가지로 뻗어 나가 작업을 하는 듯한 모양새를 닮았다하여 branch 라는 개념을 도입하게 된다. 아무 설정도 해주지 않는다면 우리는 현재 'master'브랜치에 올라가 있을 것이다. 만약 현재의 커밋 상태에서 새로운 가지로 갈라지고 싶다면
git branch branch-name
을 통해 새로운 브랜치를 만들 수 있다. 브랜치간의 이동은
git checkout branch-name
을 통해 이동할 수 있다.
이 브랜치간의 이동을 정확히 이해하기 위해서, 아래의 사진을 보자
현재 다섯개의 커밋을 올려둔 상태이다. 제일 마지막 커밋 (제일 위 커밋) 의 제일 오른쪽을 보면 ( HEAD -> master ) 를 볼 수 있다. 의미 설명은 조금 미루고 일단 이 상태에서 새로운 브랜치를 판 다음 옮겨가 보겠다.
위의 그림에는 이제 (HEAD -> test-branch, master) 라고 써져있는 것을 볼 수 있다. 조금 애매하니 현재 상태에서 커밋을 하나 추가한 후 기록을 보겠다.
자 이제 뭔가 변화가 뚜렷하다. 두가지 특징을 볼 수 있다
정리해서 말하자면, 현재 내 위치는 HEAD 가 말해준다. 여기서 현재 내 위치는 내가 올라 있는 코드의 흐름 즉 branch 를 의미한다. 그리고 여기서 현재의 내 커밋 상태는 브랜치가 가리키고 있는 최신 커밋이 된다. 그림으로 나타내면 아래와 같다.
만약 여기서 master 브랜치로 옮긴후 commit 을 새로 만들게 된다면, 아래와 같이 갈라지게 된다.
실제로 깃을 실행시켜보면 아래와 같이 나타난다.
하나만 더 해보겠다. 현재 마스터 브랜치에서 4번째 커밋으로 reset 을 하게 되면 어떻게 될까? 4번째 커밋은 test-branch 에도 있는 커밋이기에 영향을 미칠 수 있지 않을까 싶다. 작업결과는 아래와 같다.
위에서 보이듯 5번째 커밋이 사라지고 4번째 커밋 위로 오른 것을 볼 수 있다.
놀랍게도 다섯 번째 커밋이 여전히 남아있음을 볼 수 있다. 사실 앞에서도 언급하길 reset 을 하여도 커밋은 사라지지 않는다. 정리하자면 HEAD는 브랜치를 가리키는 포인터 이고, 커밋 위에서 일어나는 추가/삭제의 동작은 브랜치가 옮겨 다니는 과정이라 생각할 수 있다. (checkout 과 reset이 어떻게 다른 동작을 하는가를 생각 할 수 있다.)
마지막으로 정말 중요한 동작 중 하나인 브랜치 합치기에 대해 얘기해보려 한다.
프로젝트를 진행하면서 이 버전, 저 버전 작업을 끝내고 최종적으로 코드를 합쳐야 한다고 하자. 이 때 사용하게 될 명령어는 git merge 이다.
이렇게 내용이 수정되는 것을 볼 수 있다. 그리고 현재 커밋의 상태를 보면
이렇게 되어 현재의 커밋을 두개의 브랜치가 하나의 커밋을 가리키고 있는 것을 볼 수 있다. 사실 merge 는 이렇게 쉬운 과정이 아니다. 가장 잦게 있는conflict 이슈를 보고 글을 마무리 하려한다.
아래와 같이 각 브랜치의 파일을 수정해보았다.
이 상태에서 master 브랜치로 test-branch를 끌고와 병합하려 한다면 어떻게 될까? 이 때 깃은 추적하고 있던 마지막 문장이 서로 다르게 수정된 것을 알아채고 "어떻게 하란거냐!" 라는 메세지를 보내게 된다.
이를 해결하는 방법은 원래의 코드를 살펴보면 알 수 있다.
위와 같이 깃에서 원래의 코드와 병합하려는 코드를 보여주어 어떻게 수정할 것인지에 대해 물어본다. 그래서 이를 수정 후 다시 커밋을 넣게 되면 머지 커밋이라는 새로운 커밋을 넣을 수 있게 된다.
사실 이 이후에 외부 레퍼지토리(사실 레포티토리가 뭔지에 대해서도 얘기를 안했다)와 소통하기, 현업에서 자주 쓰이는 git flow 에 대한 것 까지를 커버하려 했다. 그러나 예상했던 것과 달리 글이 너무 길어졌다. 쉬어 가고 싶으니 이번 편은 <내가 아는 깃에 대한 모든 것 1> 로 하고 <내가 아는 깃에 대한 모든 것 2>에서 나머지를 다루도록 하겠다,,,