AI Dev Lab
Git道場

Git道場 ができるまで — 純粋TypeScriptだけで作るGitシミュレーターの設計と実装

isomorphic-gitやmemfsを使わず、純粋TypeScriptだけでGitの状態機械を実装。コミットスナップショット設計・ブランチ切り替え・マージ処理の実装詳細と道場アーキテクチャ4作目の開発記録。

·decision保留·stage保留で観察

「Gitって結局何をしてるの?」

プログラミングを始めてしばらく経った人でも、Gitの内部動作が腑に落ちていないケースは多い。git addしてgit commitしてgit push——コピペで動かせるけど、なぜこの順番なのか説明できない。

手を動かしながら、仕組みを体感できる場所が必要だ。

それが Git道場 の出発点です。CmdDojo・SQL道場・Regex道場に続く「道場シリーズ」第4弾として、純粋TypeScriptのGitシミュレーターをブラウザ上に実装しました。


🎯 なぜGit練習サービスなのか

道場シリーズのブランド化

CmdDojo(Linux)→ SQL道場 → Regex道場 と来て、次のステップとして「Git道場」は自然な流れでした。

技術者が必ず通るコマンドラインツールを、インストール不要で学べる。そのコンセプトの延長にGitは完全に合致しています。

SEO的な狙い

「Git 入門」「git コマンド 練習」「ブランチ 使い方」——これらの検索クエリは常に需要があります。特に:

  • プログラミング初学者が最初につまずくのがGit
  • 「とりあえずVSCodeのGUIを使う」で済ませてコマンドを理解していない中級者
  • チーム開発でブランチ戦略を学びたいエンジニア

これらの層に、ブラウザで即座に試せる環境を提供できれば価値があります。


🏗️ 最大の設計課題:Gitシミュレーターをどう作るか

SQL道場はsql.jsというWebAssemblyの実装があったため、「本物のSQLエンジン」をそのまま使えました。ところがGitには同等のものがない。

選択肢を検討しました:

選択肢メリットデメリット
isomorphic-git実際のGit実装memfsとの統合が複雑・バンドルサイズ大
jsgit軽量メンテ停止・機能不足
純TS実装完全制御・軽量作り込みが必要
バックエンドでGit実行完全互換サーバーコスト・レイテンシ

純TypeScriptの「Gitシミュレーター」を自前実装することにしました。

理由は明快です。学習目的であれば、Gitの全機能を再現する必要はない。init, add, commit, branch, checkout, merge, log, diff, status ——これらのコマンドの振る舞いを正確に模倣できれば十分です。


⚙️ GitEngineの設計

状態モデル

Gitの状態を以下の型で表現しました:

export interface GitState {
 initialized: boolean
 workingDir: Record<string, string>  // ファイル名 → 内容
 index: Record<string, string>  // ステージング
 commits: GitCommit[]
 branches: Record<string, string>  // ブランチ名 → コミットID
 currentBranch: string | null
 detachedHead: string | null
}

export interface GitCommit {
 id: string
 message: string
 files: Record<string, string>  // コミット時の全ファイルスナップショット
 parent: string | null
 timestamp: number
}

重要な設計判断が1つあります。各コミットにファイルの完全スナップショットを持つことにしました。

本物のGitはdiff(差分)を保存しますが、シミュレーターとしては「全スナップショット方式」のほうが実装が格段にシンプルです。ブランチ切り替え時にファイルを復元するコードがObject.assign(state.workingDir, commit.files)の一行で済みます。

コマンドパーサー

processCommand(input: string, state: GitState)という純粋関数がコマンドを処理します:

export function processCommand(
 input: string,
 state: GitState
): { output: string; newState: GitState }

入力を受け取り、出力文字列と新しい状態を返す。副作用なし。これにより:

  • テストが容易(状態 → コマンド → 状態の変化を検証するだけ)
  • Zustandとの統合がシンプルexecuteInputでnewStateをマージするだけ)
  • Undo機能の実装も容易(状態のスタックを持てばよい)

主要コマンドの実装例

git checkout -b <branch> の実装:

case "checkout": {
 const bFlag = args.includes("-b")
 const branchName = args.find(a => !a.startsWith("-"))

 if (bFlag) {
  // ブランチ作成 + 切り替え
 const newState = {
 ...state,
 branches: { ...state.branches, [branchName]: currentCommitId },
 currentBranch: branchName,
 }
 return { output: `Switched to a new branch '${branchName}'`, newState }
 }
  // 既存ブランチへの切り替え
 const targetCommit = getCommit(state, state.branches[branchName])
 return {
 output: `Switched to branch '${branchName}'`,
 newState: {
 ...state,
 workingDir: { ...targetCommit.files },
 currentBranch: branchName,
 }
 }
}

📚 レッスン設計

コース構成

「Git入門」コースを5レッスンで構成しました:

レッスンタイトル練習問題ポイント
01Gitの基本操作5問60pt
02履歴の確認 — log と diff5問60pt
03ブランチ — 並行開発の基本5問70pt
04マージ — 変更を統合する4問60pt
05実践ワークフロー5問90pt

バリデーション関数の設計思想

CmdDojo・Regex道場との違いとして、Git道場のバリデーションはコマンド文字列ではなく状態を検証します:

// ❌ コマンドを見るだけの単純な検証(学習効果が低い)
validate: (input) => input.trim() === "git checkout feature"

// ✅ 状態を見る検証(ユーザーが本質を理解しているか確認)
validate: (_input, state) => state.currentBranch === "feature"

後者のアプローチなら、git switch feature でも git checkout feature でも正解になります。コマンドの丸暗記ではなく、Gitの状態変化を理解しているかを評価できます。

レッスン5:実践ワークフロー

最終レッスンでは、実際の開発現場に近いフローを体験させます:

1. git checkout -b feature/login # 機能ブランチを作成
2. touch login.html # ファイルを作成
3. echo "..." >> login.html # 内容を追記
4. git add . && git commit -m "add login page"
5. git checkout main # mainに戻る
6. git merge feature/login # マージ
7. git branch -d feature/login # ブランチを削除

このフローをシミュレーター上で体験することで、「なぜブランチを作るのか」「なぜmainに戻ってからマージするのか」が体感できます。


🎨 UIデザイン:パープルテーマ

道場シリーズで採用している2ペインレイアウトを踏襲しつつ、Git道場はパープル(#a855f7)をアクセントカラーに採用しました:

  • CmdDojo:ティール(#4ec9b0
  • SQL道場:ブルー(#3b82f6
  • Regex道場:グリーン(#22c55e
  • Git道場:パープル(#a855f7

ターミナルのプロンプトがブランチ名を表示する設計にしました:

(main) $ git checkout -b feature/login
Switched to a new branch 'feature/login'
(feature/login) $

ブランチが変わるとプロンプトが変わる——これだけで「ブランチを切り替えるということ」が視覚的に伝わります。


📦 技術スタック

Next.js 15 (App Router) + TypeScript
Zustand (状態管理・進捗永続化)
CSS Modules (スタイル隔離)

外部依存は追加ゼロ。GitエンジンはTypeScriptのみ、UIはZustandとCSS Modules。バンドルサイズへの影響は最小限です。

Zustandの永続化設計

セッションをまたいだ学習進捗はZustandのpersistミドルウェアでlocalStorageに保存。ただし永続化するのは進捗データのみ、GitState(仮想リポジトリの状態)はセッション変数として保持します:

const useGitDojoStore = create(
 persist(
 (set, get) => ({ /* ... */ }),
 {
 name: "git-dojo-store",
 partialize: (state) => ({
 progress: state.progress,  // 永続化
 hintsShown: state.hintsShown,
  // gitState は永続化しない(レッスン開始時にリセット)
 }),
 }
 )
)

🔢 開発工数

作業時間
設計・アーキテクチャ検討1h
GitEngine実装(コマンドパーサー)2h
レッスン・コース定義(5レッスン×クイズ)2h
UI実装(GitDojoApp・各コンポーネント)2h
CSS Modules(パープルテーマ)1h
ページ実装(トップ・コース・レッスン)0.5h
デプロイ・E2Eテスト0.5h
合計約9時間

Claude Codeによる生成AIペアプロを活用。コマンドパーサーのboilerplate、レッスンの問題文・ヒント・解説、CSSの色変数あたりはほぼ一発で生成できました。


💡 作ってわかったこと

「状態ベースの採点」は強力

コマンド文字列ではなく状態を見る設計にしたことで、予期せぬ副産物がありました。ユーザーが途中のコマンドを間違えても、最終的な状態が正しければ正解になるのです。

git add . でも git add README.md でも、ステージされていれば正解。コマンドの暗記より目的の達成を優先した採点になります。

の学習ツールの可能性

isomorphic-gitを使わなかった選択は正解でした。純TypeScriptのシミュレーターは:

  • 初回ロードが速い(WASMなし)
  • オフライン動作(service workerなしでも)
  • 完全な制御(エラーメッセージを学習に最適化できる)

本物のGitと100%互換である必要はない。**学習体験として最適化された「Gitっぽいもの」**で十分です。


🚀 今後の展望

現在は「Git入門」コース(5レッスン)のみですが、以下のコースを追加予定です:

  • ブランチ戦略(feature/develop/release/hotfix)
  • リモートとの連携(fetch/pull/push/clone)
  • 上級操作(rebase/cherry-pick/stash)

また、Git道場は「道場シリーズ」の中でも最も実用性が高いカテゴリです。「Git 使い方」系のSEOで流入を取れれば、他のサービスへの回遊も期待できます。


まとめ

Git道場を作ってみて、「環境なしで学べる」という価値の大きさを改めて実感しました。

Gitを学びたい人が一番困るのは「まず環境を作る」ところです。ブラウザでURLを開いた瞬間にgit initが打てる——その手軽さが学習の入口として機能します。

道場シリーズはこれで4サービス。次は何を作るか、ユーザーの反応を見ながら考えます。

[ ./next_action ]

読んだら、 Git道場 を実際に動かす。

この開発ログは Git道場 をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — 文章embeddingsで意味検索をブラウザ内に

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →