3/7/2024
11/4/2024

Git은 2005년 4월 3일에 리누스 토발즈가 개발을 시작해서, 17일 후인 4월 20일에 리눅스 2.6.12-rc3 커널을 Git으로 공개했다.

2.6.12-rc3의 릴리즈 메일에 Git에 대해서 언급된다.

Ok,
you know what the subject line means by now, but this release is a bit
different from the usual ones, for obvious reasons. It's the first in a
long time that I've done without using BK, and it's the first one ever
that has been built up completely with "git".

명령어 자동완성하기: https://github.com/bobthecow/git-flow-completion/wiki/Install-Bash-git-completion

git에서 언급하는 HEAD, upstream 등 용어에 대한 설명은 git help glossary로 확인할 수 있다.

깃 커밋 해시 충돌에 관하여

어느날 커밋 해시는 어떤 정보를 기반하여 만들어지는지 궁금했다. 커밋 해시는 git commit 할 때 생성되고, 커밋 해시로 git checkout하여 특정 revision으로 이동한다.

따라오는 질문은 "커밋 할 때 해시가 충돌할 여지는 없는가" 였다.

먼저, git-scm의 글 SHA-1 해시 값에 대한 단상에서 이러한 걱정에 대한 현실적인 조언을 해 준다. 또 실제로 발생하면 어떤 일이 일어나는지 알려준다.

요약하면, 해시 중복이 생성되면, 현재 구현으로는 커밋은 성공하지만, checkout하면 최초의 revision으로 이동한다. 하지만 충돌이 발생할 확률은 현실적으로 불가능하다.

SHA-1 해시 값에 대한 단상

Git을 쓰는 사람들은 가능성이 작긴 하지만 언젠가 SHA-1 값이 중복될까 봐 걱정한다. 정말 그렇게 되면 어떤 일이 벌어질까?

이미 있는 SHA-1 값이 Git 데이터베이스에 커밋되면 새로운 개체라고 해도 이미 커밋된 것으로 생각하고 이전의 커밋을 재사용한다. 그래서 해당 SHA-1 값의 커밋을 Checkout 하면 항상 처음 저장한 커밋만 Checkout 된다.

그러나 해시 값이 중복되는 일은 일어나기 어렵다. SHA-1 값의 크기는 20 바이트(160비트)이다. 해시 값이 중복될 확률이 50%가 되는 데 필요한 개체의 수는 280이다. 이 수는 1자 2,000해 ('자’는 '경’의 '억’배 - 1024, 충돌 확률을 구하는 공식은 p = (n(n-1)/2) * (1/2^160) )이다. 즉, 지구에 존재하는 모래알의 수에 1,200을 곱한 수와 맞먹는다.

아직도 SHA-1 해시 값이 중복될까 봐 걱정하는 사람들을 위해 좀 더 덧붙이겠다. 지구에서 약 6억 5천만 명의 인구가 개발하고 각자 매초 Linux 커널 히스토리 전체와(650만 개) 맞먹는 개체를 쏟아 내고 바로 Push 한다고 가정하자. 이런 상황에서 해시 값의 충돌 날 확률이 50%가 되기까지는 약 2년이 걸린다. 그냥 어느 날 동료가 한 순간에 모두 늑대에게 물려 죽을 확률이 훨씬 더 높다.

리누스 토발즈의 의견

그래도 운이 정말 나빠서, 해시 충돌 문제에 벗어날 수 없다면, 리누스 토발즈도 이 이슈에 대해 언급했다. 아쉽게도 원글이 있던 google+가 종료되어 볼 수 없지만 예전에 올라온 나프다 게시글에 누군가 요약해 주었다.

https://www.facebook.com/iamprogrammer.io/posts/1379005945454259

사람이 소스코드의 변경을 지켜보고 있기 때문에 괜찮고, 또 대안은 있다고 한다.

해시 충돌을 재현한 SO 글

사실 충돌 문제에 대해 가장 먼저 접한 것은 StackOverflow의 질문이었다.

https://stackoverflow.com/questions/9392365

답변에서, 해시 사이즈를 4-bit로 줄여서 실제로 재현했다. push, clone 할 때 에러가 발생한다.

커밋 해시를 결정하는 요소

커밋 해시가 무엇으로 결정되는지 알려주는 SO 글. 부모 커밋, 커미터, 메시지 등.

https://stackoverflow.com/questions/34764195

해시 총돌 유머

여기 있는 사이트가 사라졌다 :|

뻘글) git 불안해서 못쓰겟음니다 -.-;

https://www.codentalks.com/t/topic/2973

찾다가 나온 유머글 ㅎㅎ. 덧글에 있는 만화처럼 걱정, 우려만 해서는 안되겠다.

sha1 층돌 설명

여기도 사이트가 사라졌다 :-|

sha1 충돌 이슈에 설명. 해시에 대한 기초 설명, 구글이 sha-1 충돌 재현에 대한 주변 설명.

https://zariski.wordpress.com/2017/02/25/sha-1-%EC%B6%A9%EB%8F%8C/

md5 충돌 예제

여기 예제 사이트에서는 다른 파일인데 같은 MD5 sum을 가진 예제를 제공한다. 근데 다운받아보면 실행도 안되고, 바이너리지만 열어보면 내용도 같아 보이는데.. 심지어 파일 크기도 같다. 제대로 된 예제가 맞나?

https://www.mathstat.dal.ca/~selinger/md5collision

Configurations

.gitconfig 파일에 설정을 저장하거나 git config 명령어로 설정을 추가하거나 확인한다.

Conflict Style

[merge]
  conflictStyle = zdiff3

커밋 충돌 시 diff를 보여주는 방식을 개선한다. 기본값의 경우 다음과 같이 나타난다면:

++<<<<<<< HEAD
 +python -m SimpleHTTPServer 1234
++=======
+ python -m SimpleHTTPServer 4321

zdiff3은 원본 코드를 중간에 함께 보여준다:

++<<<<<<< HEAD
 +python -m SimpleHTTPServer 1234
++||||||| parent of dbecef5 (4321)
++python -m SimpleHTTPServer 8080
++=======
+ python -m SimpleHTTPServer 4321
++>>>>>>> dbecef5 (4321)

Commit

git commit --verbose 옵션을 자주 사용한다. 커밋 메시지를 작성할 때 변경 내용을 함께 보여줘서 유용하기 때문이다.

다음 설정은 옵션 생략하고, 기본 설정을 변경한다:

[commit]
  verbose = true

위 예시는 git rebase의 충돌 결과라 parent of dbecef5 메시지와 함께 rebase를 시작한 커밋의 원본 코드를 보여준다.

git clone

저장소를 복제하는 명렁어. 가장 기본적인 명령어 중 하나라서 모르는 사람은 없겠다.

--depth

--depth 옵션은 저장소의 최신 커밋만 복제한다. 얕은 복제라 한다:

Create a shallow clone with a history truncated to the specified number of commits.

예를 들어 --depth 1로 복제하면 최신 커밋만 복제한다. 이 옵션을 사용하면 저장소의 용량이 줄어드는 장점이 있다.

내 위키 프로젝트의 경우 전체 복제하는 경우 .git 폴더의 용량은:

$ du -sh .git
295M    .git

--depth 1로 복제하는 경우:

$ du -sh .git
25M     .git

10배의 차이가 있다.


GitHub Actions와 같이 배포 시스템을 구축하는 경우 최신 리비전만 필요한 경우가 많다.

https://github.com/actions/checkout 프로젝트는 저장소에 접근하기 위해서 많이 사용하는데, 기본적으로 --depth 1 옵션을 사용한다.

이 설정은 변경 가능하다:

with:
  # Number of commits to fetch. 0 indicates all history for all branches and tags.
  # Default: 1
  fetch-depth: ''

내 경우는 정적 사이트를 빌드하면서, 커밋 내역을 확인해서 파일의 정보를 사이트에 보여주는 기능이 있었는데, 기본값으로 사용하면서 제대로 정보를 보여줄 수 없었다.

git rebase -i

https://meetup.toast.com/posts/39

여러개의 커밋을 묶는데, git reset HEAD~# 후 다시 커밋을 생성하는 방법도 있지만, 여러개의 커밋을 남겼을 경우, 메시지들이 사라진다는 단점이 있다. 애초에 일련의 과정이 아니라, 수동으로 처리하는 행동 자체에서 꺼림칙함을 느낀다.

위 글은 git rebase -i를 이용하여 어떤 커밋을 하나로 합칠지 알려준다. 하지만 정말 유용한 기능 하나가 빠져있는데, 커밋 순서를 정렬할 수 있는 것이다. 이는 git rebase -i하면 나오는 설명에도 나온다 These lines can be re-ordered;

각 커밋을 의미하는 라인을 다시 정렬하면 git history가 그렇게 바뀐다.

예를들어 A라는 작업과 B라는 작업이 있다. A는 기능 하나를 추가하는 것이고, B는 A 작업을 하다보니 파일을 옮기고, 스타일을 바꾸는 작업들을 했다.

* 5d31146 (HEAD -> master) A2
* 90bb25a B
* b94056d A1
* 5fc47ec A
* 325da60 init

문제는 A 작업을 처리하기 위해서 3개의 커밋을 남겼는데, 그 사이에 B 작업이 껴 있을 때다.

이 때 git rebase -i 325da60 수정할 수 있는 화면이 뜬다.

pick 5fc47ec A
pick b94056d A1
pick 90bb25a B
pick 5d31146 A2

# Rebase 325da60..5d31146 onto 325da60 (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

내가 원하는 히스토리는

B
A
init

이런 순서다.

밑에 커밋이 위로 합쳐지므로, 다음과 같이 바꾼다.

pick 5fc47ec A
squash b94056d A1
squash 5d31146 A2
pick 90bb25a B

# Rebase 325da60..5d31146 onto 325da60 (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

B를 가장 밑으로 빼고, A1과 A2는 squash로 바꾼다. 이러면 A와 B만 남는다.

이제 저장하고 나오면..

# This is a combination of 3 commits.
# This is the 1st commit message:

A

## This is the commit message #2:

A1

## This is the commit message #3:

A2

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Mon Mar 25 22:49:24 2019 +0900
#
# interactive rebase in progress; onto 325da60
# Last commands done (3 commands done):
#    squash b94056d A1
#    squash 5d31146 A2
# Next command to do (1 remaining command):
#    pick 90bb25a B
# You are currently rebasing branch 'master' on '325da60'.
#
# Changes to be committed:
#	modified:   README
#

A + A1 + A2에 대한 커밋 메시지를 작성하게 된다.

A

- 1
- 2
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Mon Mar 25 22:49:24 2019 +0900
#
# interactive rebase in progress; onto 325da60
# Last commands done (3 commands done):
#    squash b94056d A1
#    squash 5d31146 A2
# Next command to do (1 remaining command):
#    pick 90bb25a B
# You are currently rebasing branch 'master' on '325da60'.
#
# Changes to be committed:
#	modified:   README
#

위처럼 커밋메시지를 작성하고, log를 보면 의도한대로 정리된 것을 볼 수 있다.

$ glog
* e3c5f82 (HEAD -> master) B
* aa6f7ef A
* 325da60 init

만약 A와 B가 같은 파일을 작업하게 되면, 당연하게도 conflict 발생한다.

git revert -m

-m, --mainline 옵션은 merge commit을 되돌리는데 사용한다. merge는 2개의 커밋을 병합하는 것이므로, 둘 중 어느 상태로 돌릴 것인지 결정해야 한다.

Usually you cannot revert a merge because you do not know which side of the merge should be considered the mainline. - git revert --help

따라서 사용법은 다음과 같다: git revert -m 1 or git revert -m 2

revert는 새 커밋에 되돌리는 작업이 포함되므로 history로는 어떤 커밋을 선택했는지 알 수 없다.

친절하게도 커밋 메시지에 둘 중 어떤 커밋으로 되돌아가는지 알려준다:

Revert "Add a feature"

This reverts commit 5c54ea679164eaca0bab639667bfcebb88769e63, reversing
changes made to b73ce1b168428a561e2dbcac96f97defaffa0e36.

5c54ea 되돌려서 parent commit 중 하나인 b73ce1로 돌아간다. 물론 새로운 커밋이기 때문에 hash는 별개다.

git log

git log --graph

TL;DR

--date-order 로 피라미드 그래프 방지하기

git log --graph --abbrev-commit --decorate --date=relative --format=format:'%C(bold red)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(cyan)<%an>%C(reset)%C(bold yellow)%d%C(reset)' --all

git log를 그래프로 보기위해 이렇게 사용 중이다.

문제는 staging -> master 머지 커밋이 아래 이미지와 같이 피라미드로 보여진다.

pyramid graph

머지 커밋의 경우 2개의 부모를 가지고 있기 때문에, 두 부모 중 어느 것을 우선적으로 보여줄 지 힌트가 없다. 따라서 피라미드로 보여지는 것으로 추정한다.

--date-order 옵션을 추가하여, 시간 기준으로 보여주도록 옵션을 주면 완화된다:

git log --graph --abbrev-commit --decorate --date=relative --format=format:'%C(bold red)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(cyan)<%an>%C(reset)%C(bold yellow)%d%C(reset)' --all --date-order

with --date-order

옵션 설명

git log --help 에서 정렬과 관련된 내용을 확인하면 어떻게 정렬 방법에 대해서 설명하고 있다.

Commit Ordering
       By default, the commits are shown in reverse chronological order.

       --date-order
           Show no parents before all of its children are shown, but otherwise show commits in the commit timestamp order.

       --author-date-order
           Show no parents before all of its children are shown, but otherwise show commits in the author timestamp order.

       --topo-order
           Show no parents before all of its children are shown, and avoid showing commits on multiple lines of history intermixed.

           For example, in a commit history like this:

                   ---1----2----4----7
                       \              \
                        3----5----6----8---

           where the numbers denote the order of commit timestamps, git rev-list and friends with --date-order show the commits in the timestamp order: 8 7 6 5 4 3 2 1.

           With --topo-order, they would show 8 6 5 3 7 4 2 1 (or 8 7 4 2 6 5 3 1); some older commits are shown before newer ones in order to avoid showing the commits from two
           parallel development track mixed together.

--topo-order에 대한 내용을 보면

                   ---1----2----4----7
                       \              \
                        3----5----6----8---

위 그래프가 있을 때, 숫자는 시간 순서로 작성되었다고 하자.

  • --topo-order 8 6 5 3 7 4 2 1 순서로 표기한다.
  • --date-order 8 7 6 5 4 3 2 1 순서로 표기한다.

--date-order--author-date-order 비교

--date-order and --author-date-order comparison

왼쪽이 --date-order 오른쪽이 --author-date-order이다.

--follow

기본적으로 git log FILENAME은 현재 파일 이름에 대해서만 로그를 보여준다.

git log --follow FILENAME으로 파일이 이동하더라도 추적한다.

다음은 예시.

$ git log --pretty=format:"%ad %h %s" --date=short docs/wiki/book.md
2023-12-02 8520c0d1f Add frontmatters
2023-11-11 f5b670292 Revise book.md and jetbrains.md
2023-10-26 e5832cc77 Revise tennise inner game
2023-10-15 146a5d7b2 Revise book.md
2023-10-13 9ac5d1ea3 Add heads
2023-10-11 3c2f6a0c3 Update tennis inner game book
2023-10-09 3af35024d Update tennis inner game book
2023-09-14 740f1e230 Add tennis inner game
2023-07-22 ee34ec929 Update document headings
2023-01-08 a0fc19715 Update book.md to include "만들면서 배우는 클린 아키텍처"
2023-01-05 e89f4febd Update book
2023-01-01 e8b5e5e97 Update all documents to include their own titles
2023-01-01 de99d7338 Migrate book

Migrate book 커밋에서 파일 이동이 있었다.

--follow를 추가하면 Migrate book 커밋 이전 내용도 확인할 수 있다.

$ git log --follow --pretty=format:"%ad %h %s" --date=short docs/wiki/book.md
2023-12-02 8520c0d1f Add frontmatters
2023-11-11 f5b670292 Revise book.md and jetbrains.md
2023-10-26 e5832cc77 Revise tennise inner game
2023-10-15 146a5d7b2 Revise book.md
2023-10-13 9ac5d1ea3 Add heads
2023-10-11 3c2f6a0c3 Update tennis inner game book
2023-10-09 3af35024d Update tennis inner game book
2023-09-14 740f1e230 Add tennis inner game
2023-07-22 ee34ec929 Update document headings
2023-01-08 a0fc19715 Update book.md to include "만들면서 배우는 클린 아키텍처"
2023-01-05 e89f4febd Update book
2023-01-01 e8b5e5e97 Update all documents to include their own titles
2023-01-01 de99d7338 Migrate book
2020-06-12 0bd294112 Update tags
2018-07-23 1ef0e7f22 Update front matters
2018-07-06 1605cfcf4 폴더 구조 변경 및 개발 환경 개선
2018-01-11 1c18d58bd Update "Chocolate Problem"
2018-01-11 ebd76bb05 Add "Chocolate Problem"

git worktree

git worktree add <path> <branch>로 현재 프로젝트를 <path>에 생성하고 <branch>로 체크아웃한다. 현재 프로젝트와 연결된다. git에서는 작업 영역을 working tree라 부르니, 알아두면 좋겠다.

git worktree는 현재 작업중인 내용을 stash나 commit 등으로 저장하지 않고, 다른 작업을 처리할 때 유용하다. 다만, javakotlin 프로젝트 같이 IDE에서 인덱싱하여 작업 영역이 무거운 경우에는 비효율적일 수 있다. 새 worktree에서 다시 인덱싱을 하기 때문이다.

git worktree list로 목록을 확인할 수 있으며, 복사된 프로젝트나 원본 프로젝트에서도 확인 가능하다.

$ git worktree list
/Users/me/workspace/some-api         e9169a43 [staging]
/Users/me/workspace/some-api-new     e826395c [new-branch]

worktree가 사용하는 branch는 git branch에서 구분되어 표시된다:

$ git branch
* new-branch  # 현재 worktree에서 사용하는 branch
  master
+ staging     # 다른(원본) worktree

worktree를 제거하기 위해서는 git worktree remove <path>를 사용한다. Tab을 통한 경로 자동 완성이 된다. worktree에서 사용한 브랜치는 계속 유지된다.

git-bisect

git bisect 명령은 이진 탐색을 이용하여 버그가 처음 발생한 커밋을 찾는데 사용한다. 메뉴얼 상으로는 버그라고 하지만, 특정 커밋을 찾는데 사용할 수 있다고 보면 된다. bisect는 2등분한다는 의미다.

이진 검색을 사용하기 때문에, 아무리 커밋이 많아도 log(n) 안에 찾을 수 있다. 커밋이 1억개가 있더라도 27번만 검색하면 된다.

흐름은 올바른 커밋과 잘못된 커밋을 먼저 선정하면, git이 자동으로 중간 커밋으로 체크아웃한다. 사용자는 현재 커밋에서 동작을 확인하고 올바른지 잘못되었는지 알려주면, git이 다음 중간 커밋으로 체크아웃을 반복한다. 올바른 커밋과 잘못된 커밋이 변경되는 지점을 찾으면 git은 경계 지점을 찾아서 멈춘다.

다음은 사용 예시.

~/myproject                                                                      master*
❯ g bisect start
status: waiting for both good and bad commits

~/myproject                                                                      master|bisect
❯ g bisect bad HEAD
status: waiting for good commit(s), bad commit known

~/myproject                                                                      master|bisect
❯ g bisect good 9867149b9a0097a8830159a14ca23182828a352c
Bisecting: 36 revisions left to test after this (roughly 5 steps)
[c5adff3fa82925bbbf12dde653af264a54e002d6] V1.0.27

~/myproject                                                                      @c5adff3f|bisect
❯ g bisect good
Bisecting: 17 revisions left to test after this (roughly 4 steps)
[e31a874ebfdd7d254f8e90541d367b310b6168c0] V1.0.33

~/myproject                                                                      @e31a874e|bisect
❯ g bisect good
Bisecting: 8 revisions left to test after this (roughly 3 steps)
[c634c9cbbb6c023ae068dad96cf945e2295272ef] V1.0.37

~/myproject                                                                      @c634c9cb|bisect
❯ g bisect good
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[7977f9b61481a366a50afd263f57c88591e858f5] V1.0.39

~/myproject                                                                      @7977f9b6|bisect
❯ g bisect good
Bisecting: 1 revision left to test after this (roughly 1 step)
[3f30e3a9f617c7acd6e9310573564b3e56fecb30] V1.0.40

~/myproject                                                                      @3f30e3a9|bisect
❯ g bisect good
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[e0b7dbc3921062a6f06a1997efb891b0b1b6041d] imp: improve some logic

~/myproject                                                                      @e0b7dbc3|bisect
❯ g bisect bad
e0b7dbc3921062a6f06a1997efb891b0b1b6041d is the first bad commit
commit e0b7dbc3921062a6f06a1997efb891b0b1b6041d (HEAD, origin/imp-logic)
Author: John Doe
Date:  Fri Mar 25 22:49:24 2022 +0900

    imp: improve some logic



 src/foo.py
 src/bar.py
 src/tests/foo_test.py |
 3 files changed, 2 insertions(+), 238 deletions(-)

git bisect run 명령은 good, bad를 자동 판단하는 스크립트를 작성하여, 커밋 탐색을 자동화하는 명령이다.

git-reset

git reset은 HEAD를 이동시키는 명령어다. 즉, 작업 영역을 변경한다.

remote branch의 변경이 있으면, fatal: Not possible to fast-forward, aborting. 에러가 발생하는 fast-forward가 불가능한 경우를 피하기 위해서 일반적으로 git reset --hard origin/<branch>로 변경하곤 한다. 다만 문제는 브랜치 이름이 길어지면 입력이 번거롭다는 점. git completion으로 해결할 수 있지만, 별칭을 사용하는 것도 편리한 방법이다.

git reset --hard @{u}를 사용하면 upstream branch로 reset할 수 있다. @{u}@{upstream}의 축약어다.

# ~/workspace/my-project new-awesome-feature-wip-and-too-long-branch-name
$ g reset --hard @{u}
HEAD is now at 93a0251b work-in-progress

git-shortlog

git shortlog는 커밋 내역을 요약한다.

$ git shortlog

Alleb (2):
      Implment something
      Fix something

Bella (3):
      Initial commit
      Add something
      Update something

기본 옵션으로는 사용자 별로 커밋 내역을 요약한다.

-s 옵션은 커밋 수만 보여준다.

$ git shortlog -s
     2  Alleb
     3  Bella

Git Large File Storage(LFS)

Git Large File Storage는 대용량 파일의 버전 관리를 위한 도구이다.

Git은 리모트 저장소로부터 clone 받을 때 대용량 파일은 실제 파일이 아닌, 참조만 받아온다.
그래서 clone 받을 때 빠르게 받을 수 있다.

References:

Git LFS는 Git의 확장으로 분류한다:

An open source Git extension for versioning large files

git lfs 명령어로 제공하지만, Git에 내장된 것은 아니다. 별도 설치가 필요하다.
Linux, macOS는 brew install git-lfs로 설치 가능.

사용법

최근 Huggingface에서 모델을 다운로드 받고 실행해 보면서 처음 사용해 보았다.
.git 폴더는 모든 리비전에 대한 내용을 담고 있어서 그런지, 모델 저장소의 경우 용량이 매우 커졌다.

대용량 파일을 업로드 할 일이 없어서 업로드에 대한 내용은 생략한다.

git clone 전에 하거나 clone 후에 하는지에 따라 사용 방법이 다르다.


Clone 전

  1. git lfs install로 LFS 사용을 활성화한다. (비활성화는 git lfs uninstall)
$ git lfs install
Updated Git hooks.
Git LFS initialized.
  1. git clone 한다.

git lfs install은 한 번만 실행하면 전역으로 적용된다. 앞으로 clone 받는 저장소에 대해서 대용량 파일을 실제 파일로 받겠다는 의미다.

다운로드 진행 상황이 UI로 표시되지 않기 때문에 clone이 멈춘듯한 모습으로 보이지만, du -sh .git 명령어로 용량을 확인하면 계속 증가하는 것을 볼 수 있다.

git lfs install을 하지 않고 clone을 받는 것과 비교하면 완료 속도가 다른 것을 체감할 수 있다.


Clone 후

git lfs pull로 대용량 파일을 다운로드 받는다.

이 경우에도 멈춘듯한 모습으로 보이지만, 실제로는 다운로드가 진행된다.

도움말 git lfs pull --help에 다르면 git lfs fetch 명령어와 같다고 한다.
아마도 특정 파일만 다운로드 받을 수도 있는 모양.

커밋 서명하기

커밋의 서명을 확인하려면 git cat-file -p <commit-hash> 명령어를 사용한다.

다음은 mochajs 저장소의 커밋을 확인한 것이다.

$ git cat-file -p HEAD
tree 6c42701b4c621fa227bd211b6b52473e68004057
parent 37358738260cfae7c244c157aee21654f2b588f2
author ***** ***** <***************@*****.***> 1709903697 -0300
committer GitHub <noreply@github.com> 1709903697 -0500
gpgsig -----BEGIN PGP SIGNATURE-----
...
 -----END PGP SIGNATURE-----
...

커밋 서명은 GPG를 사용한다.

GPG CLI는 Homebrew로 설치했다: brew install gpg

GPG 키 생성: gpg --full-generate-key

--full-generate-key 옵션은 키 pair를 생성한다.

키 생성 시 알고리즘 등 키 정보와 사용자 정보를 입력한다.
GitHub의 GPG 키 생성 가이드를 참고했다.

키 정보는 모두 Enter로 기본 값을 선택했다.

  • 알고리즘: RSA
  • 키 사이즈: 3072
  • 만료 기간: 0(무제한)

사용자 정보는 이메일 주소만 입력했다. GitHub에 등록한 이메일 주소를 입력한다. 이메일을 감추고 싶다면 GitHub의 no-reply 이메일을 사용하라고 한다.

$ gpg --full-gen-key
gpg (GnuPG) 2.2.19; Copyright (C) 2019 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

gpg: keybox '/home/user/.gnupg/pubring.kbx' created
Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
  (14) Existing key from card
Your selection?
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072)
Requested keysize is 3072 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0)
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name:
Email address: *******@gmail.com
Comment:
You selected this USER-ID:
    "*******@gmail.com"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: /home/user/.gnupg/trustdb.gpg: trustdb created
gpg: key 7754F8835F1D4F23 marked as ultimately trusted
gpg: directory '/home/user/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/user/.gnupg/openpgp-revocs.d/EC2773EB41F9362E83E76B177754F8835F1D4F23.rev'
public and secret key created and signed.

pub   rsa3072 2024-03-11 [SC]
      EC2773EB41F9362E83E76B177754F8835F1D4F23
uid                      *******@gmail.com
sub   rsa3072 2024-03-11 [E]

GPG 키 확인: gpg --list-secret-keys --keyid-format=long

--list-secret-keys 옵션은 생성된 키 목록을 출력하고
--keyid-format=long 옵션은 키 ID를 출력한다.

$ gpg --list-secret-keys --keyid-format=long
/home/user/.gnupg/pubring.kbx
-------------------------------
sec   rsa3072/7754F8835F1D4F23 2024-03-11 [SC]
      EC2773EB41F9362E83E76B177754F8835F1D4F23
uid                 [ultimate] *******@gmail.com
ssb   rsa3072/9E8A974D370C5682 2024-03-11 [E]

7754F8835F1D4F23가 키 ID이다.

GPG 키 export: gpg --armor --export <key-id>

--armor 옵션은 공개 키 정보를 ASCII로 출력한다.

$ gpg --armor --export 7754F8835F1D4F23
-----BEGIN PGP PUBLIC KEY BLOCK-----

...
-----END PGP PUBLIC KEY BLOCK-----

GPG 키 GitHub에 등록하기

GitHub의 Settings > SSH and GPG keys > New GPG key에 공개키를 등록한다.

-----BEGIN PGP PUBLIC KEY BLOCK----------END PGP PUBLIC KEY BLOCK----- 내용을 모두 복사해서 붙여넣는다.

서명하기

git commit 명령어에 -S 옵션을 추가한다.

$ git commit -S -m "commit message"

-S 옵션 대신 git 설정 commit.gpgSigntrue로 설정하면 자동 서명된다.

-S 옵션은 key-id를 받지만, 생략하면 user.signingKey 설정을 사용한다. git config --global user.signingKey <key-id>로 설정하자.

만약, 키가 없으면 다음과 같이 실패한다.

$ git commit
error: gpg failed to sign the data:
gpg: skipped "edunga1 <*******@gmail.com>": No secret key
[GNUPG:] INV_SGNR 9 edunga1 <*******@gmail.com>
[GNUPG:] FAILURE sign 17
gpg: signing failed: No secret key

fatal: failed to write commit object

키가 있으면 passphrase 입력을 요구한다.

Troubleshooting

Git commit 시 "Waiting for your editor to close the file..." 메시지와 함께 커밋이 안되는 문제

git commit -v로 커밋 메시지 작성 후 zz 또는 :wq로 저장하여 나와도 커밋이 안된다. 약 3번 중 1번 꼴로 발생한다.

❯ g commit -v
hint: Waiting for your editor to close the file... error: There was a problem with the editor 'nvim'.
Please supply the message using either -m or -F option.

Startify의 세션 저장 기능이 원인이었다.

function! GetUniqueSessionName()
  let path = fnamemodify(getcwd(), ':~:t')
  let path = empty(path) ? 'no-project' : path
  return substitute(path, '/', '-', 'g')
endfunction

autocmd VimLeavePre * execute 'SSave! ' . GetUniqueSessionName()

vim을 종료할 때 세션을 저장하고, Startify의 시작 화면에 Session 목록을 노출하도록 설정했었다. 이 설정을 제거하니까 몇 번의 테스트에도 커밋 실패가 발생하지 않았다. SSave의 문제인지, GetUniqueSessionName의 문제인지는 모르겠다.

제거 커밋: https://github.com/Edunga1/dotfiles/commit/9998b7c454e321d48d326e20da56af2328055a46