Git이 커밋데이터를 저장하는 방식

Table of Contents

1 .git

git init 이후 만들어지는 .git 폴더의 디렉토리 구조

config
description
HEAD
hooks/
info/
objects/
refs/

hooks/ 는 클라이언트|서버 훅을 관리한다. config 파일에는 해당 프로젝트에만 적용되는 설정 옵션이 있다.

objects/ 모든 컨텐츠(object, 개체)를 저장하는 데이터베이스 refs/ 커밋 개체(object)의 포인터(브랜, 태그, 리모트)를 저장한다. HEAD 현재 checkout한 브랜치를 가리킴 index staging area 정보를 저장한다.

2 Git 개체(object) 모델 개요

Git은 단순 Key-Value(파일이름-파일데이터) 구조이다.

Git은 두 개의 주요 자료구조가 있다.

  • object store : DVCS(Distributed Version Control System)을 지원하는 방식의 한 부분으로써 효율적으로 복제를 수행할 때, copy가 효율적으로 되도록 하기 위함.
  • index : 인덱스는 임시적인 리포지토리 전용(private) 정보이다. 필요시 임의로 저장|수정할 수 있다.

2.1 The SHA (Secure Hash Algorithm)

40-digit의 "object name"을 가진다. 아래처럼 생김

8hf67f4664981e4397625791c8eabbb5f2279a31

SHA1 해시함수를 개체의 내용(content of object)을 인자로 받아서 수행한다. 즉, 값이 다르면 다른 hash값이 나온다. 즉, 같은 내용물이면 같은 hash가 나온다.

여기서 장점은

  1. Git은 아주 빠르게 두 개체가 동일한지 아닌지 확인할 수 있다. (전체 content가 아니라 hash만 비교하면 되니까)

2.다른 리포지토리에서 저장된 동일한 컨텐츠는 항상 동일한 이름으로 저장된다.

2.2 Git Object Store (Git 개체가 저장하는 것)

원본 파일, 로그메시지, 저자 정보, 날짜 그리고 프로젝트 branch나 revision을 재구축하기 위해 필요한 정보들.

또한 모든 object는 아래 3개를 가진다. size : 내용의 사이즈 (size of content) content : type 따라 그에 맞는 content를 갖는다. type : .git/objects 한 곳에 여러 타입의 object를 모두 저장한다.

  1. blob : 파일데이터를 저장하는데 사용. (일반적으로 파일)
  2. tree : 트리는 기본적으로 디렉토리와 같다. 이 안에는 다른 트리와 blob이 있다.(서브디렉토리 와 파일)
  3. commit : 리포지토리 내 소개되는 각 변경의 메타데이터를 저장한다. (작성자, 커미터, 커밋데이터, 로그메시지)
  4. tag : 커밋된 특정 개체에 사람이 읽을 수 있도록 임의의 이름을 할당하는 것이다.

2.3 Subversion 과의 다른점

Serversion은 하나의 커밋과 그 다음 커밋과의 변경점만 저장한다. Git은 프로젝트 내 모든 파일의 snapshot를 커밋할 때마다 찍는다.

2.4 실습으로 보기 (Blob, Tree, Commit 타입은 어떻게 생기는가!)

$ git init test
$ cd test
$ touch index.txt
$ echo "yhnam" > index.txt
$ touch README.md
$ echo "yhnam README" > README.md
$ git add .
$ git commit -m "initial commit"

이제 만들어진 구조를 보자.

$ tree .git/objects
.git/objects
├── 69
│   └── e38ac9e9e6078d04c83e826d12c4cf41750d5f
├── 8b
│   └── 489c1af624eb25297c88176af71f7175015f0b
├── ca
│   └── 857a429caa2ddf9d46ed9a44267f6fcc87f785
├── d7
│   └── 6e599d899107369b5f877ade67c35776954194
├── info
└── pack

6 directories, 4 files

여기 보면 폴더마다 2개의 글자가 있다. Git은 40자의 체크섬(checksum, SHA-1) 해시를 각 개체마다 만들고, 앞의 두글자는 폴더에 놓는다. 각 object에 대해서 알아보자.

$ git cat-file -p 69e38ac9e9e6078d04c83e826d12c4cf41750d5f
100644 blob 8b489c1af624eb25297c88176af71f7175015f0b	README.md
100644 blob ca857a429caa2ddf9d46ed9a44267f6fcc87f785	index.txt

$ git cat-file -p 8b489c1af624eb25297c88176af71f7175015f0b
yhnam README

$ git cat-file -p ca857a429caa2ddf9d46ed9a44267f6fcc87f785
yhnam

$ git cat-file -p d76e599d899107369b5f877ade67c35776954194
tree 69e38ac9e9e6078d04c83e826d12c4cf41750d5f
author ssisksl77 <ssiskl77@gmail.com> 1641982046 +0900
committer ssisksl77 <ssiskl77@gmail.com> 1641982046 +0900

initial commit

우리는 두 파일을 커밋했다. 그결과로

  • blob 타입 object 2개( 8b489c1af624eb25297c88176af71f7175015f0b , ca857a429caa2ddf9d46ed9a44267f6fcc87f785)
  • tree 타입 object 1개( 69e38ac9e9e6078d04c83e826d12c4cf41750d5f )
  • commit 타입 object 1개( d76e599d899107369b5f877ade67c35776954194 )

그림으로 알아보자. (hash이름은 위 출력값과 다를 수 있다)

blob-object.png

Figure 1: Git Blob Type Object

tree-object.png

Figure 2: Git Tree Type Object

기본적으로 Tree 형식은 Root가 있기 마련이다! 즉, 모든 파일(모든 blob 개체)을 연결하고 있는 하나의 tree 개체가 있다. 다시 tree 개체를 보자.

git cat-file -p 69e38ac9e9e6078d04c83e826d12c4cf41750d5f
100644 blob 8b489c1af624eb25297c88176af71f7175015f0b	README.md
100644 blob ca857a429caa2ddf9d46ed9a44267f6fcc87f785	index.txt
  • 첫번째컬럼 : 100644은 file's permission을 말함.(100은 무시) 600 - 오너는 읽기/쓰기 가능 644 - 오너는 읽기/쓰기 가능. 그룹멤버/다른유저는 읽기 가능.
  • 두번째컬럼 : 타입을 말하는 듯. 위에서는 blob 을 말하고 있다.
  • 세번째컬럼 : 이 blob 의 해시
  • 네번째컬럼 : 파일명

아까 봤던 커밋메시지를 다시 보자. 출력한 정보 맨 위에는 커밋메시지가 연결되어 있는 트리를 보여주고 있다.

$ git cat-file -p d76e599d899107369b5f877ade67c35776954194
tree 69e38ac9e9e6078d04c83e826d12c4cf41750d5f
author ssisksl77 <ssiskl77@gmail.com> 1641982046 +0900
committer ssisksl77 <ssiskl77@gmail.com> 1641982046 +0900

initial commit

commit-object.png

Figure 3: Git Commit Type Object

3 git hash-object <data>

git hash-object <data> : .git/objects/ 디렉토리에 <data> 저장 및 접근할 수 있는 Key를 리턴.

$ git hash-object -h
usage: git hash-object [-t <type>] [-w] [--path=<file> | --no-filters] [--stdin] [--] <file>...
   or: git hash-object  --stdin-paths

   -t <type>             object type
   -w                    write the object into the object database
   --stdin               read the object from stdin
   --stdin-paths         read file names from stdin
   --no-filters          store file as is without filters
   --literally           just hash any random garbage to create corrupt objects for debugging Git
   --path <file>         process file as it were from this path

3.1 실습하기

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

아직 아무것도 없다. 추가해보자.

$ echo 'test content' | git hash-object -w --stdin

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

(여기서 -w 를 안하면 key는 리턴하지만 object는 수행하지 않는다. 즉, 변경사항이 없기 때문에 동일한 Key가 리턴된다.)

objects에 파일이 생겼다. Git은 데이터를 저장할 때 데이터와 헤더로 생성한 SHA-1 체크섬 으로 파일 이름을 짓는다. 해시의 처음 두 글자를 따서 디렉토리 이름에 사용하고 나머지 38글자를 파일 이름에 사용한다.

이제 각 변경사항마다 object를 만들어 보자.

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30

각 변경이 있을 때마다 hash가 생긴다. 이제 text.txt 를 지워도 살릴 수 있다.

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

4 Tree 개체를 깊게 들여다보자

4.1 cat-file 로 읽어보기

$ git cat-file -p master^{tree}

master^{tree} 구문은 master 브랜치가 갖는 Tree 개체를 말한다. 폴더를 추가 하나 더 해보고 위 구문을 수행해보자.

4.2 하나 만들어보기

Git은 Staging Area(Index)의 상태대로 Tree개체를 만들고 기록한다.

4.2.1 Staging Area에 파일 추가 후 index 만들기

update-indextest.txt 만 들어있는 index를 만든다. (Staging Area에 없는 파일이기 때문에 --add 추가 디렉토리파일이 아니라 데이터베이스에 있는 파일을 추가하기에 --cacheinfo 옵션 필요.)

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

4.2.2 Tree 개체로 저장하기

git write-tree 를 사용한다.

git write-tree
error: invalid object 100644 83baae61804e65cc73a7201a7252750c76066a30 for 'test.txt'
fatal: git-write-tree: error building trees

허허 test.txt 를 만들지 않고 추가했더니 안된다. 다시 해보자. 새 파일을 만들고 Git에 저장한다.

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

## 시작
$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt
$ git write-tree
ab24b11a30d2405206bd59165287845efbbf6554

$ git cat-file -t ab24b11a30d2405206bd59165287845efbbf6554
tree

$ git cat-file -p ab24b11a30d2405206bd59165287845efbbf6554
100644 blob 8b489c1af624eb25297c88176af71f7175015f0b	README.md
100644 blob c7b8df2aa621085bb6223f225ccdd19c4199c083	index.txt
040000 tree 7cf039735dff8e8575d086902da4efbccf2b7f48	lib
100644 blob 83baae61804e65cc73a7201a7252750c76066a30	test.txt

$ echo 'new file' > new.txt
$ git update-index --add --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt
$ git write-tree
4b6a39b87cf5d65ca4511c68a8c27f5090b506df

$ git cat-file -p 4b6a39b87cf5d65ca4511c68a8c27f5090b506df
100644 blob 8b489c1af624eb25297c88176af71f7175015f0b	README.md
100644 blob c7b8df2aa621085bb6223f225ccdd19c4199c083	index.txt
100644 blob fa49b077972391ad58037050f2a75f74e3671e92	new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a	test.txt

우리는 여기서 test.txt 만 보면 된다. test.txt 의 SHA값은 1f7a7a 이다. 우리는 이렇게 트리를 만들 수 있다.

더 나아가서 tree 개체를 하위 디렉토리로 만들어보자. 즉, 폴더 안으로 이동시킨다. (tree depth 증가시키기)

$ git read-tree --prefix=bak 7cf039735dff8e8575d086902da4efbccf2b7f48
$ git write-tree
0a8d1f6ba884f50667ccaa4bdc2715b363adf8ca

이렇게 하면 bak 디렉토리 안에 lib 트리가 있게 된다.

$ git cat-file -p 0a8d1f6ba884f50667ccaa4bdc2715b363adf8ca 
100644 blob 8b489c1af624eb25297c88176af71f7175015f0b	README.md
040000 tree 7cf039735dff8e8575d086902da4efbccf2b7f48	bak
100644 blob c7b8df2aa621085bb6223f225ccdd19c4199c083	index.txt
100644 blob fa49b077972391ad58037050f2a75f74e3671e92	new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a	test.txt

4.3 커밋개체

Tree 개체로 프로젝트 스냅샷은 만들었지만 아직 SHA-1를 외워야 한다는 점. 대체 누가 언제 왜 저장했는지에 대한 정보가 필요하다는 점. (그리고 이전 커밋이 뭔지도!)

커밋개체가 해결한다.

$ echo 'first commit' | git commit-tree 0a8d1f6ba884f50667ccaa4bdc2715b363adf8ca 
80fe4576ec01ca81ac1954bcb5b028f367e12bab

$ git cat-file -p 80fe4576ec01ca81ac1954bcb5b028f367e12bab
git cat-file -p 80fe4576ec01ca81ac1954bcb5b028f367e12bab
tree 0a8d1f6ba884f50667ccaa4bdc2715b363adf8ca
author ssisksl77 <ssiskl77@gmail.com> 1641990390 +0900
committer ssisksl77 <ssiskl77@gmail.com> 1641990390 +0900

first commit

커밋 개체 형식을 보자.

  • 맨 위는 최상단 Tree(루트 디렉토리)를 가리킨다.
  • user.name , user.email : 설정에서 가져온 author, commiter
  • 시간정보
  • echo 로 찍은 커밋메시지

4.3.1 commit-tree

이 함수로 만들 수 있다. Tree 개체와 SHA-1 값을 넘긴다.

$ mkdir lib
$ echo "HI" > lib/a.txt
$ git add .
$ git commit -m "add folder and file"
$ git cat-file -p master^{tree}
100644 blob 8b489c1af624eb25297c88176af71f7175015f0b	README.md
100644 blob c7b8df2aa621085bb6223f225ccdd19c4199c083	index.txt
040000 tree 7cf039735dff8e8575d086902da4efbccf2b7f48	lib

이전 커밋을 가리키도록 할 수 있다.

$ echo 'second commit' | git commit-tree <지금커밋> -p <예전커밋>

우리가 지금까지 만든 .git/objects 를 보고 끝내자.

$ find .git/objects -type f | awk -F'/' '{print($3$4)}'
69e38ac9e9e6078d04c83e826d12c4cf41750d5f
a3417ed29ce52de00849edc6d732fb46476348ce
d76e599d899107369b5f877ade67c35776954194
b35da2a112a058936be0d61dba8ebf32bac3aabd
abc40794b12174b59df43d8cd47d871e9aaff78e
ab24b11a30d2405206bd59165287845efbbf6554
c7b8df2aa621085bb6223f225ccdd19c4199c083
ca857a429caa2ddf9d46ed9a44267f6fcc87f785
edebb11f6207f100793fb16674b82364101fc71c
c1e3b52e700b18a2b8e1d2616e9277ab447bddd0
4b6a39b87cf5d65ca4511c68a8c27f5090b506df
7cf039735dff8e8575d086902da4efbccf2b7f48
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
80fe4576ec01ca81ac1954bcb5b028f367e12bab
103c423144db1ef971ef69dc02695fcf8957924b
0a8d1f6ba884f50667ccaa4bdc2715b363adf8ca
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
fa49b077972391ad58037050f2a75f74e3671e92
83baae61804e65cc73a7201a7252750c76066a30
8b489c1af624eb25297c88176af71f7175015f0b

$ find .git/objects -type f | awk -F'/' '{print($3$4)}' | xargs -n1 git cat-file -t
tree
commit
commit
tree
tree
tree
blob
blob
tree
blob
tree
tree
blob
commit
commit
tree
blob
blob
blob
blob

5 Reference

Date: 2022-01-12 Wed 00:00

Author: Younghwan Nam

Created: 2022-09-14 Wed 01:26

Emacs 27.2 (Org mode 9.4.4)

Validate