git을 협업의 툴로 이해하는 것 보다 그 본질은 버전관리라는 것을 명심해야 한다. 버전관리 시스템이 가능해야 백업이 가능하고 백업이 가능해야 협업이 가능해 진다.
하나의 프로젝트를 진행하다 보면 다양한 기능들이 추가되거나 수정되고 그때마다 버전 관리가 필요하다. 또한 새로운 버전에 버그가 생기는 경우 과거의 버전으로 돌아가야 할 경우도 발생 할 수 있다. 이 모든 것을 편하게 다루기 위한 툴이 git이다. 따라서 git은 우리에게 어려운 존재가 아닌 고마운 존재로 다가와야 되고 이번 버전 관리에 대한 기본적인 개념을 알고가자!
버전을 관리하려면 기본적으로 누가 버전을 만들었는지, 연락처는 어떻게 되는지에 대한 설정이 필요하며 아래와 같이 설정할 수 있습니다.
git init
: 저장소 생성(초기화)
git config --global user.name "My name"
git config --global user.email "My email"
--global
: 해당 컴퓨터의 전역변수로 설정
git init
을 통해 repository를 생성하게 되고 working directory에서 작업을 한 후 add
를 통해 전체 또는 일부분의 변경사항을 stage area로 이동시킨 후 해당 버전의 간단한 설명을 message
에 담고 commit
을 사용하면 repository에 작업물이 copy가 된다.
git add <file name>
git commit -m "Message"
git commit --amend
로 새로운 버전이 아닌 최신버전에 덮어쓰기를 수행할 수 있다.git commit --amend
마지막 버전의 내용이나 커밋 메세지를 수정하고 싶을때 사용한다.
주의할점은 마지막 버전()을 직접 바꾸는게 아니라 복제를 한 후() 복제 버전을 수정하여 HEAD가 가리키는 방향을 바꾸게 된다.
우리가 느끼는건 직접 수정된거 같지만 실제로는 commit id가 바뀌는 작업이다.
그렇다면 commit
을 반복할 수록 어떤 버전이 있나 확인을 하고 싶을텐데 2가지 방법이 있다.
VScode를 사용하는 경우 Git Graph extension을 설치한다.
command line에서는 git log
명령어로 확인할 수 있다.
git log --oneline
: 간단하게 볼 수 있다.commit id
는 내용을 가지고 만들어지는 id
이기 때문에 commit id
가 같다면 완전히 똑같은 내용이라는 것을 보증한다. work 1이후 work 2를 commit하게 되면 work 2의 parent는 이전 work1의 commit id
를 가르키고 현재의 commit id
는 parent와 현재 내용의 hash조합으로 생성된다.
git
에서는 master
또는 main
이라 불리는 브랜치가 있는데 master
는 마지막 버전을 가르키게 된다.
HEAD
라는 포인터도 등장하는데 기본적으로 master
를 가르키고 있다.
HEAD -> master
의 의미는 HEAD
가 master
를 가르키고 있으며 master
는 마지막 버전을 가르키고 있다.
각각의 버전은 그 버전이 만들어진 시점에 stage area
의 스냅샷이다. 이런 특징 때문에 우리는 특정 시점의 스냅샷으로 돌아갈 수 있게 된다. 결국 우리는 HEAD
를 옮겨 특정 시점으로 돌아갈 수 있으며 아래의 명령어로 사용할 수 있다.
git checkout <commit id>
master : 마지막 버전
HEAD : 현재 버전
git log
는 parent를 타고 보여주는 것이기 때문에checkout
을 통해 과거로 돌아가면git log
로 보여지는게 한계가 있기 때문에 당황할 수 있다.
이럴때는git checkout master
로 마지막 버전으로 돌아오거나git log --all
옵션으로 과거에서도 모든 log를 볼 수 있다.
추가적으로git log --all --graph
를 사용하면 갈라진 브랜치의 그래프를 그려준다.
위에서 설명한 부분을 조금 더 자세하게 하게 설명하면 master
는 브랜치이고 HEAD
가 포인터 이다.
현재 버전에서 새로운 버전을 만든다면 HEAD가 가르키는 브랜치
가 새로 만든 버전으로 이동하게 된다.
HEAD -> master
인 상태에서 새로운 버전을 만들면 브랜치인 master
가 새로운 버전으로 이동하게 되는 방식이다.
그렇다면 git checkout commit id
로 HEAD
를 master
가 아닌 마지막 버전에 직접 접근하고 새로운 버전을 만들게 되면 master
는 새로운 버전으로 이동하지 않고 HEAD
가 직접 움직이게 되는 상황이 벌어진다. 이 상황을 detached HEAD state
라고 말한다.
위와 같은 상태에서 git checkout master
를 하게 되면 마지막 버전이 아닌 중간에 끊긴 master
로 돌아가게 된다. 이 상태에서 git log
를 하게되면 마지막 버전에 대한 log가 나타나지 않아 당황할 수 있다. git의 특성상 어떤 버전도 지우지 않기 때문에 복구는 가능하지만(git reflog
) 지식이 필요하다.
detached HEAD state
위험해 보이는 위의 상태를 만들어 놓은 이유는 이 기능을 적절히 사용할 수 있기 때문이다. 어떤 실험적이고 혁신적인 작업을 하다 실패를 했을때 깔끔하게 버릴 수 있기 때문이다.
만약 실험적인 작업과 일상적인 작업을 매일 반복해야 한다면 그때마다 commit id를 찾아서 checkout을 하기에는 생각만 해도 너무 귀찮다. 이런 commit id 지옥을 해소해주기 위해 나온게 Branch이다.
git branch <branchname>
새로운 브렌치를 생성했다면 git checkout <commit id>
가 아닌 git checkout <branchname>
으로 간편하게 이동할 수 있다.
실험이 성공적으로 끝났다면 master
가 test
를 병합해야 할 수 있다.
병합을 하기 위해서는 아래의 HEAD
는 master
를 가르키고 있어야 한다.
이 상태에서 Merge
를 수행해야 하는데 그 명령어는 아래와 같다.
git merge <branchname>
위의 결과 상태에서 새로 만들어진 버전의 parent는 work 10과 test 1 둘다 이며 master
브랜치가 따라가게 된다. 물론 HEAD
를 test
를 가르키게 했다면 test
브랜치가 따라게 되므로 방향이 매우 중요하다.
test의 최신버전이 아니라 특정 버전만을 master에 merge하고 싶을때 사용하는 명령어이다. 즉 다른 브랜치 위에 있는 커밋을 선택적으로 내 브랜치에 적용시킬 때 사용하는 명령어이다.
git cherry-pick <commit id>
이미 commit한 내용을 취소한 새로운 버전을 만들 때 사용한다.
-git revert <commit id>
revert 관점에서 3way merge
A -> B -> C
위의 상황에서 revert B를 하게 되면 A -> B로가는 과정을 취한 상태를 C와 비교하여 새로운 버전을 만든다.
A : 1, 2, 3, 4
B : 1, M2, 3, 4
C : 1, M2, 3, M4
revert로 만든 버전은 1,2,3,M4가 된다.
파일을 수정하고 stage로 올리지 않을 상태에서 기존의 내용과 비교하려고 할때는 아래의 명령어로 사용가능하다.
git diff
tracked 상태는 버전관리의 대상이 되는 상태이며 untracked는 그렇지 않은 상태이다. 버전관리의 상대가 되려면 add
로 tracked상태로 변환할 수 있다.
지금까지 git add -> git commit
의 2가지 과정으로 버전을 관리했는데 이 과정을 한번에 할 수 있다.
git commit -a -m <message>
여기서 -a
옵션은 auto adding 옵션인데 주의할 점은 기존의 한번이라도 add
를 한 상태 즉 tracked상태인 파일이여야 적용할 수 있는 옵션이다.
실제 환경에서는 commit하면 안되는 내용이 자동으로 commit
되는것을 방지하기 위해 tracked와 untracked를 나누어 관리하게 된다.
add의 의미
commit 대기 상태를 만든다.
untracked를 tracked로 만든다.
충돌을 해결했다는 의미 (아래의 conflict에 나온다.)
만약 commit되면 안되는 파일이 존재한다면 git status
에 매번 그 파일이 존재하게 된다. 또는 git add .
과 같은 행동을 했을때 이 파일도 같이 추가가 될 수 있다. 이런 상황을 방지하기 위해 .gitignore파일을 생성 후 이 파일에 추적하기 싫은 파일명, 폴더명을 작성 후 저장한다. 마지막으로 .gitignore파일도 add
해주면 원하는 특정 파일을 추적하지 않게 할 수 있다.
checkout은 HEAD를 옮기고 reset
은 HEAD가 가르키는 브랜치를 옮긴다.
git에서 reset
은 삭제이자 복원의 역할을 한다.
A라는 커밋이 후 B를 커밋하는 상황이다. 이때 master는 B를 가르키고 있고 B의 작업을 삭제하고 싶다면 master를 A로 이동시켜 B가 삭제되는 효과를 볼 수 있다.
reset
이 삭제이자 복원이라는건 reset B를 복원하는 역할도 하기 때문이다.
git reset --hard <commit id>
git reflog
와 같이 사용해 commit id로 복원 및 삭제가 가능
--hard, --soft, --mixed
옵션에 대해서는 document를 참고 한다.
attached 상태에서는 branch의 이동이지만 dettacked 상태에서는 checkout과 같은 효과이다.
추가정보
reset
명령어로 branch를 옮김으로 merge와 같은 작업도 되돌릴 수 있다.
git reset <file>
로 file을 unstaged상태로 되돌린다.
git reset HEAD <file>
로 add를 취소할 수 있다.
너무 긴 옵션으로 매번 사용하기 번거롭다면 사용자 지정 alias로 지정할 수 있다.
git config --global alias.[custom alias] "log --oneline --all --graph"
위 의 alias는 git log
의 많은 옵션을 git [custom alias]
로 간단히 실행시켜줄 수 있다.
삭제를 하는 방법은
vi ~/.gitconfig
를 열어 alias내용을 수정함으로 삭제할 수 있다.
<상황 1> 각각의 브랜치에서 서로 같은 파일의 다른 부분을 수정한 경우
위와 같은 상황에서는 문제 없이 merge
가 작동한다.
<상황 2> 각각의 브랜치에서 서로 같은 부분을 수정한 경우
충돌이 일어나게 된다. 충돌이 일어난 파일의 내용을 보면 아래와 같다.
<<<<<<<<< HEAD (Current Change)
HEAD가 가르키는 브랜치 변경사항
=========
병합하려는 브랜치의 변경사항
>>>>>>>>> exp (Incoming Change)
위 상태에서 한 브랜치를 수용할지 둘다 병합하여 수용할지 선택 및 직접 수정 후 충돌을 해결 했다는 명령어로 add
를 사용하게 된다. 위의 add
의 3가지 의미중 마지막 의미를 의미한다. 그 후 commit
을 하면 충돌을 성공적으로 해결할 수 있다.
두 브랜치의 공통의 조상을 Base라고 부른다.
현재 두 브랜치의 내용만 두고 비교한다면 대부분의 내용에서 충돌이 나야할 것 이다. 따라서 git은 공통의 조상인 base를 기준으로 수정내용을 비교하여 충돌을 찾아낸다.
협업에서 3way merge를 하다보면 프로젝트의 버전이 얽혀 관리가 힘들어지게 된다. rebase는 관리가 쉽도록 단순하게 바꿔주는 역할을 한다. merge와 rebase의 결과는 같지만 merge는 실제 상황을 보여주지만 복잡하고 rebase는 조작된 상황이지만 단순(merge커밋없이 한줄)해진다.
rebase가 되는 과정은 BASE를 기준으로 현재 master 커밋까지 비교하며 한줄로 생성하게 된다.
BASE ==> m1 ==> m2 ==> m3(master)
==>test1 ==> test2(test)
예를들어 test2의 브랜치를 master에 rebase하고 싶으면 아래와 같이한다.
git checkout test
git rebase master
상황은 아래와 같다.
m3와 test1(Base: BASE) -> t1'
m3와 test2(Base: t1)->t2'
BASE ---> m1 -----> m2 -----> m3 -> -> t1' -> t2'(master, head->test)
마지막으로 master를 최종 버전으로 이동시키고 rebase가 끝이난다. rebase는 해당 브랜치의 commit별로 충돌을 해결하는 단점이 있지만 조작된 상황의 버전을 생성해 버전 히스토리를 한줄로 표시할 수 있게 된다.