はじめに
はじめまして、ニフティの高垣です。
【筆者プロフィール】
- 入社時期:2025年4月
- 入社前のスキル:
- PythonやReactを主に使用
- gitを使ったチーム開発経験はハッカソン(2,3日程度)のみ
- 現在の担当:第一開発チーム(ニフティトップページやニフくじなどを担当)
みなさんはGitを使って開発をしたことがありますか?Gitはファイルの変更履歴などを記録するバージョン管理システムです。もし一度でも使ったことがあるなら、「git add
」「git commit
」「git push
」といったコマンドを、もはや指が覚えているかもしれません。
また、使ったことがない方にとっては、今後チームで開発する際の必須のツールとなります。そんな風に誰もが当たり前のように使っているこれらのコマンドですが、裏側で一体何が起きているのか、深く考えたことはありますか?
そこで、この記事ではGitコマンドを使用せずにcommitをする方法を紹介します。
このプロセスを通じて、Gitがどのように変更履歴を記録しているのか、その仕組みを理解する一助となれば幸いです。(Gitについてご存知ない方は弊社の新人研修の資料も併せてご覧ください)
一般的なcommitまでの流れ
一般的なcommitまでの流れは以下のようになります。
1. git init
2. ファイルの編集
3. git add (編集したファイル)
4. git commit
まずは、必要なファイルを用意するためにgit init
します。その後、ファイルを編集し、git add
で変更を知らせて、commit
します。
今回は、こちらの手順を手動で行います。
実行環境
この記事で紹介するコマンド操作は、macOSを想定しています。
また、手順の途中でPythonとGit(確認するために使用)が必要になるため、あらかじめインストールしておいてください。
Windowsをご利用の方へ
Windows環境で進める場合は、Git for Windowsに付属する「Git Bash」とPythonをインストールしていただくことで、同様の操作が可能です。
git init
git initとは
git initコマンドはGitで必要なリポジトリやファイルを作成するコマンドです。基本的には初回のみこのコマンドを使用します。
git initを実行すると.gitという隠しディレクトリが作成されます。このディレクトリの中でGitに関する全てのファイルを格納しています。
作成されるもの
具体的には以下のようなディレクトリを作成しています。
- .git/hooks: 特定のGitイベントが発生した際に自動的に実行されるスクリプトを配置
- .git/info:excludeを配置(gitで追跡されないルール)
- .git/objects/: オブジェクト(後述)を配置
- (/ハッシュ値の上2桁)
- /info: 追加情報
- /pack: パックファイルを配置する
- .git/refs: 特定のコミットハッシュを指すポインタを保存する
- /heads: ローカルリポジトリのブランチが指すコミットハッシュを格納
- /remotes: リモートリポジトリのブランチが指すコミットハッシュを格納
- /tags: タグの参照を格納
- .git/logs: 各ブランチやHEADで実行したアクションを記録する
手順
では、さっそくgit initを手動で行なっていきます。
1.作業用のフォルダを作成します
1 |
$ mkdir git_manual |
2.作成したフォルダに移動します
1 |
$ cd git_manual |
3.Gitリポジトリの初期設定に必要なディレクトリ構造を作成します
1 |
$ mkdir -p .git/hooks .git/info .git/objects/info .git/objects/pack .git/refs/heads .git/refs/remotes .git/refs/tags .git/logs |
.git/を作成した後は、configの設定と現在のブランチを設定します。
configでは、ファイルの実行権限の変更を追跡するかどうか、ファイル名の大文字・小文字を区別するかどうかなどを設定します。
4..git/configを作成し必要な情報を記述します
$ cat <<EOF > .git/config
を実行した後以下をコピペしてください。
1 2 3 4 5 6 7 8 |
[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true EOF |
5..git/HEADを作成し、mainブランチをHEADにします
1 |
$ echo "ref: refs/heads/main" > .git/HEAD |
git statusコマンドを使用して、ここまで上手く行けているかどうか確認してみましょう。
1 2 3 4 5 6 |
$ git status On branch main No commits yet nothing to commit (create/copy files and use "git add" to track) |
上記が表示されたらOKです。
(補足: git initをすると実際にはhookのサンプルも生成されますが、今回は必要ないため省略しています)
commit
commitの仕組みについて
続いてcommitをしていきますが、その前にcommitの仕組みについて説明します。
commitをする際、
- コミットメッセージ
- タイムスタンプ
- コミットを作成した人物の情報
- コミットした人物の情報
- ファイルの差分
- コミット時点でのディレクトリの階層構造と全ファイル(treeオブジェクト)
などの情報を保存する必要があります。Gitでは、これら全てをまとめてコミットオブジェクトとして管理しています。コミットオブジェクトは一意に識別するためにハッシュ値(SHA-1)を生成し、それをキーとしています。(この仕組みをコンテンツアドレスストレージと呼びます)
コミットオブジェクトはディレクトリの階層構造や全ファイルの情報を毎回保存します。そのため、Gitにおけるcommitとはファイルの「差分」を記録するのではなく、リポジトリの「スナップショット」を記録しています。
commitの流れ
大まかなcommitの流れは、
1. ファイル内容をGitが管理できる形に変換する(BLOBの作成)
2.インデックスファイルの作成
3. treeオブジェクトを作成
4. コミットオブジェクトを作成
5. HEADにハッシュ値を登録
となっています。
インデックスファイルの作成が、git add
に該当し、それ以降がgit commit
に該当します。
コミットオブジェクトを作成するだけでは、現在のブランチで一番最新のコミットがどれなのかわからないため、Gitに知らせる必要があります。
そこで、.git/refs/headsにコミットオブジェクトのハッシュ値を保存します。これにより、最新のコミットを一意に特定することができます。
また、BLOB・インデックスファイル・treeオブジェクト・コミットオブジェクトの作成は次の図ような手順で作成されます。

先ほど、Gitにおけるcommitとはリポジトリの「スナップショット」を記録していると述べましたが、そのまま保存するのはあまり賢い方法ではありません。
そこで、オブジェクトを圧縮することで容量を効率的に消費しています。
commitの手順
それでは、今回の目玉であるcommitをしていきます。
1.適当なファイルを追加します(例としてexample.txtを作成)
1 |
$ echo "first commit" > example.txt |
2.コミットオブジェクトの作成
先ほどの図を見ると、BLOB・treeオブジェクト・コミットオブジェクトの作成手順は基本的に共通しています。また、それぞれの工程で前のステップで生成したハッシュ値が必要となります。
そこで今回は、これら一連の流れを自動化できるよう、コミットオブジェクトを作成するシェルスクリプトを作成しました。
以下のシェルスプリクトをcommit.shとして保存してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
#!/bin/bash set -euo pipefail # 使い方: ./manual_commit.sh <ファイル名> "<コミットメッセージ>" if [ "$#" -ne 2 ]; then echo "Usage: $0 <ファイル名> \"<コミットメッセージ>\"" exit 1 fi FILE="$1" MSG="$2" GIT_DIR=".git" # --- Step 1: BLOBオブジェクトの作成 --- echo "==============================" echo "🟢 BLOBオブジェクトの作成" echo "------------------------------" # ファイル内容からBLOBオブジェクトの生データを作成 tmp_blob=$(mktemp) trap 'rm -f "$tmp_blob"' EXIT blob_size=$(wc -c <"$FILE" | tr -d '[:space:]') { printf "blob %s\0" "$blob_size"; cat "$FILE"; } > "$tmp_blob" # ハッシュを計算し、zlibで圧縮して.git/objectsに保存 blob_hash=$(shasum "$tmp_blob" | cut -d' ' -f1) path="$GIT_DIR/objects/${blob_hash:0:2}/${blob_hash:2}" mkdir -p "$(dirname "$path")" python3 -c "import sys,zlib;sys.stdout.buffer.write(zlib.compress(sys.stdin.buffer.read()))" < "$tmp_blob" > "$path" echo "BLOB 作成完了: $blob_hash" echo "==============================" echo "" echo "==============================" echo "🟢 インデックスファイルの手動作成" echo "------------------------------" # --- Step 2: インデックスファイルの作成 --- python3 -c ' import sys, os, hashlib, struct, binascii def create_index(file_path, blob_hash_hex): header = struct.pack(">4sII", b"DIRC", 2, 1) # Signature, Version 2, 1 Entry stat_info = os.stat(file_path) ctime_sec = int(stat_info.st_ctime) ctime_nsec = stat_info.st_ctime_ns % 10**9 mtime_sec = int(stat_info.st_mtime) mtime_nsec = stat_info.st_mtime_ns % 10**9 dev = stat_info.st_dev ino = stat_info.st_ino mode = stat_info.st_mode uid = stat_info.st_uid gid = stat_info.st_gid size = stat_info.st_size blob_hash_bin = binascii.unhexlify(blob_hash_hex) path_bytes = file_path.encode("utf-8") # ファイル名の長さをフラグとして使用 flags = len(path_bytes) entry = struct.pack( ">IIIIIIIIII20sH", ctime_sec, ctime_nsec, mtime_sec, mtime_nsec, dev, ino, mode, uid, gid, size, blob_hash_bin, flags ) + path_bytes + b"\x00" # パディングを追加して8バイト境界に揃える entry_len_with_pad = (len(entry) + 7) & ~7 entry += b"\x00" * (entry_len_with_pad - len(entry)) index_content = header + entry checksum = hashlib.sha1(index_content).digest() with open(".git/index", "wb") as f: f.write(index_content + checksum) if __name__ == "__main__": create_index(sys.argv[1], sys.argv[2]) ' "$FILE" "$blob_hash" echo "インデックス作成完了" echo "==============================" echo "" # --- Step 3: treeオブジェクトの作成 --- echo "==============================" echo "🟢 treeオブジェクトの作成" echo "------------------------------" # インデックスを読み取る代わりに、前のステップで得た変数を使用します mode="100644" blob_hash_text="$blob_hash" file_name="$FILE" tmp_tree=$(mktemp) tmp_tree_header=$(mktemp) trap 'rm -f "$tmp_tree" "$tmp_tree_header"' EXIT { printf "%s %s\0" "$mode" "$file_name"; echo -n "$blob_hash_text" | xxd -r -p; } > "$tmp_tree" tree_size=$(wc -c <"$tmp_tree" | tr -d '[:space:]') { printf "tree %s\0" "$tree_size"; cat "$tmp_tree"; } > "$tmp_tree_header" # ハッシュを計算し、圧縮して保存 tree_hash=$(shasum "$tmp_tree_header" | cut -d' ' -f1) path=".git/objects/${tree_hash:0:2}/${tree_hash:2}" mkdir -p "$(dirname "$path")" python3 -c "import sys,zlib;sys.stdout.buffer.write(zlib.compress(sys.stdin.buffer.read()))" < "$tmp_tree_header" > "$path" echo "treeオブジェクト作成完了: $tree_hash" echo "==============================" echo "" # --- Step 4: コミットオブジェクトの作成 --- echo "==============================" echo "🟢 コミットオブジェクトの作成" echo "------------------------------" author="hoge <hoge@example.com>" timestamp=$(date +'%s %z') # コミットオブジェクトの生データを作成 commit_content="tree $tree_hash author $author $timestamp committer $author $timestamp $MSG " tmp_commit=$(mktemp) trap 'rm -f "$tmp_commit"' EXIT commit_size=$(printf "%s" "$commit_content" | wc -c | tr -d '[:space:]') { printf "commit %s\0" "$commit_size"; printf "%s" "$commit_content"; } > "$tmp_commit" # ハッシュを計算し、圧縮して保存 commit_hash=$(shasum "$tmp_commit" | cut -d' ' -f1) path=".git/objects/${commit_hash:0:2}/${commit_hash:2}" mkdir -p "$(dirname "$path")" python3 -c "import sys,zlib;sys.stdout.buffer.write(zlib.compress(sys.stdin.buffer.read()))" < "$tmp_commit" > "$path" echo "コミットオブジェクト作成完了: $commit_hash" echo "==============================" echo "" |
シェルスクリプトに実行権限を与えてください
1 |
$ chmod +x commit.sh |
第1引数に編集したファイル、第2引数にコミットメッセージを入力して実行してください
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ ./commit.sh example.txt "first commit" ============================== 🟢 BLOBオブジェクトの作成 ------------------------------ BLOB 作成完了: 5ec586d228b5ff1e8c845c4ed8c2d01f3a159b24 ============================== ============================== 🟢 インデックスファイルの手動作成 ------------------------------ インデックス作成完了 ============================== ============================== 🟢 treeオブジェクトの作成 ------------------------------ treeオブジェクト作成完了: 2a154faaf71f2b3a17055d393504c1349395b049 ============================== ============================== 🟢 コミットオブジェクトの作成 ------------------------------ コミットオブジェクト作成完了: aa5e60c3c7b817707e4c4f2e29995451406bb319 ============================== |
完了するとそれぞれのハッシュ値が表示されます。このうち、コミットオブジェクトのハッシュ値をGitに知らせます。
3.HEADにハッシュ値を登録します
1 |
$ echo "aa5e60c3c7b817707e4c4f2e29995451406bb319" > .git/refs/heads/main |
4.commitできたか確認します
1 2 3 4 5 6 |
$ git log --show-signature commit aa5e60c3c7b817707e4c4f2e29995451406bb319 (HEAD -> main) Author: hoge <hoge@example.com> Date: Tue Aug 19 16:27:35 2025 +0900 first commit |
ハッシュ値やAuthorが表示されていれば完了です。お疲れ様でした!!
補足
今回は、BLOBを作成した後にインデックスファイルを作成しましたが、インデックスファイルを作成せずに、コミットオブジェクトを作成することも可能です。その場合に、面白い挙動が見られますので、シェルスクリプトを変更してぜひチャレンジしてみてください。
終わりに
今回は、手動でcommitをしてみました。Gitは非常に便利なツールであり、普段何気なく使っているコマンドは、私たちの代わりに複雑な処理を自動で実行してくれます。しかし、その裏側にある論理的な仕組みを理解することは単なる豆知識ではありません。これはGitに限らず、あらゆるツールに共通することであり、本質的な理解やトラブルシュートの力につながります。
この記事が、普段は意識しない部分にも目を向けるきっかけとなり、技術への理解をより深めるきっかけとなれば幸いです。
この記事を読んだ学生さんに向けて
入社して約4ヶ月ですが、弊社は本当に様々なことに挑戦できる環境だと感じています。
私の場合は新しいインフラ環境の構築や、企画会議でのファシリテーターなどに取り組みました。これらは全て初挑戦でしたが、どの場面でも周囲の手厚いサポートがあり、大きく成長できたと実感しています。
様々な技術に触れてみたい方、開発だけでなく企画にも参加してみたい方にとっては、弊社は非常に魅力的な環境だと思います。
また、弊社の新人研修で使用している資料も公開していますので、興味のある方はぜひこちらもご覧ください。
どうぞご期待ください。