Skip to content

Instantly share code, notes, and snippets.

@shiv3
Last active December 5, 2023 16:13
Show Gist options
  • Save shiv3/5d9e139f2166200fbb64d9f3c2413c27 to your computer and use it in GitHub Desktop.
Save shiv3/5d9e139f2166200fbb64d9f3c2413c27 to your computer and use it in GitHub Desktop.
git add

git addを知る

Advent Calendarネタが全然無いのでgitを作ることにしました。

Git

gitは言わずと知れたバージョン管理ツールです。 多分エンジニアとして生きていく上で一番お世話になっているツールであり、今後もそうだと思います。ですが、gitの使い方は雰囲気分かっていてもgit自体についてあまり知らなかったのでとりあえずgoで作ってみることにしました。

Gitを調べる

ディレクトリ構造

とりあえず空のディレクトリでgit initを実行し、作成されるファイルを見てみます。

$ mkdir git
$ cd git
$ git init
$ tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

git add

git addは、ファイルをステージングエリアに追加するコマンドです。

ステージングエリア

ステージングエリアとは、git addしたファイルが保存される場所です。 git addしたファイルは、ステージングエリアに保存され、git commitしたときに、ステージングエリアにあるファイルがコミットされます。 簡単にいうと、ステージングエリアは、次にコミットするファイルを保存しておく場所です。

では、ステージングエリアに保存されるファイルはどこに保存されているのでしょうか。

ステージングエリアについて調べる

git addの挙動

ここで、git addの挙動を確認するために、git initで作成された.gitディレクトリ自体にもgitを設定しておきます

$ cd .git
$ git init 
$ git add .
$ git commit -m "initial git status"
[master (root-commit) e8097c2] initial git status
 17 files changed, 806 insertions(+)
 create mode 100644 HEAD
...

ファイルを作成し、git addしてみます。

$ touch test.txt
$ git add test.txt

.gitディレクトリのindexファイルとobjectsディレクトリにファイルが作成されていることがわかります。

$ git status -u
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	index
	objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391

2つともバイナリファイルとして作成されています。

$ hexdump -C index
00000000  44 49 52 43 00 00 00 02  00 00 00 01 65 6c 7d 01  |DIRC........el}.|
00000010  2f 35 d7 6e 65 6c 7d 01  2f 35 d7 6e 01 00 00 0e  |/5.nel}./5.n....|
00000020  06 61 a5 1f 00 00 81 a4  00 00 01 f6 00 00 00 14  |.a..............|
00000030  00 00 00 00 e6 9d e2 9b  b2 d1 d6 43 4b 8b 29 ae  |...........CK.).|
00000040  77 5a d8 c2 e4 8c 53 91  00 08 74 65 73 74 2e 74  |wZ....S...test.t|
00000050  78 74 00 00 21 ec 61 8d  3b c7 43 20 62 cc ad 7a  |xt..!.a.;.C b..z|
00000060  af 77 ba e0 be b5 c4 b0                           |.w......|
00000068

$ hexdump -C objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
00000000  78 01 4b ca c9 4f 52 30  60 00 00 09 b0 01 f0     |x.K..OR0`......|
0000000f

これらのファイルはそれぞれ、

  • blobオブジェクトファイル
  • indexファイル

というものです。

gitオブジェクトファイル

gitオブジェクトファイルは、gitの中心的な概念で、gitの中で扱う全てのデータは、このオブジェクトファイルとして保存されます。

オブジェクトファイルには、以下のような種類があります。

  • blob
  • tree
  • commit
  • tag

ここではステージングエリアに保存されるblobオブジェクトについて紹介します。

blobオブジェクト

blobオブジェクトは、gitで扱うファイルの中身を保存するオブジェクトです。 blobオブジェクトは、ファイルにblobファイルサイズというヘッダ情報を付加し、zlibで圧縮したものです。 実際にblobファイルを作ってみましょう

func saveObjectFile(file *os.File) error {
	// ファイルサイズを取得
	stat, err := file.Stat()
	if err != nil {
		return err
	}
	fileSize := stat.Size()
	// ファイルサイズからファイル名を取得
	h := getHash(int(fileSize))
	blobFileName := hex.EncodeToString(h)
	targetPath := ".git/objects/" + blobFileName[:2]

	// ディレクトリがなければ作成
	// ディレクトリ名はblobファイル名の先頭2文字
	if _, err := os.Stat(targetPath); os.IsNotExist(err) {
		err := os.Mkdir(targetPath, 0755)
		if err != nil {
			return err
		}
	}

	// ファイル名からblobオブジェクトファイルを作成
	blobFile, err := os.Create(targetPath + "/" + blobFileName[2:])
	if err != nil {
		return err
	}
	defer blobFile.Close()

	var content bytes.Buffer
	// ファイルのヘッダーを書き込み
	content.WriteString(fmt.Sprintf("blob %d\x00", fileSize))
	// ファイルの中身を書き込み
	_, err = io.Copy(&content, file)

	// zlibで圧縮して書き込み
	zw := zlib.NewWriter(blobFile)
	defer zw.Close()
	_, err = io.Copy(zw, &content)
	if err != nil {
		return err
	}
	fmt.Printf("blob file saved to %s\n", targetPath+"/"+blobFileName[2:])
	return nil
}

func getFileName(fileSize int) string {
	// ファイルサイズとblobのヘッダーを結合してsha1でハッシュ化
	str := fmt.Sprintf("blob %d\x00", fileSize)
	sha1 := sha1.New()
	io.WriteString(sha1, str)
	hash := hex.EncodeToString(sha1.Sum(nil))
	return hash
}

indexファイル

次にindexファイルを作成します。

indexファイルは

  • 12バイトのヘッダー。
  • ソートされたindexエントリ で構成されます。

indexエントリ

エントリというのはインデックスにあるファイルの情報のことで、以下のような構造体になっています。 具体的にはstat(2)の結果、ハッシュ、パス名などを格納しています。 ハッシュは前述のBlob ObjectのIDで、パス名はファイル名です。

git-goを参考に以下のようにstructを定義してみます。

type Index struct {
	Version         uint32
	Entries         []*Entry
}


type Entry struct {
	Hash         [20]byte
	Name         string
	CreatedAt    time.Time
	ModifiedAt   time.Time
	Dev, Inode   uint32
	Mode         uint32
	UID, GID     uint32
	Size         uint32
	Stage        uint8 // 1: merged Z: ancestor 2: ours 3: theirs
	SkipWorktree bool
	IntentToAdd  bool
}

Stage

ここで、Stageというのは、git addしたときに、ステージングエリアに追加されたファイルのことです。 git addしたときに、ステージングエリアに追加されたファイルは、Stageが0になります。 git addした後に、ファイルを変更すると、Stageが2になります。 git addした後に、ファイルを削除すると、Stageが1になります。 git addした後に、ファイルを削除して、その後ファイルを変更すると、Stageが3になります。

indexファイルの作成

indexファイルを作成するには、以下のようなコードを書きます。

var (
	indexSignature    = [4]byte{'D', 'I', 'R', 'C'}
	entryHeaderLength = 62
)

func EncodeIndex(w io.Writer, index Index) error {
	// ヘッダーを書き込み
	if err := encodeIndexHeader(w, index); err != nil {
		return err
	}
	// エントリーを書き込み
	if err := encodeIndexEntries(w, index); err != nil {
		return err
	}
	return nil
}
func encodeIndexHeader(w io.Writer, index Index) error {
	bin := []interface{}{
		indexSignature,
		index.Version,
		uint32(len(index.Entries)),
	}
	for _, v := range bin {
		if err := binary.Write(w, binary.BigEndian, v); err != nil {
			return err
		}
	}
	return nil
}

func encodeIndexEntries(w io.Writer, index Index) error {
	// エントリーを名前順にソート
	sort.Slice(index.Entries, func(i, j int) bool {
		return index.Entries[i].Name < index.Entries[j].Name
	})

	for _, entry := range index.Entries {
		flags := uint16(entry.Stage&0x3) << 12
		const nameMask = 0xFFF
		if l := len(entry.Name); l < nameMask {
			flags |= uint16(l)
		} else {
			flags |= nameMask
		}
		bin := []interface{}{
			uint32(entry.CreatedAt.Unix()), uint32(entry.CreatedAt.Nanosecond()),
			uint32(entry.ModifiedAt.Unix()), uint32(entry.ModifiedAt.Nanosecond()),
			entry.Dev,
			entry.Inode,
			entry.Mode,
			entry.UID,
			entry.GID,
			entry.Size,
			entry.Hash[:],
			flags,
		}
		for _, v := range bin {
			if err := binary.Write(w, binary.BigEndian, v); err != nil {
				return err
			}
		}
		if _, err := io.WriteString(w, entry.Name); err != nil {
			return err
		}

		padLen := 8 - (entryHeaderLength+len(entry.Name))%8
		_, err := w.Write(bytes.Repeat([]byte{'\x00'}, padLen))
		if err != nil {
			return err
		}
	}
	return nil
}

func NewEntry(f *os.File) (*Entry, error) {
	stat, err := f.Stat()
	if err != nil {
		return nil, err
	}
	var h [20]byte
	copy(h[:], getHash(int(stat.Size())))
	return &Entry{
		Hash:         h,
		Name:         filepath.Base(f.Name()),
		CreatedAt:    stat.ModTime(),
		ModifiedAt:   stat.ModTime(),
		Dev:          uint32(stat.Sys().(*syscall.Stat_t).Dev),
		Inode:        uint32(stat.Sys().(*syscall.Stat_t).Ino),
		Mode:         uint32(stat.Mode()),
		UID:          uint32(stat.Sys().(*syscall.Stat_t).Uid),
		GID:          uint32(stat.Sys().(*syscall.Stat_t).Gid),
		Size:         uint32(stat.Size()),
		Stage:        0,
		SkipWorktree: false,
		IntentToAdd:  false,
	}, nil
}

func NewIndex() *Index {
	return &Index{
		Version: 2,
		Entries: []*Entry{},
	}
}
func (i *Index) AddEntry(entry *Entry) {
	i.Entries = append(i.Entries, entry)
}

では、先程作成したblobファイルとともにindexファイルを作成してみましょう。

package main

import (
	"bytes"
	"compress/zlib"
	"crypto/sha1"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"syscall"
	"time"
)

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func run() error {
	path := os.Args[1]
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()
	if err := saveObjectFile(file); err != nil {
		return err
	}
	if err := saveIndexFile(file); err != nil {
		return err
	}
	return nil
}

上記のコードをgit_add.goという名前で保存します。

$ mkdir gittmp
$ cd gittmp
$ git init

git_add.goを実行します。

go run ../gitgo/git_add.go ../git/test.txt

作成されたファイルをそれぞれ確認してみます。

$ hexdump -C .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
00000000  78 9c 4a ca c9 4f 52 30  60 00 04 00 00 ff ff 09  |x.J..OR0`.......|
00000010  b0 01 f0                                          |...|
00000013

$ hexdump -C .git/index
00000000  44 49 52 43 00 00 00 02  00 00 00 01 65 6c 7d 01  |DIRC........el}.|
00000010  2f 35 d7 6e 65 6c 7d 01  2f 35 d7 6e 01 00 00 0e  |/5.nel}./5.n....|
00000020  06 61 a5 1f 00 00 01 a4  00 00 01 f6 00 00 00 14  |.a..............|
00000030  00 00 00 00 e6 9d e2 9b  b2 d1 d6 43 4b 8b 29 ae  |...........CK.).|
00000040  77 5a d8 c2 e4 8c 53 91  00 08 74 65 73 74 2e 74  |wZ....S...test.t|
00000050  78 74 00 00                                       |xt..|
00000054

git statusを実行すると、test.txtがステージングエリアに追加されていることがわかります。

git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   test.txt

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	deleted:    test.txt

また、作成されたblobファイルが正しく読めるかについても確認してみます。 blobファイルはgit cat-fileで確認できます。

$ echo "test" > test2.txt
$ go run ../gitgo/git_add.go test2.txt
blob file saved to .git/objects/26/aec756de006da7efb3cf1ed7579562a428f91a
$ git cat-file -p 26aec756de006da7efb3cf1ed7579562a428f91a
test

まとめ

git addの挙動を確認するために、git initで作成される.gitディレクトリにgit addで作成されるindexファイルとblobファイルをgoのコード作成し、そのファイルがgitコマンドで読めることを確認しました。

参考

package main
import (
"bytes"
"compress/zlib"
"crypto/sha1"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"syscall"
"time"
)
func main() {
if err := run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func run() error {
path := os.Args[1]
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
if err := saveObjectFile(file); err != nil {
return err
}
if err := saveIndexFile(file); err != nil {
return err
}
return nil
}
func saveObjectFile(file *os.File) error {
// ファイルサイズを取得
stat, err := file.Stat()
if err != nil {
return err
}
fileSize := stat.Size()
// ファイルサイズからファイル名を取得
h := getHash(int(fileSize))
blobFileName := hex.EncodeToString(h)
targetPath := ".git/objects/" + blobFileName[:2]
// ディレクトリがなければ作成
// ディレクトリ名はblobファイル名の先頭2文字
if _, err := os.Stat(targetPath); os.IsNotExist(err) {
err := os.Mkdir(targetPath, 0755)
if err != nil {
return err
}
}
// ファイル名からblobオブジェクトファイルを作成
blobFile, err := os.Create(targetPath + "/" + blobFileName[2:])
if err != nil {
return err
}
defer blobFile.Close()
var content bytes.Buffer
// ファイルのヘッダーを書き込み
content.WriteString(fmt.Sprintf("blob %d\x00", fileSize))
// ファイルの中身を書き込み
_, err = io.Copy(&content, file)
// zlibで圧縮して書き込み
zw := zlib.NewWriter(blobFile)
defer zw.Close()
_, err = io.Copy(zw, &content)
if err != nil {
return err
}
fmt.Printf("blob file saved to %s\n", targetPath+"/"+blobFileName[2:])
return nil
}
func getHash(fileSize int) []byte {
// ファイルサイズとblobのヘッダーを結合してsha1でハッシュ化
str := fmt.Sprintf("blob %d\x00", fileSize)
sha1 := sha1.New()
io.WriteString(sha1, str)
return sha1.Sum(nil)
}
func saveIndexFile(f *os.File) error {
targetPath := ".git/index"
target, err := os.Create(targetPath)
if err != nil {
return err
}
defer target.Close()
// 空のindexを作成
index := NewIndex()
// ファイルの情報をEntryを作成してindexに追加
entry, err := NewEntry(f)
if err != nil {
return err
}
index.AddEntry(entry)
// indexをファイルに書き込み
return EncodeIndex(target, *index)
}
type Index struct {
Version uint32
Entries []*Entry
}
type Entry struct {
Hash [20]byte
Name string
CreatedAt time.Time
ModifiedAt time.Time
Dev, Inode uint32
Mode uint32
UID, GID uint32
Size uint32
Stage uint8 // 1: merged Z: ancestor 2: ours 3: theirs
SkipWorktree bool
IntentToAdd bool
}
var (
indexSignature = [4]byte{'D', 'I', 'R', 'C'}
entryHeaderLength = 62
)
func EncodeIndex(w io.Writer, index Index) error {
// ヘッダーを書き込み
if err := encodeIndexHeader(w, index); err != nil {
return err
}
// エントリーを書き込み
if err := encodeIndexEntries(w, index); err != nil {
return err
}
return nil
}
func encodeIndexHeader(w io.Writer, index Index) error {
bin := []interface{}{
indexSignature,
index.Version,
uint32(len(index.Entries)),
}
for _, v := range bin {
if err := binary.Write(w, binary.BigEndian, v); err != nil {
return err
}
}
return nil
}
func encodeIndexEntries(w io.Writer, index Index) error {
// エントリーを名前順にソート
sort.Slice(index.Entries, func(i, j int) bool {
return index.Entries[i].Name < index.Entries[j].Name
})
for _, entry := range index.Entries {
flags := uint16(entry.Stage&0x3) << 12
const nameMask = 0xFFF
if l := len(entry.Name); l < nameMask {
flags |= uint16(l)
} else {
flags |= nameMask
}
bin := []interface{}{
uint32(entry.CreatedAt.Unix()), uint32(entry.CreatedAt.Nanosecond()),
uint32(entry.ModifiedAt.Unix()), uint32(entry.ModifiedAt.Nanosecond()),
entry.Dev,
entry.Inode,
entry.Mode,
entry.UID,
entry.GID,
entry.Size,
entry.Hash[:],
flags,
}
for _, v := range bin {
if err := binary.Write(w, binary.BigEndian, v); err != nil {
return err
}
}
if _, err := io.WriteString(w, entry.Name); err != nil {
return err
}
padLen := 8 - (entryHeaderLength+len(entry.Name))%8
_, err := w.Write(bytes.Repeat([]byte{'\x00'}, padLen))
if err != nil {
return err
}
}
return nil
}
func NewEntry(f *os.File) (*Entry, error) {
stat, err := f.Stat()
if err != nil {
return nil, err
}
var h [20]byte
copy(h[:], getHash(int(stat.Size())))
return &Entry{
Hash: h,
Name: filepath.Base(f.Name()),
CreatedAt: stat.ModTime(),
ModifiedAt: stat.ModTime(),
Dev: uint32(stat.Sys().(*syscall.Stat_t).Dev),
Inode: uint32(stat.Sys().(*syscall.Stat_t).Ino),
Mode: uint32(stat.Mode()),
UID: uint32(stat.Sys().(*syscall.Stat_t).Uid),
GID: uint32(stat.Sys().(*syscall.Stat_t).Gid),
Size: uint32(stat.Size()),
Stage: 0,
SkipWorktree: false,
IntentToAdd: false,
}, nil
}
func NewIndex() *Index {
return &Index{
Version: 2,
Entries: []*Entry{},
}
}
func (i *Index) AddEntry(entry *Entry) {
i.Entries = append(i.Entries, entry)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment