본문 바로가기
프로그래밍 놀이터/Tips

[도서 정리] 7. Git 도구 #1 - ProGit

by 돼지왕 왕돼지 2020. 1. 11.
반응형

[도서 정리] 7. Git 도구 #1 - ProGit



7.1. 리비전 조회하기


* 리비전 하나 가리키기




* SHA-1 줄여 쓰기


-

Git 은 해시값의 앞 몇 글자만으로도 어떤 커밋인지 충분히 식별할 수 있다.

중복되지 않으면 해시값의 앞 4자만으로도 나타낼 수 있다.



-

git show [part_of_hash] 로 특정 커밋을 조회할 수 있다.

$ git show <part_of_hash>



-

git log 명령에 —abbrev-commit 이라는 옵션을 추가하면 짧고 중복되지 않는 해시값을 보여준다.

기본으로 7자를 보여주고 해시값이 중복되는 경우 더 긴 해시값을 보여준다.

보통은 8자에서 10자 내외로도 충분히 유일하게 커밋을 나타낼 수 있다.



-

참고로 리눅스 커널은 45만 개 이상의 커밋, 360만 개 이상의 오브젝트가 있지만, 해시값 11개만 사용해도 충돌이 없다.



-

언젠가 SHA-1 값이 중복될까 걱정하는데, 실제 발생한다면..

이미 있는 SHA-1 값이 git db 에 커밋되면 새로운 개체라고 해도 이미 커밋된 것으로 생각한다.

그래서 해당 SHA-1 값의 커밋을 checkout 하면 항상 처음 저장한 커밋만 checkout 된다.


그러나 해시값이 중복되는 일은 일어나기 어렵다.

SHA-1 값의 크기는 20바이트(160bit)이다.

해시값이 중복될 확률이 50% 되는데 필요한 개체의 수는 2^80 이며, 이 수는 1자 2000해이다.

지구에서 약 6억 5천만 명의 인구가 개발을 하고 각자 매초 리눅스 커널 히스토리 전체(360만 개)와 맞먹는 개체를 쏟아 내고 바로 push 한다고 가정해도, 이런 상황에서 해시값의 충돌 날 확률이 50% 가 되기까지는 약 2년이 걸린다.




* 브랜치로 가리키기


-

브랜치를 사용하는 것이 커밋을 나타내는 가장 쉬운 방법이다.

git show 로 SHA 값을 넣어도 되지만, branch name 을 넣을 수도 있다.



-

브랜치가 가리키는 개체의 SHA-1 이 궁금하다면 rev-parse 라는 plumbing 도구가 해결해준다.

$ git rev-parse <branch_name>




* RefLog 로 가리키기


-

Git 은 자동으로 브랜치와 HEAD 가 지난 몇 달 동안에 가리켰었던 커밋을 모두 기록하는데 이 로그를 Reflog 라고 부른다.

git reflog 를 실행하면 된다.

$ git reflog



-

Git 은 브랜치가 가리키는 것이 달라질 때마다 그 정보를 임시 영역에 저장한다.

그래서 예전에 가리키던 것이 무엇인지 확인해 볼 수 있다.

@{n} 규칙을 사용하면 아래와 같이 HEAD 가 5번 전에 가리켰던 것을 알 수 있다.


ex) 

$ git show HEAD@{5}


순서뿐 아니라 시간도 사용할 수 있다.


ex) 

$ git show master@{yesterday}


Reflog 에 남아있을 경우에만 조회할 수 있기 때문에 너무 오래된 커밋은 조회할 수 없다.



-

git log -g 명령을 사용하면 git reflog 결과를 git log 명령과 같은 형태로 볼 수 있다.


ex) 

$ git log -g master



-

reflog 의 일은 모두 로컬의 일이기 때문에 내 relog 가 동료의 저장소에는 있을 수 없다.




* 계통 관계로 가리키기


-

이름 끝에 ^ 를 붙이면 git 은 해당 커밋의 부모를 찾는다.

HEAD^ 는 HEAD 의 부모를 이야기하므로 이전 커밋을 보여준다.

^ 뒤에 숫자도 사용할 수 있다.

예를 들어 d921970^2 는 d921970 의 두 번째 부모를 의미한다.



-

계통을 표현하는 방법으로 ~라는 것도 있다.

HEAD~ 와 HEAD^ 는 똑같이 첫 번째 부모를 가리킨다.

그러나 그 뒤에 숫자를 사용하면 달라진다.

HEAD~2 는 명령을 실행할 시점의 첫번째 부모의 첫번째 부모 즉 조부모를 가리킨다.


enter image description here



-

HEAD^^^ 는 HEAD~3 와 같다.

둘을 섞어 쓸 수도 있다.

HEAD~3^2




* 범위로 커밋 가리키기


-

범위를 주고 여러 커밋을 한꺼번에 조회할 수도 있다.

범위를 사용하여 조회할 수 있으면 브랜치를 관리할 때 유용하다.



-

범위를 표현하는 문법으로 double dot (..) 을 많이 쓴다.

double dot 은 어떤 커밋들이 한쪽에는 관련됐고 다른 쪽에는 관련되지 않았는지 git 에게 물어보는 것이다.

a branch 커밋들 중에서 아직 master 브랜치에 머지하지 않은 것을 보고싶으면,

master..a 라고 사용한다. 이것은 master 에는 없지만 a 에는 있는 커밋을 의미한다.


반대로 a 에는 없지만, master 에는 있는 것이 궁금하면 a..master 로 하면 된다.



-

토픽 브랜치를 머지하기 전에 무엇이 변경되었는지를 확인해 보고 싶을 때 유용하다.

리모트 저장소에 push 하고 싶을 때도 마찬가지다.


ex)

$ git log origin/master..HEAD



-

double dot 은 두 개 이상의 브랜치에는 사용할 수 없다.

현재 작업 중인 브랜치에는 있지만 다른 여러 브랜치에는 없는 커밋을 보고 싶으면 .. 으로는 확인할 수 없다.

git 은 ^이나 —not 옵션 뒤에 브랜치 이름을 넣으면 그 브랜치에 없는 커밋을 찾아준다.


아래 3개의 명령은 동일하다.

$ git log refA..refB

$ git log ^refA refB

$ git log refB —not refA


이 옵션은 세 개 이상의 Refs 에 사용할 수 있는 장점이 있다.


$ git log refA refB ^refC

$ git log refA refB —not refC



-

triple dot 은 양쪽에 있는 두 Refs 사이에서 공통으로 가지는 것을 제외하고 서로 다른 커밋만 보여준다.

만약 master 와 a 의 공통부분은 빼고 다른 커밋만 보고 싶다면 아래와 같이 한다.


$ git log master…a FEDC # FDEC 는 최근 날짜순


log 명령에 —left-right 옵션을 추가하면 각 커밋이 어느 브랜치에 속하는지도 보여준다.





7.2. 대화형 명령


-

git 은 대화형 스크립트도 제공하여 명령을 좀 더 쉽게 사용할 수 있다.

스크립트를 통해 커밋할 파일을 고르고 수정된 파일 일부분만 커밋할 수도 있다.

스크립트는 수정하는 파일이 매우 많아서 통째로 커밋하기 어려울 때 이슈별로 나눠서 커밋하기에 좋다.


git add 명령에 -i 나 —interactive 옵션을 주고 실행하면 된다.


$ git add -i

$ git add -interactive




* Staging Area 에 파일 추가하고 추가 취소하기


-

operation 을 한 후 프롬프트에 아무것도 입력하지 않고 엔터를 치면 작업을 수행한다.



-

stage 는 “update” 명령을 사용하면 되고,

unstage 는 “revert” 를 하면 된다.

“diff” 는 git diff —cached 와 같다. (—cached 는 push 된 것과 비교한다. 없으면 unstage 와 stage 된것(없으면 push된것)과 비교한다.)




* 파일 일부분만 Staging Area 에 추가하기


-

“patch” 명령을 이용하면 된다.

hunk 단위로 나온다.

? 를 입력하면 선택할 수 있는 명령을 선택해준다.



-

대화형 스크립트로만 파일 일부분을 stage 할 수 있는 것은 아니다.

git add -p 나 git add —patch 로도 같은 일을 할 수 있다.


reset —patch 명령을 사용해서 파일 일부만 stage area 에서 내릴 수도 있다.

또 checkout —patch 를 사용해서 파일 일부를 다시 checkout 받을 수 있다.


stash save —patch 명령으로는 파일 일부만 stash 할 수 있다.







7.3. Stashing 과 Cleaning


-

커밋하지 않고 나중에 작업분을 돌리고 싶다면 git stash 명령을 사용하면 된다.

stash 명령을 사용하면 워킹 디렉터리에서 수정한 파일들만 저장한다.

stash 는 modified 이면서 tracked 상태인 파일과 staging area 에 있는 파일들을 보관해두는 장소다.




* 하던 일을 Stash 하기


-

git stash 나 git stash save 를 실행한다.

$ git stash

$ git stash save



-

git stash list 를 통해 저장한 stash 를 확인할 수 있다.

$ git stash list



-

git stash apply 를 사용하여 stash 를 다시 적용 할 수 있다.

git stash apply stash_name 으로 stash 를 골라 적용할 수 있다.

이름이 없으면 git 은 가장 최근 stash 를 적용한다.

$ git stash apply

$ git stash apply <stash_name>



-

stash 를 적용할 때 충돌이 발생할 수 있다.



-

git 은 stash 를 적용할 때 staged 상태였던 파일을 자동으로 다시 staged 상태로 만들어 주지 않는다.

그래서 git stash apply 명령을 실행할 때 —index 옵션을 주어야 원래 작업하던 상태로 돌아올 수 있다.

$ git stash apply --index



-

git stash drop 명령을 사용하여 stash 를 제거할 수 있다.

git stash pop 이라는 명령은 stash 를 적용하고 나서 바로 스택에서 제거해준다.

$ git stash drop

$ git stash pop




* Stash 를 만드는 새로운 방법


-

stash 를 만들 때 주로 사용하는 옵션은 stash save 명령과 함께 쓰는 --keep-index 이다.

이 옵션을 이용하면 이미 staging area 에 들어있는 파일을 stash 하지 않는다.

$ git stash save --keep-index



-

추적하지 않는 파일과 추적 중인 파일을 같이 stash 하는 일도 빈번하다.

기본적으로 git stash 는 추적 중인 파일만 저장한다.

추적 중이지 않은 파일을 같이 저장하려면 --include-untracked 나 -u 옵션을 붙여준다.

$ git stash save --include-untracked

$ git stash save -u



-

—patch 옵션을 붙이면 git 은 수정된 모든 사항을 저장하지 않는다.

대신 대화형 프롬프트가 뜨며 변경된 데이터 중 저장할 것과 저장하지 않을 것을 지정할 수 있다.

$ git stash save --patch




* Stash 를 적용한 브랜치 만들기


-

git stash branch 명령을 실행하면 stash 할 당시의 커밋을 checkout 한 후 새로운 브랜치를 만들고 여기에 적용한다.

이 모든 것이 성공하면 stash 를 삭제한다.


ex) 

$ git stash branch <new_branch_name>




* 워킹 디렉터리 청소하기


-

작업하고 있던 파일을 stash 하지 않고 단순히 그 파일들을 치워버리고 싶을 때는 git clean 명령을 사용한다.

$ git clean


이 명령을 사용할 때는 신중해야 한다.

이 명령을 사용하면 워킹 디렉터리 안의 추적하고 있지 않은 모든 파일이 지워지기 때문이다.


git stash -all 명령을 이용하면 지우는 건 똑같지만, 먼저 모든 파일을 stash 하므로 좀 더 안전하다.

$ git stash -all



-

워킹 디렉터리의 불필요한 파일들을 전부 지우려면 git clean 을 사용하는데, 추적 중이지 않은 모든 정보를 워킹 디렉터리에서 지우고 싶다면 git clean -f -d 명령을 사용한다.

$ git clean -f -d


이 명령은 하위 디렉터리까지 모두 지워버린다.

-f 옵션은 강제의 의미이다.


이 명령을 실행했을 때 어떤 일이 일어날지 미리 보고 싶다면 -n 옵션을 사용하는데, -n 은 가상으로 실행해보고 어떤 파일들이 지워질지 알려달라는 뜻이다.

git clean 이 무슨 짓을 할지 확신이 안 들 때는 항상 -n 옵션을 붙여서 먼저 실행해보자.

$ git clean -f -d -n



-

git clean 은 추적 중이지 않은 파일만 지우는 게 기본 동작이다.

.gitignore 에 명시했거나 해서 무시되는 파일은 지우지 않는다.

무시된 파일까지 함께 지우려면 -x 옵션이 필요하다.

$ git clean -f -d -x



-

clean 명령을 대화형으로 실행하려면 -i 옵션을 붙이면 된다.





7.4. 내 작업에 서명하기


-

Git 은 암호학적으로 안전하다. 하지만 그냥 되는 건 아니다.

저장소에 아무나 접근하지 못하게 하고 진짜로 확인된 사람에게서만 커밋을 받으려면 GPG 를 이용한다.




* GPG 소개


-

뭔가를 서명하려면 GPG 설정도 하고 개인 키도 설치해야 한다.

$ gpg —list-keys


가진 키가 없으면 키를 새로 만들어야 한다.

$ gpg —gen-key


서명에 사용할 수 있는 개인 키가 이미 있다면 user.signingkey 로 설정해서 사용할 수 있다.

$ git config —global user.signingkey 0A46826A


설정이 완료되면 이제 git 태그와 커밋에 서명할 때 등록한 키를 사용한다.




* 태그 서명하기


-

태그에 서명할 때 -a 대신 -s 를 사용하면 된다. (-a 는 unsigned annotated tag)


ex) 

$ git tag -s v1.5 -m ‘my signed 1.5 tag’


git show 로 확인해보면 gpg 서명이 붙어 있는 걸 확인할 수 있다.




* 태그 확인하기


-

git tag -v tag_name 명령을 이용해 태그에 서명한 사람이 정말 그 사람이 맞는지 확인한다.

이 명령은 서명을 확인하기 위해 gpg 를 사용한다.

확인 작업을 하려면 서명한 사람의 gpg 공개키를 키 관리 시스템에 등록해 두어야 한다.


서명한 사람의 공개키가 없으면 error 메시지가 나온다.




* 커밋에 서명하기


-

최신 버전(v.1.7.9 이상)의 git 은 커밋에도 서명할 수 있다.

커밋에 서명하고 싶으면 git commit 명령에 -S 옵션만 붙여주면 된다.


ex) 

$ git commit -a -S -m ‘signed commit’


서명을 확인하려면 git log 에 —show-signature 옵션을 붙여주자.



-

git log 로 출력한 로그에서 커밋에 대한 서명 정보를 알려면 %G? 포맷을 이용하면 된다.


ex) 

$ git log —pretty=“format:%h %G? %aN %s”


서명을 한 경우 G, 그렇지 않은 경우 N 이 표시된다.



-

git 1.8.3 버전 이후로는 git merge 와 git pull 에서 gpg 서명 정보를 이용해 merge 를 허용하지 않을 수 있다.

merge 할 때 —verify-signatures 옵션을 붙이면 머지할 커밋 중 서명하지 않았거나 신뢰할 수 없는 사람이 서명한 커밋이 있으면 머지되지 않는다.



-

git merge 명령에도 -S 옵션을 붙일 수 있는데, 이 옵션을 붙이면 머지 커밋을 서명하겠다는 의미이다.


ex) 

$ git merge —verify-signatures -S signed-branch




* 모두가 서명하게 하려면


-

태그와 커밋에 서명하는 것은 멋지지만 실제로 서명 기능을 사용하려면 팀의 모든 사람이 서명 기능을 이해하고 사용해야만 한다.

반드시 작업에 적용하기 전에 gpg 서명 기능을 이해하고 이 기능이 가지는 장점을 완전히 파악하고 있어야만 한다.







7.5. 검색


* Git Grep


-

git 의 grep 명령을 이용하면 커밋 트리의 내용이나 워킹 디렉터리의 내용을 문자열이나 정규식을 이용해 쉽게 찾을 수 있다.

기본적으로 대상을 지정하지 않으면 워킹 디렉터리의 파일에서 찾는다.

명령을 실행할 때 -n 옵션을 추가하면 찾을 문자열이 위치한 라인 번호도 같이 출력한다.


ex) 

$ git grep -n Random



-

결과 대신 어떤 파일에서 몇 개나 찾았는지만 알고 싶다면 --count 옵션을 이용한다.

$ git grep --count Random



-

매칭되는 라인이 있는 함수나 메서드를 등의 정보도 함께 보고 싶다면 -p 옵션을 준다.


ex) 

$ git grep -n -p Random *.kt # .kt 파일에서 Random 이 있는 함수와 클래스명 등이 보여진다.



-

—and 옵션을 이용해서 여러 단어가 한 라인에 동시에 나타나는 줄 찾기 같은 복잡한 조합으로 검색할 수 있다.

—break 와 —heading 옵션을 붙여 더 읽기 쉬운 형태로 잘라서 출력할 수도 있다.


ex) 

$ git grep —break —heading -n -e ‘Random’ —and \(-e true -e false\) branch_name



-

git grep 명령은 grep 이나 ack 같은 일반적인 검색 도구보다 몇 가지 좋은 점이 있는데,

우선 매우 빠르다.

또한 워킹 디렉터리만이 아니라 git 히스토리 내의 어떠한 정보라도 찾아낼 수 있다.




* Git 로그 검색


-

git log 명령을 이용하면 diff 내용도 검색하여 어떤 커밋에서 찾고자 하는 내용을 추가했는지 찾을 수 있다.

BUF_MAX 라는 상수가 가장 처음 나타난 때를 찾는 문제라면 -S 옵션을 이용해 해당 문자열이 추가된 커밋과 없어진 커밋만 검색할 수 있다.


ex) 

$ git log -SZBUFMAX —oneline #-SZ 와 검색어 사이에 띄워쓰기 없음


더 세세한 조건을 걸어 찾고 싶다면 로그를 검색할 때 -G 옵션으로 정규식을 써서 검색하면 된다.



-

라인 히스토리 검색이라는 미친듯 좋은 로그 검색 도구가 있다.

git log 에 -L 옵션을 붙이면 어떤 함수나 한 라인의 히스토리를 볼 수 있다.

예를 들어 zlib.c 파일에 있는 git_deflate_bound 함수의 모든 변경사항이 보고 싶다면..

$ git log -L :git_deflate_bound:zlib.c 


git 이 함수의 처음과 끝을 인식하지 못할 때는 정규식으로 인식하게 할 수도 있다.

$ git log -L ‘/unsigned long git_deflate_bound/‘, /^}/:zlib.c





7.6. 히스토리 단장하기


* 마지막 커밋을 수정하기


-

git commit —amend 를 사용하면 된다.

$ git commit --amend




* 커밋 메시지를 여러 개 수정하기


-

예전 커밋을 수정하려면 다른 도구가 필요하다.

rebase 명령을 이용하여 수정할 수 있다.

현재 작업하는 브랜치에서 각 커밋을 하나하나 수정하는 것이 아니라 어느 시점부터 HEAD 까지의 커밋을 한 번에 rebase 한다.

대화형 rebase 도구를 사용하면 커밋을 처리할 때마다 잠시 멈춘다.

그러면 각 커밋의 메시지를 수정하거나 파일을 추가하고 변경하는 등의 일을 진행할 수 있다.


-i 옵션을 추가하면 대화형 모드로 rebase 할 수 있다.


예를 들어 마지막 커밋 메시지 세 개를 모두 수정하거나 그 중 몇개를 수정한다면..

$ git rebase -i HEAD~3


이 명령은 rebase 이기 때문에 메시지의 수정 여부와 관계없이 HEAD~3..HEAD 범위에 있는 모든 커밋을 수정한다.



-

중앙 서버에 push 한 커밋은 절대 고치지 말아야 한다.

push 한 커밋을 rebase 하면 결국 같은 내용을 두 번 push 하는 것이기 때문에 다른 개발자들이 혼란스러워 한다.



-

대화형 rebase 는 스크립트에 적혀 있는 순서대로 HEAD~3 부터 적용하기 시작하고 위에서 아래로 각각의 커밋을 순서대로 수정한다.

순서대로 적용하는 것이기 때문에 제일 위에 있는 것이 최신이 아니라 가장 오래된 것이다.



-

특정 커밋에서 실행을 멈추게 하려면 pick 이라는 단어를 edit 로 수정하면 그 커밋에서 멈춘다.

rebase 가 완료되면 git commit —amend 를 통해 커밋메시지를 수정하고, git rebase —continue 로 마무리한다.

$ git commit --amend

$ git rebase --continue




* 커밋 순서 바꾸기




* 커밋 합치기


-

squash 가 그 기능을 한다.

합치면서 커밋 메시지도 merge 하는데, 수정 가능하다.




* 커밋 분리하기


-

edit 를 사용하면 된다.

명령 프롬프트로 넘어간 다음 그 커밋을 해제하고 그 내용을 다시 두 개로 나눠서 커밋하면 된다.


ex) 

$ git reset HEAD^

  # git add

  # git commit

  # git add

  # git commit

$ git rebase —continue



-

rebase 하면 목록에 있는 모든 커밋의 SHA-1 값이 변경된다.

절대로 이미 서버에 push 한 커밋을 수정하면 안 된다.




* filter-branch 는 포크레인


-

수정해야 하는 커밋이 너무 많아서 rebase 스크립트로 수정하기 어려울 것 같으면 다른 방법을 사용하는 것이 좋다.

모든 커밋의 이메일 주소를 변경하거나 어떤 파일을 삭제하는 경우 filter-branch 라는 명령으로 수정할 수 있는데.. rebase 가 삽이라면 이 명령은 포크레인이라고 할 수 있다.



-

filter-branch 도 역시 수정하려는 커밋이 이미 공개돼서 다른 사람과 함께 공유하는 중이라면 사용하지 말아야 한다.



-

filter-branch 는 히스토리 전체에서 필요한 것만 골라내는 데 사용하는 도구다.

filter-branch 의 —tree-filter 라는 옵션을 사용하면 히스토리에서 특정 파일을 아예 제거할 수 있다.


ex) 히스토리에서 passwords.txt 파일을 제거할 수 있다.

$ git filter-branch —tree-filter ‘rm -f passwords.txt’ HEAD


—tree-filter 옵션은 프로젝트를 checkout 한 후에 각 커밋에 명시한 명령을 실행시키고 그 결과를 다시 커밋한다.



-

실수로 편집기의 백업파일을 커밋했으면 git filter-branch --tree-filter ‘rm -f *~’ HEAD  를 하면 된다.

이 명령은 모든 파일과 커밋을 정리하고 브랜치 포인터를 다시 복원해준다.

이런 작업을 테스팅 브랜치에서 실험하고 나서 master 브랜치에 적용하는 게 좋다.



-

—filter-branch 명령에 —all 옵션을 추가하면 모든 브랜치에 적용할 수 있다.



-

다른 VCS 에서 코드를 임포트하면 그 VCS 만을 위한 디렉터리가 있을 수 있다.

SVN 에서 코드를 임포트하면 trunk, tags, branch 디렉터리가 포함된다.

모든 커밋에 대해 trunk 디렉터리를 프로젝트 루트 디렉터리로 만들 때도 filter-branch 명령이 유용하다.


$ git filter-branch —subdirectory-filter trunk HEAD


이제 trunk 디렉터리가 루트 디렉터리이고, git 은 입력한 디렉터리와 관련이 없는 커밋을 자동으로 삭제한다.



-

프로젝트를 오픈 소스로 공개할 때 아마도 회사 이메일 주소로 커밋된 것을 개인 이메일 주소로 변경해야 한다.

아니면 아예 git config 로 이름과 이메일 주소를 설정하는 것을 잊었을 수도 있다.

자신의 이메일 주소만 변경하도록 조심해야 한다.

filter-branch 명령의 —commit-filter 옵션을 사용하여 해당 커밋만 골라서 이메일 주소를 수정할 수 있다.


$ git filter-branch —commit-filter ‘

    if [“$GIT_AUTHOR_EMAIL” = “abc@abc.com”];

    then

        GIT_AUTHOR_NAME=“myName”;

        GIT_AUTHOR_EMAIL=“abcd@abc.com”;

        git commit-tree “$@“;

    else

        git commit-tree “$@“;

    fi’ HEAD


조건에 만족하는 커밋의 SHA-1 값만 바뀌는 것이 아니라 모든 커밋의 SHA-1 값이 바뀐다.




반응형

댓글