[Git] git의 버전관리

hyunsooo·2022년 10월 18일
0

Git basis

Git의 본질

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
마지막 버전의 내용이나 커밋 메세지를 수정하고 싶을때 사용한다.
주의할점은 마지막 버전(AA)을 직접 바꾸는게 아니라 복제를 한 후(AA^`) 복제 버전을 수정하여 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 idparent와 현재 내용의 hash조합으로 생성된다.

git에서는 master 또는 main이라 불리는 브랜치가 있는데 master마지막 버전을 가르키게 된다.

HEAD라는 포인터도 등장하는데 기본적으로 master를 가르키고 있다.
HEAD -> master의 의미는 HEADmaster를 가르키고 있으며 master마지막 버전을 가르키고 있다.


checkout

각각의 버전은 그 버전이 만들어진 시점에 stage area의 스냅샷이다. 이런 특징 때문에 우리는 특정 시점의 스냅샷으로 돌아갈 수 있게 된다. 결국 우리는 HEAD를 옮겨 특정 시점으로 돌아갈 수 있으며 아래의 명령어로 사용할 수 있다.

  • git checkout <commit id>

  • master : 마지막 버전

  • HEAD : 현재 버전

git logparent를 타고 보여주는 것이기 때문에 checkout을 통해 과거로 돌아가면 git log로 보여지는게 한계가 있기 때문에 당황할 수 있다.
이럴때는 git checkout master로 마지막 버전으로 돌아오거나 git log --all 옵션으로 과거에서도 모든 log를 볼 수 있다.
추가적으로 git log --all --graph를 사용하면 갈라진 브랜치의 그래프를 그려준다.


[ git graph extention ]
[ command line ]

details

위에서 설명한 부분을 조금 더 자세하게 하게 설명하면 master브랜치이고 HEAD포인터 이다.

현재 버전에서 새로운 버전을 만든다면 HEAD가 가르키는 브랜치가 새로 만든 버전으로 이동하게 된다.

HEAD -> master인 상태에서 새로운 버전을 만들면 브랜치인 master가 새로운 버전으로 이동하게 되는 방식이다.

[ HEAD가 master를 가르키는 상태 ]
[ HEAD가 직접 버전에 접근한 상태 ]
[ HEAD가 직접 접근 후 commit한 상태 ]

그렇다면 git checkout commit idHEADmaster가 아닌 마지막 버전에 직접 접근하고 새로운 버전을 만들게 되면 master는 새로운 버전으로 이동하지 않고 HEAD가 직접 움직이게 되는 상황이 벌어진다. 이 상황을 detached HEAD state라고 말한다.

위와 같은 상태에서 git checkout master를 하게 되면 마지막 버전이 아닌 중간에 끊긴 master로 돌아가게 된다. 이 상태에서 git log를 하게되면 마지막 버전에 대한 log가 나타나지 않아 당황할 수 있다. git의 특성상 어떤 버전도 지우지 않기 때문에 복구는 가능하지만(git reflog) 지식이 필요하다.

detached HEAD state
위험해 보이는 위의 상태를 만들어 놓은 이유는 이 기능을 적절히 사용할 수 있기 때문이다. 어떤 실험적이고 혁신적인 작업을 하다 실패를 했을때 깔끔하게 버릴 수 있기 때문이다.


Branch

만약 실험적인 작업과 일상적인 작업을 매일 반복해야 한다면 그때마다 commit id를 찾아서 checkout을 하기에는 생각만 해도 너무 귀찮다. 이런 commit id 지옥을 해소해주기 위해 나온게 Branch이다.

  • git branch <branchname>


새로운 브렌치를 생성했다면 git checkout <commit id>가 아닌 git checkout <branchname>으로 간편하게 이동할 수 있다.

실험이 성공적으로 끝났다면 mastertest를 병합해야 할 수 있다.


Merge

병합을 하기 위해서는 아래의 HEADmaster를 가르키고 있어야 한다.
이 상태에서 Merge를 수행해야 하는데 그 명령어는 아래와 같다.

  • git merge <branchname>

[ git graph에서 merge 하기 ]



[ 결과 ]




위의 결과 상태에서 새로 만들어진 버전의 parent는 work 10과 test 1 둘다 이며 master브랜치가 따라가게 된다. 물론 HEADtest를 가르키게 했다면 test브랜치가 따라게 되므로 방향이 매우 중요하다.


cherry pick(부분 병합)

test의 최신버전이 아니라 특정 버전만을 master에 merge하고 싶을때 사용하는 명령어이다. 즉 다른 브랜치 위에 있는 커밋을 선택적으로 내 브랜치에 적용시킬 때 사용하는 명령어이다.

  • git cherry-pick <commit id>

revert

이미 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가 된다.


diff

파일을 수정하고 stage로 올리지 않을 상태에서 기존의 내용과 비교하려고 할때는 아래의 명령어로 사용가능하다.

  • git diff

Tracked vs. Untracked

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에 나온다.)


.gitignore

만약 commit되면 안되는 파일이 존재한다면 git status에 매번 그 파일이 존재하게 된다. 또는 git add .과 같은 행동을 했을때 이 파일도 같이 추가가 될 수 있다. 이런 상황을 방지하기 위해 .gitignore파일을 생성 후 이 파일에 추적하기 싫은 파일명, 폴더명을 작성 후 저장한다. 마지막으로 .gitignore파일도 add해주면 원하는 특정 파일을 추적하지 않게 할 수 있다.


reset

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

너무 긴 옵션으로 매번 사용하기 번거롭다면 사용자 지정 alias로 지정할 수 있다.

  • git config --global alias.[custom alias] "log --oneline --all --graph"

위 의 alias는 git log의 많은 옵션을 git [custom alias]로 간단히 실행시켜줄 수 있다.

삭제를 하는 방법은

vi ~/.gitconfig를 열어 alias내용을 수정함으로 삭제할 수 있다.


Conflict

<상황 1> 각각의 브랜치에서 서로 같은 파일의 다른 부분을 수정한 경우

위와 같은 상황에서는 문제 없이 merge가 작동한다.


<상황 2> 각각의 브랜치에서 서로 같은 부분을 수정한 경우

충돌이 일어나게 된다. 충돌이 일어난 파일의 내용을 보면 아래와 같다.

<<<<<<<<< HEAD (Current Change)
HEAD가 가르키는 브랜치 변경사항
=========
병합하려는 브랜치의 변경사항
>>>>>>>>> exp (Incoming Change)

위 상태에서 한 브랜치를 수용할지 둘다 병합하여 수용할지 선택 및 직접 수정 후 충돌을 해결 했다는 명령어로 add를 사용하게 된다. 위의 add의 3가지 의미중 마지막 의미를 의미한다. 그 후 commit을 하면 충돌을 성공적으로 해결할 수 있다.


3 way merge

두 브랜치의 공통의 조상을 Base라고 부른다.
현재 두 브랜치의 내용만 두고 비교한다면 대부분의 내용에서 충돌이 나야할 것 이다. 따라서 git은 공통의 조상인 base를 기준으로 수정내용을 비교하여 충돌을 찾아낸다.


rebase

협업에서 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별로 충돌을 해결하는 단점이 있지만 조작된 상황의 버전을 생성해 버전 히스토리를 한줄로 표시할 수 있게 된다.

profile
지식 공유

0개의 댓글