git 사용하기

 

쉬다가 문득 git 사용법도 자세히 찾아보고 정리해봐야겠다는 생각을 했다.

원래 무언가를 공부할때 하나부터 열 끝까지 자세히 찾아보고 시작하는데, 프로그래밍이나 관련 툴을 공부할때는 항상 내가 당장 필요한 것들만 수박 겉핥기식으로 찾아보고 만다. 완벽주의적으로 공부하다가 질려버려서 좌절해본 몇번의 경험 때문인 듯하다.

어쨌든, 때문에 git의 모든 기능을 써 보지 않아 모든 기능을 알지 못한다. 언제나 그렇듯, 이 글도 역시 내가 알게 된 것들을 다시 잊어버리기 전에 기록해두고 필요할때 쉽게 꺼내보고자 하는 목적으로 작성한다. 또한 새로운 git기능을 배우는 데로 하나하나 추가해나가고자 한다.

git 간편 안내서backlog git 입문를 참고했지만 무엇보다 git Documentation이 아주 자세하여 잘 참고했다.


git 이란

결국 버전관리 시스템이겠다. 다만 기존의 VCS와 다른 점이 좀 있다.

DVCS: 분산버전관리 시스템

우선 git은 DVCS(분산 버전관리 시스템)이다. git 이전의 많은 VCS가 중앙서버가 버전을 직접 관리하고 사용자가 중앙서버로부터 체크아웃후 수정후 다시 체크인 하는, 중앙집중형 VCS (CVCS)였다. 이에 반해 git과 같은 DVCS는 CVCS와 같이 중앙서버에서 모든 파일을 관리하지 않는다. clone을 통해 각 사용자는 다른 저장소로부터 히스토리를 포함한 모든 파일을 복제해 자신만의 로컬저장소를 가질 수 있으며 필요에 따라 다른 원격저장소로부터 데이터를 갱신하거나 또는 원격저장소로 발행할 수 있다. 리눅스 커널 개발 협업도구로 사용을 목적으로 개발된 것을 생각하면 고개가 끄덕여진다.

Data as stream of file system snapshot

기존의 다른 버전관리 시스템이 버전이 바뀔때마다 파일의 변화를 시간 순으로 관리하면서 이를 파일 집합으로 관리해왔다. 이에 반해 git은 파일집합이 아닌 버전별로 (시간순으로) 관리한다. 이렇게 데이터에 대한 새로운 접근 방식은 Git Branch 를 사용할 때 빛을 발한다.

또한 이렇게 데이터를 접근하기 위해 데이터를 파일시스템 snapshot의 stream으로서 취급한다. 때문에 그 크기가 아주 작고 변경사항이 발생할때마다 git은 최소의 부하만이 걸린다.

Hash의 사용

위와같이 git에서는 파일시스템의 snapshot으로 각각의 데이터가 저장되는데 이런 모든 데이터를 저장하면서 checksum을 계산하고 이 checksum으로 데이터를 관리한다. git log명령어로 로그를 살펴봐도 commit들이 SHA-1 Hash로 식별되고, .git폴더 아래 objects를 확인해봐도 모든 파일들이 SHA-1 Hash로 식별됨을 확인해볼 수 있다. SHA-1 해시는 40자리의 16진수를 사용한다.

데이터의 세 가지 상태 : Modified / Staged / Committed

git 저장소에서 추적되는 데이터는 크게 3가지 상태로 구분할 수 있겠다.

  • Modified: 데이터가 수정되었으나 아직 git에 의해 저장, 등록되지 않음.
  • Staged: (수정된 데이터 중 commit 예정인 데이터가) git index에 등록됨.
  • Committed: 데이터가 로컬 데이터베이스(HEAD)에 저장됨.

areas

Stashing : 잠깐 보관해두기

각 개념들을 명확히 이해하기 전 staged와 stashed를 혼동했었다. stash는

변경사항을 확정짓기(commit) 전에 다른 branch로 전환하여 작업해야 할 경우, stash 스택에 잠시 보관하였다가 다른 branch로 전환해 작업후 다시 돌아오면 stash 스택에서 다시 꺼내어 사용할 수 있다.

설치 및 최초 설정

sudo apt-get install git
git --version
git --help
git config --global user.name "fennecfox38"
git config --global user.email fennecfox38@gmail.com
git config credential.helper store

git config으로 user 정보를 지정해줄 수 있는데, 각 git 저장소 별로 지정해 줄 수도 있고, --global옵션을 통해 전역으로 지정해 줄 수도 있다.

git config credential.helper store를 통해 최초 push시에만 비밀번호를 물어보고, 이후 인증없이 사용가능하다. (비밀번호가 저장되어 있으므로.)

저장소

git repository, 저장소가 있어야 git을 사용하고 뭐라도 하겠다. 로컬에서 저장소를 만들 수도 있고, 다른 저장소로부터 복제할 수도 있다.

저장소 만들기

mkdir new_repo
cd new_repo
git init

이렇게 저장소를 생성할 폴더에서 git init 명령어를 실행하면 새로운 git 저장소가 만들어진다.

저장소 받아오기

현재 폴더 아래에 다음과 같이 git clone 명령어를 사용해보자.

git clone https://github.com/fennecfox38/fennecfox38.github.io.git

# 로컬 저장소
# git clone D:/Project/SampleRepository

# 원격 저장소
# git clone https://github.com/username/SampleRepository.git

현재 경로 아래에 해당 저장소 이름의 디렉토리가 생성되고, 그 안에 저장소가 그대로 복제된다.

원격 저장소 추가하기

git remote 명령어를 통해 원격저장소를 관리할 수 있다. 아래와 같은 명령으로 로컬 git에 등록된 원격 저장소를 확인해보자.

git remote

git clone명령어를 통해 로컬저장소를 만들었다면, 자동으로 원격저장소로 origin이 만들어졌을 것고, git init명령어를 통해 로컬에서 최초로 git 저장소를 만들었다면, 원격저장소는 만들어지지 않았을 것이다.

git remote add remote_name https://www.github.com/username/testrepository.git

위와 같이 명령어를 실행하면, github의 username이란 계정의 testrepository저장소를 ‘remote_name’이라는 이름의 원격저장소로 등록할 수 있다.

Git 저장소의 구성

어찌되었건, 위와같이 만들어진 저장소에는 .git이라는 폴더아래 git으로 해당 저장소를 유지, 관리하기 위한 정보, 후술할 내용들이 저장된다.

이외에도 저장소 최상위 폴더에 아래와 같은 것들도 저장되는데,

  • .gitattribute 에는 어떻게 merge 할지, binary file의 diff는 어떻게 할지, checkin/checkout은 어떻게 할지에 대한 정보를 설정할 수 있다.
  • .gitignore 에는 commit을 원하지 않는 파일이름, 또는 확장자를 지정할 수 있다.

가지(branch)치기

개발을 하다보면 여러가지 버전을 유지해야하는 경우가 생긴다. 굉장히 복잡한 실험적인 기능을 구현하느라, 안정된 이전 버전은 따로 둔 채 작업을 하고 싶을 수도 있고, 어떠한 이유로 여러가지 기능을 각각 구현하는 작업을 동시에 진행해야 할 일도 생기기 때문이다.

이러한 상황을 위해 git은 가지치기 기능을 지원한다.

현재 가지 확인하기

git branch 

명령을 실행하면, 해당 저장소에 있는 branch들과 현재 작업중인 branch에 ‘*‘를 표시하며 branch를 보여준다.

새 가지 만들기

git branch branch_name

위와 같이 명령을 실행하면 현재 branch가 그대로 복사된 ‘branch_name’이라는 이름을 가진 새 branch가 만들어진다.

이렇게 만든 branch는 git push remote_name branch_name을 통해 원격저장소로 전송하기 전까지 원격 저장소에 반영되지 않는다.

가지 삭제하기

git branch -d branch_name

위와 같이 명령을 실행하면 ‘branch_name’이라는 branch가 삭제된다.

가지 갈아타기 (checkout)

git checkout main

# branch를 만들면서 갈아타기
git checkout -b new_branch

위와 같이 명령을 실행하면 현재 작업 branch를 ‘main’ branch로 바꾼다. 또한 -b 옵션을 붙이면 만들어지지 않은 branch를 만들고 갈아타게 된다.


변경사항 반영하기

저장소에 있는 데이터를 수정했으면 이를 반영해야할 것이다. 인덱스에 추가해놓는 추가 작업과, 인덱스에 추가된 변경사항들을 확정하는 commit 작업이 필요하겠다.

추가 (add / staging data change)

git add 명령어로 변경된 파일을 인덱스에 추가할 수 있다.

git add modified_file.md

# 모든 파일 변경을 추가하는 방법
# git add *

위와 같이 특정 파일만 선택하여 추가하거나, 변경된 모든 파일을 추가할 수 있다.

확정 (commit)

위에서 인덱스에 추가된 변경사항들을 확정하는 작업이며, 변경사항을 HEAD에 기록하여 저장소의 이력으로 남게되는 작업이다.

# commit 메세지를 전달하지 않으면 vim이나 nano와 같은 editor를 불러 메세지를 작성하게 한다.
git commit

# '-m' 옵션으로 commit 메세지를 전달한다.
git commit -m "Update README.md"

# add(stash) 없이 변경된 모든 사항을 commit한다.
git commit -a

변경 내용 발행(push)하기

commit으로 확정한 변경사항들은 아직 로컬 저장소의 HEAD에만 존재한다. 이를 git push 명령어를 통해 원격저장소로 발행할 수 있다.

git push remote_name branch_name

위와 같이 명령어를 사용하면, ‘branch_name’이라는 branch의 HEAD에 기록된 변경사항들(commits)을 ‘remote_name’이라는 원격저장소로 올려보내 발행할 수 있다.

강제 발행

필자의 경우 종종 git push -f를 통해 강제로 발행해야하는 경우가 있었다. 주로 잘못된 내용을 commit후 원격저장소로 발행까지 마친 경우이다. 로컬 저장소에서 제대로 수정후 아래와 같이 명령을 내렸다.

git push -f origin main

위와 같이 명령을 내리면 main branch의 HEAD에 기록된 commit들을 그냥 origin 원격저장소에 덮어씌우게된다. 충돌을 무시하고 싸그리 덮어씌우기 때문에, origin 원격저장소에 무슨 내용이 있었던 main branch의 HEAD에 기록된 commit들만 복사되어 남는다.


받아오기(fetch), 병합(merge) 그리고 갱신(pull)

앞서 branch에 대해 설명하면서 여러가지 버전을 유지해야할 이유를 설명했었다. 때문에 여러 branch에서 각자 작업을 하다가도 확정 변경사항 (commit)을 다른 branch로부터 가져와 병합해야할 일도 생기기 마련이다.

또 그뿐 아니라, 로컬 저장소 내 branch간의 병합이 아니더라도 병합해야할 일이 있다. 로컬저장소에서 확정한 변경사항을 원격저장소로 올려보낼 수도 있겠지만, 반대로 다른 사용자가 다른 저장소에서 확정한 변경사항을 원격저장소로 발행했을 수도 있다. 함께 작업한다면, 다른 사용자가 작업한 변경사항도 받아와 병합하는 갱신작업이 필요하겠다.

병합 (merge)

git checkout main
git merge another_branch

위 명령들을 살펴보자. 잘 알다시피 현재 branch는 main인데 git merge 명령에 another_branch라는 인수를 줬다. 따라서 이 명령은 another_branch의 변경사항을 main으로 가져오는 병합을 시도한다. 물론 이때 another_branch는 main보다 앞서있어야 하고, 뒤쳐진 commit이 없거나 있어도 충돌의 여지가 없어야한다. (충돌한다면 충돌을 해결해야한다.)

받아오기 (fetch)

git fetch remote_name
git checkout FETCH_HEAD

위 명령어는 원격 저장소로부터 변경사항을 받아오기만 한다. 원격저장소의 최신 이력은 FETCH_HEAD라는 이름의 임시 branch에서 확인 가능하다.

갱신 (pull)

# 원격저장소로부터 변경사항을 받아와 병합하기
git pull remote_name branch_name

위와 같은 명령으로 로컬저장소의 branch를 갱신할 수 있다. git pull 명령어는 원격저장소로부터 변경사항을 받아와 병합하는 명령어로, fetchmerge명령어가 아래와 같이 합쳐졌다고 볼 수 있겠다.

# 원격저장소로부터 변경사항 받아오기
git fetch remote_name

# 원격저장소로부터 해당 브랜치를 병합하기
git merge FETCH_HEAD
# git merge remote_name/branch_name

변경이력 (log)

git log 명령어를 통해 현재 branch의 변경이력을 확인할 수 있다. 아래는 필자의 현재 jekyll 블로그 git 저장소의 temp branch 변경이력이다.

~/Project/fennecfox38.github.io$ git log
commit 7fd985008b8c49f0e82655f84796e78b0388b77a (HEAD -> temp, origin/temp)
Author: fenencfox38 <fennecfox38@gmail.com>
Date:   Mon Feb 22 17:21:06 2021 +0900

    Update 2-3 Sound Processing

commit 6e6631918fd382ff85ce493034dadd02b6ed8e9f
Author: fenencfox38 <fennecfox38@gmail.com>
Date:   Mon Feb 22 14:09:36 2021 +0900

    assets rearranged
:

commit 다음에 이어지는 40자리 16진수에 주목하라. 해당 commit의 SHA-1 해시값이다.

변경이력 지우기 (reset)

이미 commit을 했는데 심지어 발행까지 하고 나니 잘못되었음을 인지하고 수정하고 싶을 수도 있겠다.

이미 원격저장소로 발행까지 해버린 경우에는 앞서 이야기한 강제발행을 통해 commit을 덮어쓰기가 가능하니 괜찮다만, 이미 확정한 commit들은 어떻게 수정할까.

여러 방법이 있겠지만 필자는 현재까지 변경이력을 지우는 reset을 애용해왔다.

git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]

위와 같이 사용하라고 나와있지만 나머지는 잘 모르겠고… commit에는 commit 해쉬값을 전달해주면 되겠다.

--hard 옵션은 commit되지 않은 변경사항들 (add, stash 된 변경사항들, 또는 그렇지 않고 그냥 작업디렉토리에만 남아있는 untracked인 변경사항들)은 모조리 날려버린다. 그러고 HEAD만 주어진 commit으로 옮겨버린다.

--soft 옵션은 commit되지 않은 변경사항들 (add, stash 된 변경사항들, 또는 그렇지 않고 그냥 작업디렉토리에만 남아있는 untracked인 변경사항들)을 그대로 유지하면서 HEAD만 주어진 commit으로 옮겨버린다.

--mixed 옵션은 기본 옵션으로 옵션값을 주지 않으면 기본값으로 주어지게된다. commit되지 않은 변경사항들 (add, stash 된 변경사항들, 또는 그렇지 않고 그냥 작업디렉토리에만 남아있는 untracked인 변경사항들)은 남아있게되나, unstashed된 상태로 남겨진다. 그리고 HEAD를 주어진 commit으로 옮겨버린다.

# commit 7fd9850... 을 지우기
git reset 7fd9850... #(reset의 기본 옵션은 --mixed)

# 현재 HEAD가 가리키는 commit 상태로 돌리기 (commit 되지 않은 변경사항들 전부 사라짐)
git reset --hard HEAD

# 현재 HEAD가 가리키는 commit의 한단계 전 상태로 돌리기 (작업 디렉토리와 인덱스는 계속 유지시켜 commit되지 않은 변경사항도 유지)
git reset --soft HEAD^

필자는 아직까지 복잡한 reset옵션이 필요한 적이 없었기에 git reset HEAD^로 바꿔야할 commit만 하나씩 지워 수정해서 다시 commit하면서 사용했다.

특정 commit만 골라 병합하기 (cherry-pick)

BitmapProject Repository에서 cherry-pick을 애용했었다. 이게 바람직한 사용인지는 사실 의문이다.

처음에는 windows 컴퓨터 환경에서 작업하다가, 나중에 WSL을 통해 작업할 것을 염두해 WSL환경에 맞게 옮겼다가, Android의 termux로 작업환경을 다시 바꾸었다. 하지만 termux 작업환경은 한시적으로 사용하는 것이기 때문에 작업은 계속 termux에서 하지만 main branch로 지속적으로 갱신하기를 원했다. 하지만 다른 commit은 병합해야하겠지만, 작업환경을 termux로 옮긴 commit(Makefile수정)은 옮기면 안 됐다. 때문에 특정 commit만 선택해 병합하는 cherry-pick을 자주 사용했었다.

어쨌든 아래와 같이 commit을 가져와 새로 추가해야할 branch로 이동 후, cherry-pick 명령어와 함께 해당 commit을 건네주면 되겠다.

git checkout target_branch
git cherry-pick 7e4fe58 # (commit hash code)

commit에 수정사항 추가하기: git commit --amend

마지막으로 확정한 commit에 빠뜨린 부분이 있다면 해당 커밋에 내용을 추가할 수 있다.

git add를 이용해 추가할 수정 사항들을 add한 후 git commit --amend명령을 실행하면, 해당 커밋에 추가한 수정사항까지 반영된다.

commit의 내용이 변경되었기때문에 hash가 당연히 바뀌는 것에 유의하여라. 해당 commit이 이미 remote에도 push된 경우라면, remote로 push할 때 git push -f를 사용하여야 하며, 이는 다른 공동작업자의 작업물에 영향을 끼칠 수 있으므로 지양하는 것이 좋다.

+ 이전 commit에 수정사항 추가하기: git commit --amend with git rebase

만약 가장 최신 commit이 아닌 이전 commit을 수정하고 싶다면 git rebase를 이용하여 해결할 수 있다.

수정할 commit 뿐 아니라 그 이후 commit들도 모두 hash code가 바뀌며 (내용은 동일), 마찬가지로 git push -f를 사용해서 remote로 push 해야 함에 유의하라.

1. git log로 수정할 commit과 그 이전 commit의 hash code 확인

$ git log
commit f3699170a37a9aa29635f5bb6113eb4539d6f4c2
Author: fennecfox38 <fennecfox38@gmail.com>
Date:   Sat Mar 6 12:08:52 2021 +0900

    Update Git Manual

commit 30d0212f3d89fefbd6be2d7c84611ef74f108efb
Author: fennecfox38 <fennecfox38@gmail.com>
Date:   Thu Mar 4 11:45:49 2021 +0900

    Update GitHub-Page-Open & etc.
:

Update Git Manual에 수정사항을 추가할 예정이므로 그 이전 commit의 hash 30d0212f3d89fefbd6be2d7c84611ef74f108efb를 기억해 둔다.

2. git rebase –interactive로 rebase

git rebase --interactive 30d0212 명령어로 수정할 commit 이전 commit(30d0212)으로 rebase한다.

그럼 아래와 같이 rebase-todo 리스트를 수정할 수 있는데,

pick f369917 Update Gi-Manual
pick 4f256ba Update BOJ-6549
pick d18fa0d Update BOJ-2261
pick 3a23233 Posting Ubuntu Installation
pick 1c63dfc Post ATmega328P DIY
pick 88c6f64 Update ATmega328P DIY

수정할 commit의 pick을 찾아 아래와 같이 edit으로 바꿔준다.

edit f369917 Update Gi-Manual
pick 4f256ba Update BOJ-6549
pick d18fa0d Update BOJ-2261
pick 3a23233 Posting Ubuntu Installation
pick 1c63dfc Post ATmega328P DIY
pick 88c6f64 Update ATmega328P DIY

이제 rebase-todo 리스트를 저장하면

Stopped at f369917...  Update GitManual
You can amend the commit now, with

  git commit --amend 

Once you are satisfied with your changes, run

  git rebase --continue

수정사항을 git commit --amend를 통해서 반영시킨후, git rebase --continue를 통해 rebase를 이어가라고 안내하고 있다.

3. 수정사항 반영하기, rebase --continue

$ git add -all
$ git commit --amend
$ git rebase --continue

수정사항을 반영한 후, 위와 같이 commit에 수정사항 반영 후, git rebase --continue 하면 끝.


사실 여기까지 정리하면서도 어 이게 맞나 싶어서 찾아보고 참고한 내용이 많다. 이를 통해 느낀점 두가지가 있다.

  1. git은 시간을 내서 제대로 배울 가치가 있는 도구이다. 시간 내서 배워야겠다.
  2. 이걸 내가 정리하는 것보다 git documentation을 정독하는게 훨씬 낫겠다. 그래서 이 글은 여기까지만 쓰고 더 이상 업데이트 하지 않을 예정이다. 또 업데이트를 하고 있다…

지금보니 오개념도 많은 거 같은데… 이 글을 지워야하나 남겨두어야 하나… 이제 와서 하는 이야기이지만 이런 어줍잖은 블로그 글을 읽는 대신 git Documentation을 정독하길 바란다.

This work is licensed under Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.
(The excerpted works are exceptionally subject to a licence from its source.) Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)