uchan note

プログラミングや電子工作の話題を書きます

「30 日でできる!OS 自作入門」のような段階的に進展するプログラムについての解説書の執筆を Git で支援する

プログラムについての解説書を Git を用いて執筆する方法の提案です.

まえがき

プログラミングに関する解説書には大きく分けて 2 つの系統があります. 1 つはテーマごとにサンプルプログラムを提示して解説するもの,もう 1 つは特定のプログラムの各部分について解説するものです. 後者の中でも,完成形を用いるものと,段階的に進展する各時点での断面(スナップショット)を解説するものがあります. この記事は,後者の執筆を Git で支援するというものです. この形態の書籍としては「30 日でできる!OS 自作入門」が有名でしょう. OS の作り方を 30 章に分け,各章で少しずつ OS を作り上げていきます.

ロールプレイングゲームRPG)の作り方を説明する書籍を書くとします. 章立てはこんな感じになるかもしれません.

  1. プレイヤーキャラクタだけが登場するゲームループの基本形
  2. 扉を開ける,階段を上り下りする,NPC と会話する,というようなイベント
  3. 敵とのエンカウントと戦闘システム
  4. ゲーム全体のシナリオ

書籍では各段階の実装方法を記述し,また付録として各段階のスナップショットをディレクトリに分けて配布したいです. すなわち,次のようなファイル構成で執筆を進めていくことを考えます. 最終的に,text 以下をビルドした結果が書籍になり,読者には src 以下をそのまま配布することになります.

/               書籍のルートディレクトリ
    Makefile    書籍のビルドスクリプト(原稿から PDF などを生成する)
    text/       原稿ディレクトリ(Markdown とか Sphinx とか.今回は Re:VIEW を仮定)
    src/        対象のゲームプログラムのソースコード(付録用)
        sec1/   第 1 章に対応するソースコードのスナップショット
        secN/

Re:VIEW を使った執筆モデル

Re:VIEW という出版支援ツールがあります. 技術書典 という技術系の同人誌即売会でも割と人気のツールです. 専用のマークアップ記法を使って原稿を書くと,HTML,PDF,ePubInDesign などに向けて原稿を出力できます. 私は TeX よりも簡単にかけて,Markdown より表現力が高いので最近はずっと Re:VIEW を使っています.

段階的に進展するプログラムの解説書を Re:VIEW で書く場合,Re:VIEW が持つソースコード埋め込み機能1が便利です. これは,本文中に外部のファイル全体,またはファイルの中でマーキングした範囲を取り込む機能です. 本文中に手動でソースコードをコピペするのとやっていることは同じですが,コピペが自動化されることでソースコード更新時に本文への反映し忘れがありません.

「まえがき」で紹介したファイル構成になっていれば,第 N 章のソースコードを埋め込みたいときは secN ディレクトリの中のファイルを参照します. こうすることで,同一のファイルが各章で更新されていくようなプログラムの作り方をしたとしても,各章の時点でのソースコードを埋め込むことが可能です.

スナップショットの作成

さて,スナップショットをどのように作成するか( srcN ディレクトリの中身をどうやって生成するか)が工夫のしどころです. 原始的にやるなら,対象のゲームプログラムのファイルを手動でコピーすることができます. 対象プログラムが Git で管理されていて,各章に対応するバージョンにタグが付いているなら, git checkout TAG としてから必要なファイルを srcN にコピーすれば良いですね.

この方法は,ゲームプログラムの開発がスムーズに進行する間は何も問題がありません(もしくは,ゲームプログラムが完成している場合). しかし,執筆が進んでからゲームプログラムの初期のコミットにミスが発覚すると大問題です.

例えば,ゲームプログラムの開発が第 3 章の内容に差し掛かったときに,第 1 章で作ったプレイヤーキャラクタの名前が typo していることに気づいたとしましょう. 普通のゲームプログラム開発であれば,プレイヤーキャラクタの名前を修正するコミットを master に対して追加するだけですが,今やっているのは解説書のためのゲーム開発です. 第 1 章で引用するソースコードにおいてもプレイヤーの名前が正しくなっているべきです. Git のコミットの歴史を改ざんして,あたかも最初からプレイヤーの名前が正しかったかのようなコミットログにする必要があります. この場合,典型的にはプレイヤー名を実装したときのコミットを修正し,修正コミットに対して master を rebase することになります.

D 第 3 章実装中のコミット
↑
C 第 2 章実装中のコミット
↑
B 第 1 章実装中のコミット(typo 含む)
↑
A 初期コミット

という歴史を次のように書き換えたいです.

D' 第 3 章実装中のコミット
↑
C' 第 2 章実装中のコミット
↑
B' 第 1 章実装中のコミット(ammend したもの)
↑
A 初期コミット

これは Git の操作としてはそれほど難しくありません. また,書籍が出版されるまではきっとゲームプログラムのリポジトリを非公開にしておくと思いますので,上記のような歴史改ざんを行っても他人に迷惑をかけることはありません.安心してできます.

さて,ゲームプログラム側の修正はできましたが,この変更を第 3 章以前のすべてのスナップショットディレクトリ( src1 から src3 まで)に反映する必要があります. この作業は typo の修正パッチをすべてのディレクトリに適用するようなものです. スナップショットを細かく切っていればいるほど,パッチの適用作業が大変になります. すべての必要なパッチ適用を漏らさない自信があるでしょうか.自動化したくなるはずです.

git-extract-tags

ここで登場するのが私が作ったツール git-extract-tags です. このツールは,指定した Git リポジトリの各タグに対応するファイルをコピーする機能を持ちます.

ゲームプログラムの Git リポジトリで,書籍の各章に対応するタグを rpgbook-sec1 などと付けておきます. 書籍で引用するためのタグには統一されたプレフィクス(ここでは rpgbook- )を付けておく必要があります. この状態で git-extract-tags を使うと次のように動作します.

  • そのプレフィクスで始まるタグを検索する.
  • 各タグの時点でのリポジトリのファイルを git archive で取り出す.
  • タグ名からプレフィクスを取り除いたディレクトリ(例えば sec1 )にファイルを展開する.

この機能を使うと,手動でスナップショットのファイルをコピーしたり,バグ修正後にパッチを当ててまわる作業を自動化できます.

具体例

例えば次のようなファイル構成だったとします.

rpgbook/        書籍の Git リポジトリ
    Makefile
    text/
    src/
myrpg/          ゲームプログラムの Git リポジトリ
    ソースコード群

myprg リポジトリにはゲームプログラムのソースコードが格納されていて,書籍の各章に対応する rpgbook-secN のようなタグ(軽量タグ)が適切なコミットに付いているとします. この時,次のコマンドを実行すると,各タグに対応するソースコードrpgbook/src/secN に展開されます.

$ cd rpgbook
$ git extract-tags rpgbook- ../myrpg

git-extract-tags PREFIX PROG_REPO という引数です. PREFIX には書籍用のプレフィクスを指定します. PROG_REPO には対象プログラムのリポジトリへのパスを指定します.

--src-base SRC_BASE オプションにより展開先のディレクトリを src 以外に変更できます. --add-ignore オプションを指定すると src/secN.gitignore に自動的に追加します.

注意点がいくつかあります.

  • git-extract-tags は .git ディレクトリの位置を自動検出するので,書籍リポジトリのルートディレクトリ以外で実行した場合でも rpgbook/src/secN にファイルを展開します.
  • ファイルを展開する前に src/secN ディレクトリを完全に消去するので,ゲームプログラムのリポジトリに存在しないファイルがあった場合,消えてしまいます.

git-extract-tags を使って快適な執筆ライフを!

git-extract-tags のセットアップ

git-extract-tags は GitPython に依存します. pip install gitpython でインストールしてください.

後は git-extract-tags を PATH が通ったディレクトリに置くだけです. $HOME/bin に PATH が通っているとしたら,こんな感じにするといいと思います.

$ cd path/to/somewhere
$ git clone https://github.com/uchan-nos/git-extract-tags.git
$ cd ~/bin
$ ln -s path/to/somewhere/git-extract-tags/git-extract-tags ./