Git道場 ができるまで — 純粋TypeScriptだけで作るGitシミュレーターの設計と実装
「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レッスンで構成しました:
| レッスン | タイトル | 練習問題 | ポイント |
|---|---|---|---|
| 01 | Gitの基本操作 | 5問 | 60pt |
| 02 | 履歴の確認 — log と diff | 5問 | 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サービス。次は何を作るか、ユーザーの反応を見ながら考えます。