GIT

내가 자주 사용하는 git 명령어

iKay 2020. 5. 30. 01:30
반응형

서론

업무 때문에 여러 사람과 함께 git을 사용한지 일 년 정도 된 것 같다. 그 기간 동안 git을 사용하면서 느꼈던 점, 현재 자주 사용하는 커맨드 라인, 앞으로 사용하면 좋을 것 같은 커맨드 라인 등을 남기면서 정리하는 것이 좋을 것 같아 이렇게 쓴다.

 

git은 가볍지만 빠르고 강력하다고 생각한다. 소스코드를 저장하기 위해 git 데이터 크기가 너무 커서 곤란에 빠져본 엔지니어는 거의 없을 것이다. 파일이 달리지지 않으면 새로 저장하지 않고 이전 상태에 대한 링크만 저장하기 때문이다. 조회도 빠른 편인데 이유는 대부분의 명령어가 원격 서버와 통신하지 않고 로컬에서 수행하기 때문이다. 여러 사람이 한 프로젝트를 동시에 병렬적으로 작업할 수 있고 히스토리 조회가 쉽다는 점, 강제로 덮어 쓰지만 않는다면 데이터 무결성이 보장된다는 점이 때문에 git은 강력하다고 생각한다.

 

본래 git은 Linus Tovalds를 주축으로 LIinux 개발 커뮤니티에서 복잡한 Linux 커널 소스코드를 개발하기 위한 버전 관리 도구로써 만들어 졌다. 하지만 git이 점차 보급화 됨에 따라 웹, 모바일, 펌웨어 등의 애플리케이션 소스코드를 일반적인 SW 엔지니에의해 관리하는데도 흔히 사용되게 되었다. 최근에는 인프라를 소스코드로 관리하는 기술(Infrastructure as code)가 많이 도입되고 있는 추세라서 SW 엔지니어 뿐만 아니라 인프라 엔지니어 사이에서도 git이 흔하게 사용되는 추세이다.

 

이처럼 IT 분야에서 일한다면 git을 사용하지 않고 일하기는 어려울 것이다. 스스로도 git을 거의 매일 사용한다. 하지만 늘 사용하는 명령어만 사용하다 보니 가끔은 낯설 때도 있다. 지금도 git에 어떤 기능이 있는지 100% 다 이해 못 하고 있는 것 같다. 이번에 이렇게 글을 쓰게된 계기로 반성할 점은 반성하고 알고 있는 것은 정리하는 좋은 기회가 된 것 같다.

사전 지식

이 부분은 git을 사용하기에 앞서 알고 있어야 할 점이라 생각해서 정리하고 넘어간다.

 

git을 사용하기 위해 우선 알아야 하는 점은 git에는 Working Directory, Index 그리고 HEAD 3 가지 상태가 있다는 것이다. 브랜치를 만들고 작업 후 commit 하기 까지 다음과 같은 과정을 거치게 된다.

1. Working Directory

브랜치를 checkout 하면 HEAD 내용이 Working Directory에 나타난다.

2. Index

Working Directory로 부터 변경 된 파일을 add 하면 Index에 stage 된다.

3. HEAD

HEAD는 현재 브랜치의 최신 커밋을 의미한다. Index의 내용을 commit 완료하면 HEAD는 한 칸 앞으로 이동한다.

위 설명을 도식으로 간단히 나타내면 다음과 같다.

 1.Working           
 Directory      2.Index         3.HEAD

    |                       |                      |
    |               (checkout)              | 
    | <------------------------- |
    |                       |                      |
    |      (add)        |                      |
    | -----------> |                      |
    |                       |   (commit)    |
    |                       |  --------->  |

주로 사용하는 git 명령어

본격적으로 내가 자주 사용하는 git 커맨드 라인을 사용 빈도순으로 나열해 본다.

빈번히 사용

빈번히 사용되는 git 커맨드 라인만 사용할 줄 알아도 혼자 프로젝트를 진행하거나 협업하는데 크게 지장은 없을 것 같다.

1. diff

두 부분의 차이점을 자세히 살펴볼 수 있다.

 

가장 빈번히 사용하는 명령어 인 것 같다. 어떤 기능을 구현 하기 위해, 코드 리뷰를 하기 위해, 내 코드를 master에 merge 하기 전에 코드간 차이점을 자세히 살펴보는 것은 중요하다고 생각하기 때문이다.

 

사실 이렇게 커맨드 라인으로 둘을 직접 비교하기 보다는 IDE, GitHub PR를 통해 간접적으로 더 많이 사용하는 편이다. 어떤 도구를 사용하든 보는데는 큰 차이가 없다.

# Working Directory와 HEAD를 비교. 작업한 것을 Index에 stage 하기 전 변경 조회
$ git diff

# Stage된 Index와 HEAD를 비교
$ git diff --staged

# 서로 다른 브랜치를 비교
$ git diff master topic1

# 커밋 해시로 서로 다른 커밋 비교
$ git diff 3cfd9c0c b5c18eef

2. status

현재 어떤 브랜치인지, 변경된 파일이 무엇인지 볼 수 있다.

 

개인적으로 작업 시작 전 어떤 브랜치인지 확인하기 위해, 작업 하면서 변경된 파일은 무엇인지 중간중간에 확인할 때 자주 사용한다. 또한 merge 후 충돌이 일어났을 때 어떤 파일에 충돌이 일어났는지도 확인 가능하다.

 

사실, zsh를 쓰면 커맨드 라인에 현재 어떤 브랜치인지 바로 나오고, IDE 플러그인을 확장해 사용하면, 마음만 먹는다면? 직접 커맨드 라인을 사용할 일이 없을 수도 있겠다.

# Working Directory, Index, HEAD 간 변경된 파일을 보여준다. 
$ git status

# 결과를 간단히 짧게 보려면 -s 옵션 주면 된다. 
$ git status -s

3. checkout

checkout을 통해 브랜치를 변경할 수 있다.

 

브랜치는 작업하는 컨텍스트라고 봐도 될 것 같다. 보통 모든 작업은 master에서 새로운 브랜치를(예를 들어 topic1) 생성 후 checkout해서 작업을 완료한 후 master에 작업했던 브랜치를(예를 들어 topic1) merge 하는 형태로 이루어 지는 것이 이상적이다. 그것이 hotfix라도 말이다.

# topic1 이라는 이름의 branch를 즉시 생성하고 branch를 변경한다
$ git checkout -b topic1

# 변경된 파일이 있는 경우 브랜치 변경이 바로 되지 않는다. 이 때, -f 옵션을 주면 변경된 사항을 되돌리고 즉시 브랜치를 옮긴다.
$ git checkout -f master

4. add

위에서 언급한 대로 Working Directory에서 Index로 stage 하는데 사용한다.

 

add는 directory에 recrusive하게 되기 때문에 상위 디렉토리를 path로 주면 변경된 것을 모두 Index로 stage 한다. 하지만 key 같은 것은 stage 되지 않게 확인하는 습관은 중요하다. 잘못되어 두 번 일하는 것은 힘들다.

 

보통 변경사항이 있을 때는 commit할 때 -a 옵션을 줘서 add를 생략하지만 merge 후 충돌을 해결해야 할 때는 신중하게 작업을 해야 하기 때문에 파일 하나씩 충돌을 해결하고 add 하는 편이다.

# src/lib/index.js 파일을 Index.js에 stage 한다. 주로 merge 후 충돌을 해결하고 하나씩 add 한다.  
$ git add src/lib/index.js

# 소스 디렉토리에 있는 모든 변경된 파일을 stage 한다.
$ git add .

5. restore & clean

add 한 것을 복구한다.

사실 아직은 괜찮다. 되돌리기 쉽고 부작용이 없고 안전하다. restore를 통해 변경된 파일을, clean을 통해 새로 생성된 파일을 버릴 수 있다.

# Working Directory 상에서 변경 된 파일인 src/lib/index.js를 변경하지 않은 것 처럼 버린다.
$ git restore src/lib/index.js

# 변경된 모든 것을 버린다. 
$ git restore .

# 새로 생성된 파일은 restore 되지 않는다. clean 해주면 된다.

# 우선 --dry-run을 한 다음에 버리려는 파일이 맞는지 확인하고 clean 해준다.
$ git clean --dry-run

# clean은 정말 하려고 하는것잉 맞는지 확인하기 위해 재차 확인하기 위해 -f 명령어가 붙는다. 
$ git clean -f


# add 후 Index에 stage된 파일인 src/lib/index.js를 Working Directory로 내린다.
$ git restore --staged src/lib/index.js

6. commit

Index에 stage 된 것을 HEAD로 옮기고 영속시킨다. 이 HEAD는 이전 HEAD보다 앞서게 되고 branch도 새로 commit된 HEAD로 이동한다.

 

내가 작업했던 내용을 영속 시키는 과정이라 보면 된다. 여기서 부터는 되돌리려면 조금씩 곤란할 때가 있다. 사실 push 하지 않았다면 괜찮을 수도 있다. 그래도 신중하게 commit 하자.

 

종종 commit 하지 말아야 하는 파일(log, key 등)을 포함하거나, 실수로 config 파일을 수정해서 commit 하는 경우를 본 적이 있고 스스로도 가끔 실수하는 편이다. 이런 것을 방지하기 위해 commit 전 한 번더 diff 해보는 습관, commit 단위를 응집력 있고 원자적으로 만드는 것이 중요하다고 생각한다.

# vim 화면이 나오는데 커밋 메시지를 적절히 알아보기 쉽게 작성하면 된다. 
$ git commit 

# add 생략 하면서 커밋 메시지를 작성할 수 있다.   
$ git commit -a -m "Fix error handling"

# 현재 stage된 index 포함, 바로 직전 commit을 합쳐서 다시 commit한다. 
# 주의할 점은 이전 커밋이 다시 커밋되는 것이기 때문에 원격 저장소에 push한 경우 가급적 사용하지 않는 것이 좋다. 
# 로컬에서 실수로 커밋을 수정해야 할 때만 사용하도록 한다. 
$ git commit --amend

7. push

로컬에서 작업한 브랜치, 커밋을 원격 저장소에 전송한다. 원격 브랜치를 삭제한다.

 

push 하기 전에 diff로 반드시 한 번더 검토하자. push 전이라면 비교적 수월하게 실수를 해결할 수 있다.

 

그리고 실수로 정말 중요한 key 같은 것을 올린 상황이 아니라면 force push(-f) 하지 말자. PR에 올린 것을 force push 하게 되면 코드 리뷰 하는 동료들은 힘들어 진다. 그리고 그것을 이미 받았던 동료들도 힘들어 진다. 여러모로 여러 사람이 힘들어지니 하지 말자.

# 로컬 브랜치 변경 사항을 원격 브랜치에 반영한다.
$ git push

# 로컬 브랜치(topic1)를 처음 원격 저장소(origin)로 push하는 경우 -u 옵션을 준다. 
$ git push -u origin topic1

# 원격 저장소(origin) 브랜치 topic1를 삭제한다.
git push -d origin topic1

# force push는 하지말자......
#$ git push -f

8. merge

다른 브랜치를 현재 브랜치에 병합한다.

 

git이 강력한 이유는 동시에 서로 다른 브랜치를 작업한 후 하나의 브랜치로 merge 할 수 있다는 것이라 생각한다. merge 종류로 3-way, fast-forward 등이 있다.

 

3-way는 흔히 master에서 topic을 checkout 한 후 topic에 작업을 끝내고 master에 다시 merge 하는 경우이다. 다시 말해, master 브랜치, topic 브랜치, 그리고 공통된 조상 이렇게 3개 merge 하는 것이다.

 

fast-forward는 현재 로컬 브랜치보다 원격 브랜치가 앞서는 경우 로컬 브랜치가 원격을 따라 가는 경우이다. 나중에 설명하겠지만 pull 하는 것과 fetch&merge 하는 것은 같다.

 

merge를 하는 경우 충돌이 발생할 수 있다. merge를 했는데 충돌이 일어났지만, 지금 충돌을 해결하기 싫어서 되돌리고 싶을 때 --abort를 주면 된다.

# master 브랜치에 topic1 브랜치를 merge 한다. 
# 이런 경우 master가 mainline=1, topic1이 mainline=2가 되는 것도 알아 두면 좋다.
# merge된 것을 revert 할 때 어떤 부모를 따라갈지를 알려줘야 하는데, mainline이 어떤 부모를 말하는 것이다. 
$ git checkout master
$ git merge topic1

# 만약 merge 했는데 충돌이 났고 되돌리고 싶다면 --abort 하면 된다.
$ git merge --abort

9. pull

pull은 원격 브랜치를 fetch 후 로컬 브랜치와 merge 하는 것과 같게 동작한다.

$ git pull

자주 사용

10. fetch

원격 브랜치의 최신 사항을 로컬로 가져온다. 주로 pull을 사용하기 때문에 pull 보다 사용 되는 빈도는 적은 것 같다.

$ git fetch

11. log

commit 히스토리를 확인한다.

 

개인적으로 브랜치 graph를 봐야할 때 제일 많이 사용하는 편이지만 IDE로 보는 것이 편해서 직접 branch graph를 커맨드 라인으로 직접 자주는 조회하지 않는다.

 

어떤 함수에 대한 변경 사항을 봐야할 때, 이 함수는 언제 만들어 졌고, 언제 어떻게 수정 되었는지 히스토리가 궁금할 때가 있다. 이런 경우도 히스토리 추적이 가능하다.

 

그 외 좋은 기능이 많고 정말 무궁무진하지만, 어떤 기능이 있는지 아직 100% 파악하지 못 하고 있고 능숙히 사용하기 어려운 것 같다.

# 브랜치 그래프를 조회한다.  
$ git log --pretty=oneline --graph

# src/lib.js 파일의 sum이라는 함수에 대한 변경사항을 추적한다. 
$ git log -L :sum:src/lib/index.js

종종 사용

12. stash

stash는 영어로 "숨기다, 넣어두다" 라는 뜻이다. 영어 단어에서 유추할 수 있듯이 아직 commit 되지 않은 변경 사항을 임시로 "넣어둘 때" 사용된다.

 

브랜치는 작업하는 컨텍스트라고 볼 수도 있다. 아직 작업하던 내용을 완성하지 못 해 commit 하지 못 했지만 컨텍스를 바꿔야 하는 경우 사용된다. 예를 들어 topic1을 진행하던 중에 commit 하지 못 했지만 hotfix를 처리해야 하는 경우 일 것이다.

 

stash가 유용하긴 하지만 자주 사용되는 편은 아니라서 이 곳에 분류했다.

# 현재 topic1 브랜치에서 작업중 이다. 그런데 갑자기 hotfix를 처리해야 하는 경우가 생겨서 
# commit 하지 못하고 브랜치를 hotfix로 변경해 작업을 해야만 한다.
# 이런 경우 stash 해서 임시로 작업한 내용을 저장한다. 

# -u: untracked file. 새로 만든 파일이 있는 경우 옵션을 준다. 
# -m: (optional) 간단한 메시지를 메모한다.
$ git stash -u -m "tmp topic1" push

$ git checkout hotfix

# hotfix 처리 ...

$ git stash list
stash@{0}: On topic1: tmp-topic1

# git ref로 stash를 pop 한다. pop 하면 stash를 따로 지우지 않아도 자동으로 지워진다. 
$ git stash pop stash@{0}

# topic1을 계속 한다. 

13. rebase

현재 브랜치를 다른 곳으로 옮길 때 주로 사용한다.

# case1
          A--B--C topic1
        /
D--E--F--G master

# topic1 브랜치 작업 중 master의 끝으로 이동시키고 싶은 경우
$ git rebase master

# 그러면 다음과 같이 변한다.

                           A'--B'--C' topic
                         /
D---E---F---G master

# case2
o---o---o---o---o  master
         \
          o---o---o---o---o  topic1
                                          \
                                            o---o---o  topic2

# topic1에서 분기 했지만 topic2를 topic1과 무관하게 master 끝으로 이동 시키고 싶을 경우
$ git rebase --onto master topic1 topic2

# 그러면 다음과 같이 변한다.
o---o---o---o---o  master
        |                       \
        |                         o'--o'--o' topic2
         \
            o---o---o---o---o  topic1

 

혹은 현재 브랜치의 커밋을 수정/합칠 때 주로 사용한다. 이때 -i 옵션을 주고 대화형(interative)으로 하면 편리하다.

# 최근 3개의 커밋에 대해 합치거나 수정하거나 삭제할 부분이 있는 경우 대화형으로 rebase 하면 편리하다. 
$ git rebase -i HEAD~3

# vim 창이 열리고 설명대로 진행하면 된다. 

 

* 이 명령어는 현재 브랜치를 망칠 수도 있으니 브랜치를 복사해서 rebase 하거나 신중하게 해야 한다. 특히 push 했던 브랜치라면 rebase를 하지 않는 것이 좋다.

14. reset

현재 브랜치가 가르키는 커밋을 변경한다. reset 명령어에 대해 확실히 숙지하기 위해 이 문서를 읽어 보는 것을 권장한다. git-scm.com/book/ko/v2/Git-%EB%8F%84%EA%B5%AC-Reset-%EB%AA%85%ED%99%95%ED%9E%88-%EC%95%8C%EA%B3%A0-%EA%B0%80%EA%B8%B0

 

주로 로컬에서 커밋을 잘 못 해서 다시 커밋 하고 싶거나 여러 커밋을 합치고 싶을 때(--soft, mixed), 아니면 다른 커밋 시점으로 되돌아 가고 싶을 때(--hard) 사용한다. 하지만 실수를 하기 쉽기 때문에 너무 남용하지 말자. reset하지 않도록 좋은 commit을 만들면 된다. 다시 강조 하지만 웬만한 실수(key를 커밋한 경우) 아니면 reset 할 필요가 없다.

 

# 최근 2개의 커밋을 reset 하고 싶다. 옵션을 주지 않으면 mixed로 동작 함. 
$ git reset [--soft]

# 현재 브랜치의 모든 커밋이 b0b4da0c9e 커밋 해시 시점에서 시작하게 만들고 싶다. rebase와 달리 모든 커밋이 합쳐진다. 
$ git reset b0b4da0c9e


# (응용) 기존 develop 브랜치 히스토리는 유지하면서 앞으로의 커밋은 master와 같게 만들고 싶다. 
$ git checkout develop
$ git reset --hard master # develop 브랜칫가 master를 가르키게 한다. 서로 다른 부분은 버린다.
$ git reset origin/develop # 본래 origin/develop 끝으로 이동한다. 
$ git commit -m "Reset develop to master by force" # develop을 master와 형상을 같게 하면서, master와 origin/develop이 달랐던 부분을 커밋하는 것이다.

# 실제로 이 이후에 master와 develop을 diff 해보면 같음을 알 수 있따. 
$ git diff develop master

 

* 이 명령어는 현재 브랜치를 망칠 수도 있으니 브랜치를 복사해서 reset 하거나 신중하게 해야 한다. 특히 push 했던 브랜치라면 reset을 하지 않는 것이 좋다.

 

가끔 사용

15. cherry-pick

특정 커밋을 골라서 현재 브랜치로 가져올 때 사용한다.

 

rebase는 현재 브랜치를 다른 브랜치 끝으로 이동시키는 반면, cherry-pick은 반대로 커밋을 가져온다.

 

$ git cherry-pick {commit hash}

 

16. revert

git을 과거로 되돌리지만 reset 처럼 커밋 history를 지우지는 않는다.

# -m == --mainline
# -m 1: merge를 요청한 브랜치(근본)
# -m 2: merge 시키기 위해 가져온 브랜치
$ git revert -m 1 HEAD

 

결론

SW 엔지니어는 여러 사람과 협업을 하는 경우가 많고, SW는 점점 복잡도가 지수적으로 증가하기 때문에 소스 코드의 버전 관리를 잘 하는 것이 매우 중요하다고 생각한다. 특히 요즘 VCS(Version Control System)으로 git이 널리 사용되고 있기 때문에 능숙하게 사용하는 것이 유리하다고 생각한다.

 

앞으로는 자주 사용하는 git 커맨드 라인 수를 늘리도록 더 노력해야 겠다.

반응형