Comments
Description
Transcript
オオカミのプロパティバー
Pro Git Scott Chacon* 2014-07-01 *これは、書籍 Pro Git のPDF版です。クリエイティブ・コモンズ 表⽰ - ⾮営利 - 継承 3.0(CC BY-NC-SA 3.0)ライセンスの下に提供されています。Git を学ぶ際の助けになれば幸いです。もし役 にたったなら、書籍の⽅もぜひよろしくお願いします。 http://tinyurl.com/amazonprogit Pro Git ⽇本語版電⼦書籍(PDF/EPUB/MOBI)を http://progit-ja.github.io/ で公開しています。誤 訳の指摘、訳⽂の改善提案、その他ご意⾒がありましたら、そちらからお願いいたします。また、Pro Git ⽇本語版の翻訳、改善にご尽⼒いただいた皆さんに、厚く御礼申し上げます。 目次 1 使い始める 1 1.1 バージョン管理に関して . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1.1 ローカル・バージョン管理システム . . . . . . . . . . . . . . . . . . . . 1 1.1.2 集中バージョン管理システム . . . . . . . . . . . . . . . . . . . . . . . . 2 1.1.3 分散バージョン管理システム . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2 Git略史 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.3 Gitの基本 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.3.1 スナップショットで、差分ではない . . . . . . . . . . . . . . . . . . . . 5 1.3.2 ほとんど全ての操作がローカル . . . . . . . . . . . . . . . . . . . . . . . 5 1.3.3 Gitは完全性を持つ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.3.4 Gitは通常はデータを追加するだけ . . . . . . . . . . . . . . . . . . . . . 6 1.3.5 三つの状態 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.4 Gitのインストール . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 1.4.1 ソースからのインストール . . . . . . . . . . . . . . . . . . . . . . . . . . 8 1.4.2 Linuxにインストール . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1.4.3 Macにインストール . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1.4.4 Windowsにインストール . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.5 最初のGitの構成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.5.1 個⼈の識別情報 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 1.5.2 エディター . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 1.5.3 diffツール . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.5.4 設定の確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.6 ヘルプを⾒る . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.7 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2 Git の基本 15 2.1 Git リポジトリの取得 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.1.1 既存のディレクトリでのリポジトリの初期化 . . . . . . . . . . . . . . . 15 2.1.2 既存のリポジトリのクローン . . . . . . . . . . . . . . . . . . . . . . . . 16 2.2 変更内容のリポジトリへの記録 . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.2.1 ファイルの状態の確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.2.2 新しいファイルの追跡 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.2.3 変更したファイルのステージング . . . . . . . . . . . . . . . . . . . . . . 19 2.2.4 ファイルの無視 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.2.5 ステージされている変更 / されていない変更の閲覧 . . . . . . . . . . . 22 2.2.6 変更のコミット . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 iii 2.2.7 ステージングエリアの省略 . . . . . . . . . . . . . . . . . . . . . . . . . . 26 2.2.8 ファイルの削除 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 2.2.9 ファイルの移動 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 2.3 コミット履歴の閲覧 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 2.3.1 ログ出⼒の制限 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 2.3.2 ⽇時にもとづくログ出⼒の制限 . . . . . . . . . . . . . . . . . . . . . . . 35 2.3.3 GUI による歴史の可視化 . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 2.4 作業のやり直し . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 2.4.1 直近のコミットの変更 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 2.4.2 ステージしたファイルの取り消し . . . . . . . . . . . . . . . . . . . . . . 38 2.4.3 ファイルへの変更の取り消し . . . . . . . . . . . . . . . . . . . . . . . . 39 2.5 リモートでの作業 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 2.5.1 リモートの表⽰ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 2.5.2 リモートリポジトリの追加 . . . . . . . . . . . . . . . . . . . . . . . . . . 41 2.5.3 リモートからのフェッチ、そしてプル . . . . . . . . . . . . . . . . . . . 42 2.5.4 リモートへのプッシュ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 2.5.5 リモートの調査 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 2.5.6 リモートの削除・リネーム . . . . . . . . . . . . . . . . . . . . . . . . . . 44 2.6 タグ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 2.6.1 タグの⼀覧表⽰ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 2.6.2 タグの作成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 2.6.3 注釈付きのタグ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 2.6.4 署名付きのタグ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 2.6.5 軽量版のタグ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 2.6.6 タグの検証 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 2.6.7 後からのタグ付け . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 2.6.8 タグの共有 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 2.7 ヒントと裏技 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 2.7.1 ⾃動補完 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 2.7.2 Git エイリアス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 2.8 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3 Git のブランチ機能 55 3.1 ブランチとは . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.2 ブランチとマージの基本 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 3.2.1 ブランチの基本 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 3.2.2 マージの基本 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 3.2.3 マージ時のコンフリクト . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 3.3 ブランチの管理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 3.4 ブランチでの作業の流れ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 3.4.1 ⻑期稼働⽤ブランチ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 3.4.2 トピックブランチ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 3.5 リモートブランチ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 3.5.1 プッシュ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 3.5.2 追跡ブランチ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 3.5.3 リモートブランチの削除 . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 iv 3.6 リベース . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 3.6.1 リベースの基本 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 3.6.2 さらに興味深いリベース . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 3.6.3 ほんとうは怖いリベース . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 3.7 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 4 Git サーバー 85 4.1 プロトコル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 4.1.1 Local プロトコル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 利点 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 ⽋点 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 4.1.2 SSH プロトコル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 利点 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 ⽋点 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 4.1.3 Git プロトコル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 利点 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 ⽋点 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 4.1.4 HTTP/S プロトコル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 利点 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 ⽋点 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 4.2 サーバー⽤の Git の取得 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 4.2.1 ベアリポジトリのサーバー上への設置 . . . . . . . . . . . . . . . . . . . 91 4.2.2 ちょっとしたセットアップ . . . . . . . . . . . . . . . . . . . . . . . . . . 92 SSH アクセス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 4.3 SSH 公開鍵の作成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 4.4 サーバーのセットアップ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 4.5 ⼀般公開 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 4.6 GitWeb . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 4.7 Gitosis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 4.8 Gitolite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 4.8.1 インストール . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 4.8.2 インストールのカスタマイズ . . . . . . . . . . . . . . . . . . . . . . . . 105 4.8.3 設定ファイルおよびアクセス制御ルール . . . . . . . . . . . . . . . . . . 106 4.8.4 『拒否』 ルールによる⾼度なアクセス制御 . . . . . . . . . . . . . . . . 107 4.8.5 ファイル単位でのプッシュの制限 . . . . . . . . . . . . . . . . . . . . . . 108 4.8.6 個⼈ブランチ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 4.8.7 『ワイルドカード』 リポジトリ . . . . . . . . . . . . . . . . . . . . . . . 109 4.8.8 その他の機能 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 4.9 Git デーモン . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 4.10 Git のホスティング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 4.10.1 GitHub . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 4.10.2 ユーザーアカウントの作成 . . . . . . . . . . . . . . . . . . . . . . . . . . 112 4.10.3 新しいリポジトリの作成 . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 4.10.4 Subversion からのインポート . . . . . . . . . . . . . . . . . . . . . . . . 115 4.10.5 共同作業者の追加 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 4.10.6 あなたのプロジェクト . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 v 4.10.7 プロジェクトのフォーク . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 4.10.8 GitHub のまとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 4.11 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 5 Git での分散作業 119 5.1 分散作業の流れ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 5.1.1 中央集権型のワークフロー . . . . . . . . . . . . . . . . . . . . . . . . . . 119 5.1.2 統合マネージャー型のワークフロー . . . . . . . . . . . . . . . . . . . . 120 5.1.3 独裁者と若頭型のワークフロー . . . . . . . . . . . . . . . . . . . . . . . 121 5.2 プロジェクトへの貢献 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 5.2.1 コミットの指針 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 5.2.2 ⾮公開な⼩規模のチーム . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 5.2.3 ⾮公開で管理されているチーム . . . . . . . . . . . . . . . . . . . . . . . 130 5.2.4 ⼩規模な公開プロジェクト . . . . . . . . . . . . . . . . . . . . . . . . . . 135 5.2.5 ⼤規模な公開プロジェクト . . . . . . . . . . . . . . . . . . . . . . . . . . 138 5.2.6 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 5.3 プロジェクトの運営 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 5.3.1 トピックブランチでの作業 . . . . . . . . . . . . . . . . . . . . . . . . . . 142 5.3.2 メールで受け取ったパッチの適⽤ . . . . . . . . . . . . . . . . . . . . . . 142 apply でのパッチの適⽤ . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 am でのパッチの適⽤ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 5.3.3 リモートブランチのチェックアウト . . . . . . . . . . . . . . . . . . . . 146 5.3.4 何が変わるのかの把握 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 5.3.5 提供された作業の取り込み . . . . . . . . . . . . . . . . . . . . . . . . . . 148 マージのワークフロー . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 ⼤規模マージのワークフロー . . . . . . . . . . . . . . . . . . . . . . . . 151 リベースとチェリーピックのワークフロー . . . . . . . . . . . . . . . . 152 5.3.6 リリース⽤のタグ付け . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 5.3.7 ビルド番号の⽣成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 5.3.8 リリースの準備 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 5.3.9 短いログ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 5.4 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 6 Git のさまざまなツール 157 6.1 リビジョンの選択 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 6.1.1 単⼀のリビジョン . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 6.1.2 SHA の短縮形 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 6.1.3 SHA-1 に関するちょっとしたメモ . . . . . . . . . . . . . . . . . . . . . 158 6.1.4 ブランチの参照 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 6.1.5 参照ログの短縮形 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160 6.1.6 家系の参照 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161 6.1.7 コミットの範囲指定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 ダブルドット . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 複数のポイント . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 トリプルドット . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 6.2 対話的なステージング . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 vi 6.2.1 ファイルのステージとその取り消し . . . . . . . . . . . . . . . . . . . . 166 6.2.2 パッチのステージ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 6.3 作業を隠す . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 6.3.1 ⾃分の作業を隠す . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 6.3.2 隠した内容の適⽤の取り消し . . . . . . . . . . . . . . . . . . . . . . . . 172 6.3.3 隠した変更からのブランチの作成 . . . . . . . . . . . . . . . . . . . . . . 173 6.4 歴史の書き換え . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 6.4.1 直近のコミットの変更 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 6.4.2 複数のコミットメッセージの変更 . . . . . . . . . . . . . . . . . . . . . . 175 6.4.3 コミットの並べ替え . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 6.4.4 コミットのまとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 6.4.5 コミットの分割 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179 6.4.6 最強のオプション: filter-branch . . . . . . . . . . . . . . . . . . . . . . 180 全コミットからのファイルの削除 . . . . . . . . . . . . . . . . . . . . . . 180 サブディレクトリを新たなルートへ . . . . . . . . . . . . . . . . . . . . 180 メールアドレスの⼀括変更 . . . . . . . . . . . . . . . . . . . . . . . . . . 181 6.5 Git によるデバッグ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 6.5.1 ファイルの注記 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 6.5.2 ⼆分探索 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 6.6 サブモジュール . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 6.6.1 サブモジュールの作り⽅ . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 6.6.2 サブモジュールを含むプロジェクトのクローン . . . . . . . . . . . . . . 187 6.6.3 親プロジェクト . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190 6.6.4 サブモジュールでの問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . 190 6.7 サブツリーマージ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192 6.8 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195 7 Git のカスタマイズ 197 7.1 Git の設定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 7.1.1 基本的なクライアントのオプション . . . . . . . . . . . . . . . . . . . . 198 core.editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 commit.template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 core.pager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 user.signingkey . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 core.excludesfile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 help.autocorrect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 7.1.2 Git における⾊ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 color.ui . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 color.* . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 7.1.3 外部のマージツールおよび Diff ツール . . . . . . . . . . . . . . . . . . . 201 7.1.4 書式設定と空⽩⽂字 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204 core.autocrlf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204 core.whitespace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 7.1.5 サーバーの設定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 receive.fsckObjects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 receive.denyNonFastForwards . . . . . . . . . . . . . . . . . . . . . . 206 vii receive.denyDeletes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 7.2 Git の属性 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 7.2.1 バイナリファイル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 バイナリファイルの特定 . . . . . . . . . . . . . . . . . . . . . . . . . . . 208 バイナリファイルの差分 . . . . . . . . . . . . . . . . . . . . . . . . . . . 208 MS Word ファイル . . . . . . . . . . . . . . . . . . . . . . . . . . 209 OpenDocument Text ファイル . . . . . . . . . . . . . . . . . . . 210 画像ファイル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211 7.2.2 キーワード展開 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 7.2.3 リポジトリをエクスポートする . . . . . . . . . . . . . . . . . . . . . . . 215 export-ignore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 export-subst . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216 7.2.4 マージの戦略 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216 7.3 Git フック . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 7.3.1 フックをインストールする . . . . . . . . . . . . . . . . . . . . . . . . . . 217 7.3.2 クライアントサイドフック . . . . . . . . . . . . . . . . . . . . . . . . . . 217 コミットワークフローフック . . . . . . . . . . . . . . . . . . . . . . . . 217 Eメールワークフローフック . . . . . . . . . . . . . . . . . . . . . . . . . 218 その他のクライアントフック . . . . . . . . . . . . . . . . . . . . . . . . 218 7.3.3 サーバーサイドフック . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 pre-receive および post-receive . . . . . . . . . . . . . . . . . . . . . . 219 update . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 7.4 Git ポリシーの実施例 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 7.4.1 サーバーサイドフック . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 特定のコミットメッセージ書式の強制 . . . . . . . . . . . . . . . . . . . 220 ユーザーベースのアクセス制御 . . . . . . . . . . . . . . . . . . . . . . . 222 Fast-Forward なプッシュへの限定 . . . . . . . . . . . . . . . . . . . . . 224 7.4.2 クライアントサイドフック . . . . . . . . . . . . . . . . . . . . . . . . . . 226 7.5 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230 8 Gitとその他のシステムの連携 231 8.1 Git と Subversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 8.1.1 git svn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 8.1.2 準備 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232 8.1.3 はじめましょう . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233 8.1.4 Subversion へのコミットの書き戻し . . . . . . . . . . . . . . . . . . . . 235 8.1.5 新しい変更の取り込み . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236 8.1.6 Git でのブランチに関する問題 . . . . . . . . . . . . . . . . . . . . . . . 238 8.1.7 Subversion のブランチ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 新しい SVN ブランチの作成 . . . . . . . . . . . . . . . . . . . . . . . . . 239 8.1.8 アクティブなブランチの切り替え . . . . . . . . . . . . . . . . . . . . . . 239 8.1.9 Subversion コマンド . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 SVN 形式のログ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 SVN アノテーション . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 SVN サーバ情報 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 Subversion が無視するものを無視する . . . . . . . . . . . . . . . . . . 242 viii 8.1.10 Git-Svn のまとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242 8.2 Git への移⾏ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 8.2.1 インポート . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 8.2.2 Subversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 8.2.3 Perforce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 8.2.4 カスタムインポーター . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 8.3 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254 9 Gitの内側 255 9.1 配管(Plumbing)と磁器(Porcelain) . . . . . . . . . . . . . . . . . . . . . . 255 9.2 Gitオブジェクト . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256 9.2.1 ツリーオブジェクト . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259 9.2.2 コミットオブジェクト . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 9.2.3 オブジェクトストレージ . . . . . . . . . . . . . . . . . . . . . . . . . . . 264 9.3 Gitの参照 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266 9.3.1 HEADブランチ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 9.3.2 タグ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269 9.3.3 リモート . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270 9.4 パックファイル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 9.5 参照仕様(Refspec) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274 9.5.1 参照仕様へのプッシュ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276 9.5.2 参照の削除 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277 9.6 トランスファープロトコル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277 9.6.1 無⼝なプロトコル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277 9.6.2 スマートプロトコル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280 データのアップロード . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281 データのダウンロード . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282 9.7 メインテナンスとデータリカバリ . . . . . . . . . . . . . . . . . . . . . . . . . . 283 9.7.1 メインテナンス . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283 9.7.2 データリカバリ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284 9.7.3 オブジェクトの除去 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287 9.8 要約 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 ix 第1章 使い始める この章は、Gitを使い始めることに関してになります。まずはバージョン管理システム の背景に触れ、その後にGitをあなたのシステムで動かす⽅法、そしてGitで作業を始める ための設定⽅法について説明します。この章を読み終えるころには、なぜGitが広まって いるか、なぜGitを使うべきなのか、それをするための準備が全て整っているだろうとい うことを、あなたはきっと理解しているでしょう。 1.1 バージョン管理に関して バージョン管理とは何でしょうか、また、なぜそれを気にする必要があるのでしょう か? バージョン管理とは、変更を⼀つのファイル、もしくは時間を通じたファイルの集 合に記録するシステムで、そのため後で特定バージョンを呼び出すことができます。現実 にはコンピューター上のほとんどあらゆるファイルのタイプでバージョン管理を⾏なう事 ができますが、本書の中の例では、バージョン管理されるファイルとして、ソフトウェア のソースコードを利⽤します。 もしあなたが、グラフィックス・デザイナー、もしくはウェブ・デザイナーであって、 (あなたが最も確実に望んでいるであろう)画像もしくはレイアウトの全てのバージョン を管理したいのであれば、バージョン管理システム(VCS)はとても賢く利⽤できるもの です。VCSを使ってできることとしては、ファイルを以前の状態まで戻したり、プロジェ クト丸ごとを以前の状態に戻したり、過去の変更を⾒直したり、誰が最後に問題を引き起 こすだろう何かを修正したか、誰が、何時、課題を導⼊したかを確認したりといった様々 なことがあります。VCSを使うということはまた、⼀般的に、何かをもみくちゃにする か、ファイルを失うとしても、簡単に復活させることができることを意味します。加え て、とても僅かな諸経費で、それら全てを得ることができます。 1.1.1 ローカル・バージョン管理システム 多くの⼈々の選り抜きのバージョン管理⼿法は、他のディレクトリ(もし彼らが賢いの であれば、恐らく⽇時が書かれたディレクトリ)にファイルをコピーするというもので す。このアプローチは、とても単純なためにすごく⼀般的ですが、信じられない間違い傾 向もあります。どのディレクトリにいるのか忘れやすいですし、偶然に間違ったファイル に書き込んだり、意図しないファイルに上書きしたりします。 この問題を扱うため、⼤昔にプログラマは、バージョン管理下で全ての変更をファイル に保持するシンプルなデータベースを持つ、ローカルなバージョン管理システムを開発し 1 第1章 使い始める Scott Chacon Pro Git ました(図1-1参照)。 図 1.1: ローカル・バージョン管理図解 もっとも有名なVCSツールの⼀つが、RCSと呼ばれるシステムでした。今⽇でも依然と して多くのコンピューターに⼊っています。⼈気のMac OS Xオペレーティング・システ ムさえも、開発者ツールをインストールしたときは、rcsコマンドを含みます。このツー ルは基本的に、ディスク上に特殊フォーマットで、⼀つのリビジョンからもう⼀つのリビ ジョンへのパッチ(これはファイル間の差分です)の集合を保持することで稼動します。 そういうわけで、全てのパッチを積み上げることで、いつかは、あらゆる時点の、あらゆ るファイルのように⾒えるものを再⽣成する事ができます。 1.1.2 集中バージョン管理システム 次に⼈々が遭遇した⼤きな問題は、他のシステムの開発者と共同制作をする必要がある ことです。この問題に対処するために、集中バージョン管理システム(CVCSs)が開発 されました。CVSやSubversion、Perforceのような、これらのシステムは、全てのバー ジョン管理されたファイルと、その中央の場所からファイルをチェック・アウトする多数 のクライアントを含む単⼀のサーバーを持ちます。⻑年の間、これはバージョン管理の標 準となって来ました(図1-2参照)。 図 1.2: 集中バージョン管理図解 2 Scott Chacon Pro Git 1.2節 Git略史 この構成は、特にローカルVCSと⽐較して、多くの利点を提供します。例えば、全て の⼈は、プロジェクトのその他の全ての⼈々が何をしているのか、⼀定の程度は知ってい ます。管理者は、誰が何をできるのかについて、きめ細かい統制⼿段を持ちます。このた め、⼀つのCVCSを管理するということは、全てのクライアントのローカル・データベー スを取り扱うより、はるかに容易です。 しかしながら、この構成はまた、深刻な不利益も持ちます。もっとも明⽩なのは、中央 サーバーで発⽣する単⼀障害点です。もし、そのサーバーが1時間の間停⽌すると、その 1時間の間は誰も全く、共同作業や、彼らが作業を進めている全てに対してバージョン変 更の保存をすることができなくなります。もし中央データベースがのっているハードディ スクが破損し、適切なバックアップが保持されていないとすると、⼈々が偶然にローカ ル・マシンに持っていた幾らかの単⼀スナップショット(訳者注:ある時点のファイル、 ディレクトリなどの編集対象の状態)を除いた、プロジェクト全体の履歴を失うことにな ります。ローカルVCSシステムも、これと同じ問題に悩まされます。つまり、単⼀の場所 にプロジェクトの全体の履歴を持っているときはいつでも、全てを失う事を覚悟すること になります。 1.1.3 分散バージョン管理システム ここから分散バージョン管理システム(DVCSs)に⼊ります。DVCS(Git、Mercurial、 Bazaar、Darcsのようなもの)では、クライアントはファイルの最新スナップショットを チェックアウト(訳者注:バージョン管理システムから、作業ディレクトリにファイルや ディレクトリをコピーすること)するだけではありません。リポジトリ(訳者注:バー ジョン管理の対象になるファイル、ディレクトリ、更新履歴などの⼀群)全体をミラー リングします。故にどのサーバーが故障したとして、故障したサーバーを介してそれら のDVCSが共同作業をしていたとしても、あらゆるクライアント・リポジトリは修復のた めにサーバーにコピーして戻す事ができます。そのサーバーを介してコラボレーションし ていたシステムは, どれか⼀つのクライアントのリポジトリからサーバー復旧の為バック アップをコピーすることができます. 全てのチェックアウトは、実は全データの完全バッ クアップなのです(図1-3を参照)。 そのうえ、これらのDVCSの多くは、 連携する複数のリモート・リポジトリを扱いな がら⼤変よく機能するため、同⼀のプロジェクト内において、同時に異なった⽅法で、異 なる⼈々のグループと共同作業が可能です。このことは、集中システムでは不可能であっ た階層モデルのような、幾つかの様式のワークフローを始めることを許します。 1.2 Git略史 ⼈⽣における多くの素晴らしい出来事のように、Gitはわずかな創造的破壊と熱烈な論 争から始まりました。Linuxカーネルは、⾮常に巨⼤な範囲のオープンソース・ソフト ウェア・プロジェクトの⼀つです。Linuxカーネル保守の⼤部分の期間(1991-2002)の 間は、このソフトウェアに対する変更は、パッチとアーカイブしたファイルとして次々に まわされていました。2002年に、Linuxカーネル・プロジェクトはプロプライエタリの DVCSであるBitKeeperを使い始めました。 2005年に、Linuxカーネルを開発していたコミュニティと、BitKeeperを開発していた 営利企業との間の協⼒関係が崩壊して、課⾦無しの状態が取り消されました。これは、 Linux開発コミュニティ(と、特にLinuxの作者のLinus Torvalds)に、BitKeeperを利⽤ 3 第1章 使い始める Scott Chacon Pro Git 図 1.3: 分散バージョン管理システムの図解 している間に学んだ幾つかの教訓を元に、彼ら独⾃のツールの開発を促しました。新しい システムの⽬標の幾つかは、次の通りでした: • スピード • シンプルな設計 • ノンリニア開発(数千の並列ブランチ)への強⼒なサポート • 完全な分散 • Linux カーネルのような⼤規模プロジェクトを(スピードとデータサイズで)効率的 に取り扱い可能 2005年のその誕⽣から、Gitは使いやすく発展・成熟してきており、さらにその初期の 品質を維持しています。とても⾼速で、巨⼤プロジェクトではとても効率的で、ノンリ ニア開発のためのすごい分岐システム(branching system)を備えています(第3章参 照)。 1.3 Gitの基本 では、要するにGitとは何なのでしょうか。これは、Gitを吸収するには重要な節です。 なぜならば、もしGitが何かを理解し、Gitがどうやって稼動しているかの根本を理解で きれば、Gitを効果的に使う事が恐らくとても容易になるからです。 Gitを学ぶときは、 SubversionやPerforceのような他のVCSsに関してあなたが恐らく知っていることは、意 識しないでください。このツールを使うときに、ちょっとした混乱を回避することに役⽴ ちます。Gitは、ユーザー・インターフェイスがとてもよく似ているのにも関わらず、そ れら他のシステムとは⼤きく異なって、情報を格納して取り扱います(訳者注:「取り扱 う」の部分はthinksなので、「⾒なします」と訳す⽅が原語に近い)。これらの相違を理 解する事は、Gitを扱っている間の混乱を、防いでくれるでしょう。 4 Scott Chacon Pro Git 1.3節 Gitの基本 1.3.1 スナップショットで、差分ではない Gitと他のVCS (Subversionとその類を含む)の主要な相違は、Gitのデータについての 考え⽅です。概念的には、他のシステムのほとんどは、情報をファイルを基本とした変更 のリストとして格納します。これらのシステム(CVS、Subversion、Perforce、Bazaar 等々)は、図1-4に描かれているように、システムが保持しているファイルの集合と、時 間を通じてそれぞれのファイルに加えられた変更の情報を考えます。 図 1.4: 他のシステムは、データをそれぞれのファイルの基本バージョンへの変更として 格納する傾向があります。 Gitは、この⽅法ではデータを考えたり、格納しません。代わりに、Gitはデータをミ ニ・ファイルシステムのスナップショットの集合のように考えます。Gitで全てのコミッ ト(訳注:commitとは変更を記録・保存するGitの操作。詳細は後の章を参照)をすると き、もしくはプロジェクトの状態を保存するとき、Gitは基本的に、その時の全てのファ イルの状態のスナップショットを撮り(訳者注:意訳)、そのスナップショットへの参照 を格納するのです。効率化のため、ファイルに変更が無い場合は、Gitはファイルを再格 納せず、既に格納してある、以前の同⼀のファイルへのリンクを格納します。Gitは、む しろデータを図1-5のように考えます。 図 1.5: Gitは時間を通じたプロジェクトのスナップショットとしてデータを格納します。 これが、Gitと類似の全ての他のVCSsとの間の重要な違いです。ほとんどの他のシステ ムが以前の世代から真似してきた、ほとんど全てのバージョン管理のやり⽅(訳者注: aspectを意訳)を、Gitに⾒直させます。これは、Gitを、単純にVCSと⾔うより、その上 に組み込まれた幾つかの途⽅も無くパワフルなツールを備えたミニ・ファイルシステム にしています。このやり⽅でデータを考えることで得られる利益の幾つかを、第3章のGit branchingを扱ったときに探求します。 1.3.2 ほとんど全ての操作がローカル Gitのほとんどの操作は、ローカル・ファイルと操作する資源だけ必要とします。⼤体 はネットワークの他のコンピューターからの情報は必要ではありません。ほとんどの操作 5 第1章 使い始める Scott Chacon Pro Git がネットワーク遅延損失を伴うCVCSに慣れているのであれば、もっさりとしたCVCSに 慣れているのであれば、このGitの速度は神業のように感じるでしょう(訳者注:直訳は 「このGitの側⾯はスピードの神様がこの世のものとは思えない⼒でGitを祝福したと考え させるでしょう」)。プロジェクトの履歴は丸ごとすぐそこのローカル・ディスクに保持 しているので、⼤概の操作はほぼ瞬時のように⾒えます。 例えば、プロジェクトの履歴を閲覧するために、Gitはサーバーに履歴を取得しに⾏っ て表⽰する必要がありません。直接にローカル・データベースからそれを読むだけです。 これは、プロジェクトの履歴をほとんど即座に知るということです。もし、あるファイル の現在のバージョンと、そのファイルの1ヶ⽉前の間に導⼊された変更点を知りたいので あれば、Gitは、遠隔のサーバーに差分を計算するように問い合わせたり、ローカルで差 分を計算するために遠隔サーバーからファイルの古いバージョンを持ってくる代わりに、 1か⽉前のファイルを調べてローカルで差分の計算を⾏なえます。 これはまた、オフラインであるか、VPNから切り離されていたとしても、出来ない事 は⾮常に少ないことを意味します。もし、⾶⾏機もしくは列⾞に乗ってちょっとした仕 事をしたいとしても、アップロードするためにネットワーク接続し始めるまで、楽しく コミットできます。もし、帰宅してVPNクライアントを適切に作動させられないとして も、さらに作業ができます。多くの他のシステムでは、それらを⾏なう事は、不可能で あるか苦痛です。例えばPerforceにおいては、サーバーに接続できないときは、多くの事 が⾏なえません。SubversionとCVSにおいては、ファイルの編集はできますが、データ ベースに変更をコミットできません(なぜならば、データベースがオフラインだからで す)。このことは巨⼤な問題に思えないでしょうが、実に⼤きな違いを⽣じうることに驚 くでしょう。 1.3.3 Gitは完全性を持つ Gitの全てのものは、格納される前にチェックサムが取られ、その後、そのチェックサ ムで照合されます。これは、Gitがそれに関して感知することなしに、あらゆるファイル の内容を変更することが不可能であることを意味します。この機能は、Gitの最下層に組 み込まれ、またGitの哲学に不可⽋です。Gitがそれを感知できない状態で、転送中に情報 を失う、もしくは壊れたファイルを取得することはありません。 Gitがチェックサム⽣成に⽤いる機構は、SHA-1ハッシュと呼ばれます。これは、16進 数の⽂字(0-9とa-f)で構成された40⽂字の⽂字列で、ファイルの内容もしくはGit内の ディレクトリ構造を元に計算されます。SHA-1ハッシュは、このようなもののように⾒え ます: 24b9da6552252987aa493b52f8696cd6d3b00373 Gitはハッシュ値を⼤変よく利⽤するので、Gitのいたるところで、これらのハッシュ 値を⾒ることでしょう。事実、Gitはファイル名ではなく、ファイル内容のハッシュ値に よってアドレスが呼び出されるGitデータベースの中に全てを格納しています。 1.3.4 Gitは通常はデータを追加するだけ Gitで⾏動するとき、ほとんど全てはGitデータベースにデータを追加するだけです。シ ステムにいかなる⽅法でも、UNDO不可能なこと、もしくはデータを消させることをさ 6 Scott Chacon Pro Git 1.3節 Gitの基本 せるのは、⼤変難しいです。あらゆるVCSと同様に、まだコミットしていない変更は失っ たり、台無しにできたりします。しかし、スナップショットをGitにコミットした後は、 特にもし定期的にデータベースを他のリポジトリにプッシュ(訳注:pushはGitで管理す るあるリポジトリのデータを、他のリポジトリに転送する操作。詳細は後の章を参照)し ていれば、変更を失うことは⼤変難しくなります。 激しく物事をもみくちゃにする危険なしに試⾏錯誤を⾏なえるため、これはGitの利⽤ を喜びに変えます。Gitがデータをどのように格納しているのかと失われたように思える データをどうやって回復できるのかについての、より詳細な解説に関しては、第9章を参 照してください。 1.3.5 三つの状態 今、注意してください。もし学習プロセスの残りをスムーズに進めたいのであれば、 これはGitに関して覚えておく主要な事です。Gitは、ファイルが帰属する、コミット済、 修正済、ステージ済の、三つの主要な状態を持ちます。コミット済は、ローカル・データ ベースにデータが安全に格納されていることを意味します。修正済は、ファイルに変更を 加えていますが、データベースにそれがまだコミットされていないことを意味します。ス テージ済は、次のスナップショットのコミットに加えるために、現在のバージョンの修正 されたファイルに印をつけている状態を意味します。 このことは、Gitプロジェクト(訳者注:ディレクトリ内)の、Gitディレクトリ、作業 ディレクトリ、ステージング・エリアの三つの主要な部分(訳者注:の理解)に導きま す。 図 1.6: 作業ディレクトリ、ステージング・エリア、Gitディレクトリ Gitディレクトリは、プロジェクトのためのメタデータ(訳者注:Gitが管理するファイ ルやディレクトリなどのオブジェクトの要約)とオブジェクトのデータベースがあるとこ ろです。これは、Gitの最も重要な部分で、他のコンピューターからリポジトリをクロー ン(訳者注:コピー元の情報を記録した状態で、Gitリポジトリをコピーすること)した ときに、コピーされるものです。 作業ディレクトリは、プロジェクトの⼀つのバージョンの単⼀チェックアウトです。こ れらのファイルはGitディレクトリの圧縮されたデータベースから引き出されて、利⽤す 7 第1章 使い始める Scott Chacon Pro Git るか修正するためにディスクに配置されます。 ステージング・エリアは、普通はGitディレクトリに含まれる、次のコミットに何が含 まれるかに関しての情報を蓄えた⼀つの単純なファイルです。ときどきインデックスのよ うに引き合いにだされますが、ステージング・エリアとして呼ばれることが基本になりつ つあります。 基本的なGitのワークフローは、このような⾵に進みます: 1. 作業ディレクトリのファイルを修正します。 2. 修正されたファイルのスナップショットをステージング・エリアに追加して、ファ イルをステージします。 3. コミットします。(訳者注:Gitでは)これは、ステージング・エリアにあるファイ ルを取得し、永久不変に保持するスナップショットとしてGitディレクトリに格納す ることです。 もしファイルの特定のバージョンがGitディレクトリの中にあるとしたら、コミット済 だと⾒なされます。もし修正されていて、ステージング・エリアに加えられていれば、ス テージ済です。そして、チェックアウトされてから変更されましたが、ステージされてい ないとするなら、修正済です。第2章では、これらの状態と、どうやってこれらを利⽤を するか、もしくは完全にステージ化部分を省略するかに関してより詳しく学習します。 1.4 Gitのインストール 少しGitを使う事に⼊りましょう。何よりも最初に、Gitをインストールしなければなり ません。幾つもの経路で⼊⼿することができ、主要な⼆つの⽅法のうちの⼀つはソースか らインストールすることで、もう⼀つはプラットフォームに応じて存在するパッケージを インストールすることです。 1.4.1 ソースからのインストール もし可能であれば、もっとも最新のバージョンを⼊⼿できるので、⼀般的にソースか らGitをインストールするのが便利です。Gitのそれぞれのバージョンは、実⽤的なユー ザー・インターフェイスの向上が含まれており、もしソースからソフトウェアをコンパイ ルすることに違和感を感じないのであれば、最新バージョンを⼊⼿することは、⼤抵は 最も良い経路になります。また、多くのLinuxディストリビューションがとても古いパッ ケージを収録している事は良くあることであり、最新のディストリビューションを使って いるか、バックポート(訳者注:最新のパッケージを古いディストリビューションで使え るようにする事)をしていない限りは、ソースからのインストールがベストな選択になる でしょう。 Gitをインストールするためには、Gitが依存するライブラリーである、curl、zlib、 openssl、expat、libiconvを⼊⼿する必要があります。例えば、もし(Fedoraなどで) yumか(Debianベースのシステムなどで)apt-getが⼊ったシステムを使っているのであ れば、これらのコマンドの⼀つを依存対象の全てをインストールするのに使う事ができま す: 8 Scott Chacon Pro Git 1.4節 Gitのインストール $ yum install curl-devel expat-devel gettext-devel \ openssl-devel zlib-devel $ apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \ libz-dev libssl-dev 全ての必要な依存対象を持っているのであれば、先に進んでGitのウェブサイトから最 新版のスナップショットを持ってくる事ができます: http://git-scm.com/download そして、コンパイルしてインストールします: $ tar -zxf git-1.7.2.2.tar.gz $ cd git-1.7.2.2 $ make prefix=/usr/local all $ sudo make prefix=/usr/local install また、Gitのインストール後、アップデートでGitを通して最新版のGitを得ることがで きます。 $ git clone git://git.kernel.org/pub/scm/git/git.git 1.4.2 Linuxにインストール バイナリのインストーラーを通じてLinux上にGitをインストールしたいのであれば、 ⼤抵はディストリビューションに付属する基本的なパッケージ・マネジメント・ツールを 使って、それを⾏なう事ができます。もしFedoraを使っているのであれば、yumを使う 事が出来ます: $ yum install git-core もしくは、もしUbuntuのようなDebianベースのディストリュビューションを使ってい るのであれば、apt-getをやってみましょう: $ apt-get install git 1.4.3 Macにインストール MacにGitをインストールするには2つの簡単な⽅法があります。もっとも簡単な ⽅法は、グラフィカルなGitインストーラーを使うことで、このGitインストーラーは 9 第1章 使い始める Scott Chacon Pro Git SourceForgeのページ(図1-7参照)からダウンロードすることができます: http://sourceforge.net/projects/git-osx-installer/ 図 1.7: Git OS X installer もう⼀つの主要な⽅法は、MacPorts (http://www.macports.org) からGitをインストールす ることです。MacPortsをインストールした状態であれば、Gitを以下のようにインストー ルできます。 $ sudo port install git-core +svn +doc +bash_completion +gitweb 全てのvariantsを追加する必要はありませんが、SubversionのリポジトリでGitを使う 必要がまだあるなら、恐らく+svnを含めないといけないでしょう(第8章参照)。 1.4.4 Windowsにインストール WindowsにGitをインストールするのはとても簡単です。msysGitプロジェクトは、よ り簡単なインストール⼿続きの⼀つを備えています。GitHubのページから、単純にイン ストーラーのexeファイルをダウンロードをし、実⾏してください: http://msysgit.github.com/ インストール後、コマンドライン版(後で役に⽴つSSHクライアントを含む)とスタン ダードGUI版の両⽅を使う事ができます。 Windows利⽤時の注意点: この本で紹介されている複雑なコマンドを使えるので、Gitは msysGit shell(Unixスタイル)で使うようにしましょう。Windowsのシェル/コマンドライ ンコンソールを使わざるを得ない場合、空⽩を含むパラメーターを囲むための記号はダブ ルクオーテーション(シングルクォーテーションは使えない)を使⽤する必要があります。 同様に、サーカムフレックス記号(ˆ)が⾏末に来る場合はダブルクオーテーションで囲まな ければなりません。同記号はWindowsにおいて「次⾏に続く」を意味する記号だからです。 1.5 最初のGitの構成 今や、Gitがシステムにあります。Git環境をカスタマイズするためにしたい事が少しは あることでしょう。アップグレードの度についてまわるので、たった⼀度でそれらを終わ 10 Scott Chacon Pro Git 1.5節 最初のGitの構成 らすべきでしょう。またそれらは、またコマンドを実⾏することによっていつでも変更す ることができます。 Gitには、git configと呼ばれるツールが付属します。これで、どのようにGitが⾒えて機 能するかの全ての⾯を制御できる設定変数を取得し、設定することができます。これらの 変数は三つの異なる場所に格納されうります: • /etc/gitconfig file: システム上の全てのユーザーと全てのリポジトリに対する設定値 を保持します。もし--systemオプションをgit configに指定すると、明確にこのファ イルに読み書きを⾏ないます。 • ~/.gitconfig file: 特定のユーザーに対する設定値を保持します. --globalオプションを 指定することで、Gitに、明確にこのファイルに読み書きを⾏なわせることができま す。 • 現在使っている、あらゆるリポジトリのGitディレクトリの設定ファイル(.git/ configのことです): 特定の単⼀リポジトリに対する設定値を保持します。それぞれの レベルの値は以前のレベルの値を上書きするため、.git/configの中の設定値は/etc/ gitconfigの設定値に優先されます。 Windows環境下では、Gitは$HOMEディレクトリ(環境変数USERPROFILEで指定)の中の.gitconfigファ イルを検索に⾏きます。$HOMEディレクトリはほとんどの場合 C:\Documents and Settings\ $USER か C:\Users\$USER のいずれかです($USERは環境変数USERNAMEで指定)。また、インス トーラー時にWindowsシステムにGitをインストールすると決めたところにある、MSys のルートとの相対位置であったとしても、 /etc/gitconfigも⾒に⾏きます。 1.5.1 個人の識別情報 Gitをインストールしたときに最初にすべきことは、ユーザー名とE-mailアドレスを設 定することです。全てのGitのコミットはこの情報を⽤いるため、これは重要で、次々と まわすコミットに永続的に焼き付けられます: $ git config --global user.name "John Doe" $ git config --global user.email [email protected] また、もし--globalオプションを指定するのであれば、Gitはその後、そのシステム上で ⾏なう(訳者注:あるユーザーの)全ての操作に対して常にこの情報を使うようになるた め、この操作を⾏なう必要はたった⼀度だけです。もし、違う名前とE-mailアドレスを特 定のプロジェクトで上書きしたいのであれば、そのプロジェクトの(訳者注:Gitディレ クトリの)中で、--globalオプション無しでこのコマンドを実⾏することができます。 1.5.2 エディター 今や、個⼈の識別情報が設定され、Gitがメッセージのタイプをさせる必要があるとき に使う、標準のテキストエディターを設定できます。標準では、Gitはシステムのデフォ ルト・エディターを使います。これは⼤抵の場合、ViかVimです。Emacsのような違うテ キスト・エディターを使いたい場合は、次のようにします: 11 第1章 使い始める Scott Chacon Pro Git $ git config --global core.editor emacs 1.5.3 diffツール 設定したいと思われる、その他の便利なオプションは、マージ(訳者注:複数のリポジ トリを併合すること)時の衝突を解決するために使う、標準のdiffツールです。vimdiffを 使いたいとします: $ git config --global merge.tool vimdiff Git は kdiff3、 tkdiff、 meld、 xxdiff、 emerge、 vimdiff、 gvimdiff、 ecmerge、 opendiffを確かなマージ・ツールとして扱えます。カスタム・ツールもまた設定できます が、これをする事に関しての詳細な情報は第7章を参照してください。 1.5.4 設定の確認 設定を確認したい場合は、その時点でGitが⾒つけられる全ての設定を⼀覧するコマン ドであるgit config --listを使う事ができます: $ git config --list user.name=Scott Chacon [email protected] color.status=auto color.branch=auto color.interactive=auto color.diff=auto ... Gitは異なったファイル(例えば/etc/gitconfigと~/.gitconfig)から同⼀のキーを読み込む ため、同⼀のキーを1度以上⾒ることになるでしょう。この場合、Gitは⾒つけたそれぞれ 同⼀のキーに対して最後の値を⽤います。 また、Gitに設定されている特定のキーの値を、git config {key}をタイプすることで確 認することができます: $ git config user.name Scott Chacon 12 Scott Chacon Pro Git 1.6節 ヘルプを⾒る 1.6 ヘルプを見る もし、Gitを使っている間は助けがいつも必要なら、あらゆるGitコマンドのヘルプのマ ニュアル・ページ(manpage)を参照する3種類の⽅法があります。 $ git help <verb> $ git <verb> --help $ man git-<verb> 例えば、configコマンドのヘルプのmanpageを次のコマンドを⾛らせることで⾒るこ とができます。 $ git help config これらのコマンドは、オフラインのときでさえ、どこでも⾒る事ができるので、すばら しいです。 もしmanpageとこの本が⼗分でなく、⼈の助けが必要であれば、フリーノー ドIRCサーバー(irc.freenode.net)の#gitもしくは#githubチャンネルにアクセスしてみて ください。これらのチャンネルはいつも、全員がGitに関してとても知識があり、よく助 けてくれようとする数百⼈の⼈々でいっぱいです。 1.7 まとめ Gitとは何か、どのように今まで使われてきた他のCVCSと異なるのかについて、基本 的な理解ができたはずです。また、今や個⼈情報の設定ができた、システムに稼動する バージョンのGitがあるはずです。今や、本格的にGitの基本を学習するときです。 13 第2章 Git の基本 Git を使い始めるにあたってどれかひとつの章だけしか読めないとしたら、読むべきは 本章です。この章では、あなたが実際に Git を使う際に必要となる基本コマンドをすべ て取り上げています。本章を最後まで読めば、リポジトリの設定や初期化、ファイルの追 跡、そして変更内容のステージやコミットなどができるようになるでしょう。また、Git で特定のファイル (あるいは特定のファイルパターン) を無視させる⽅法やミスを簡単に 取り消す⽅法、プロジェクトの歴史や各コミットの変更内容を⾒る⽅法、リモートリポジ トリとの間でのプッシュやプルを⾏う⽅法についても説明します。 2.1 Git リポジトリの取得 Git プロジェクトを取得するには、⼤きく⼆通りの⽅法があります。ひとつは既存のプ ロジェクトやディレクトリを Git にインポートする⽅法、そしてもうひとつは既存の Git リポジトリを別のサーバーからクローンする⽅法です。 2.1.1 既存のディレクトリでのリポジトリの初期化 既存のプロジェクトを Git で管理し始めるときは、そのプロジェクトのディレクトリに 移動して次のように打ち込みます。 $ git init これを実⾏すると .git という名前の新しいサブディレクトリが作られ、リポジトリに 必要なすべてのファイル (Git リポジトリのスケルトン) がその中に格納されます。この時 点では、まだプロジェクト内のファイルは⼀切管理対象になっていません (今作った .git ディレクトリに実際のところどんなファイルが含まれているのかについての詳細な情報 は、第9章 を参照ください)。 空のディレクトリではなくすでに存在するファイルのバージョン管理を始めたい場合 は、まずそのファイルを監視対象に追加してから最初のコミットをすることになります。 この場合は、追加したいファイルについて git add コマンドを実⾏したあとでコミットを ⾏います。 15 第2章 Git の基本 Scott Chacon Pro Git $ git add *.c $ git add README $ git commit -m 'initial project version' これが実際のところどういう意味なのかについては後で説明します。ひとまずこの時点 で、監視対象のファイルを持つ Git リポジトリができあがり最初のコミットまで済んだこ とになります。 2.1.2 既存のリポジトリのクローン 既存の Git リポジトリ (何か協⼒したいと思っているプロジェクトなど) のコピーを取 得したい場合に使うコマンドが、git clone です。Subversion などの他の VCS を使って いる⼈なら「checkout じゃなくて clone なのか」と気になることでしょう。これは重要な 違いです。Git は、サーバーが保持しているデータをほぼすべてコピーするのです。その プロジェクトのすべてのファイルのすべての歴史が、git clone で⼿元にやってきます。 実際、もし仮にサーバーのディスクが壊れてしまったとしても、どこかのクライアントに 残っているクローンをサーバーに戻せばクローンした時点まで復元することができます (サーバーサイドのフックなど⼀部の情報は失われてしまいますが、これまでのバージョ ン管理履歴はすべてそこに残っています。第4章 で詳しく説明します)。 リポジトリをクローンするには git clone [url] とします。たとえば、Ruby の Git ライ ブラリである Grit をクローンする場合は次のようになります。 $ git clone git://github.com/schacon/grit.git これは、まず grit というディレクトリを作成してその中で .git ディレクトリを初期 化し、リポジトリのすべてのデータを引き出し、そして最新バージョンの作業コピーを チェックアウトします。新しくできた grit ディレクトリに⼊ると、プロジェクトのファ イルをごらんいただけます。もし grit ではない別の名前のディレクトリにクローンした いのなら、コマンドラインオプションでディレクトリ名を指定します。 $ git clone git://github.com/schacon/grit.git mygrit このコマンドは先ほどと同じ処理をしますが、ディレクトリ名は mygrit となります。 Git では、さまざまな転送プロトコルを使⽤することができます。先ほどの例では git:// プロトコルを使⽤しましたが、http(s):// や user@server:/path.git といった形式を使 うこともできます。これらは SSH プロトコルを使⽤します。第4章 で、サーバー側で準 備できるすべてのアクセス⽅式についての利点と⽋点を説明します。 16 Scott Chacon Pro Git 2.2節 変更内容のリポジトリへの記録 2.2 変更内容のリポジトリへの記録 これで、れっきとした Git リポジトリを準備して、そのプロジェクト内のファイルの作 業コピーを取得することができました。次は、そのコピーに対して何らかの変更を⾏い、 適当な時点で変更内容のスナップショットをリポジトリにコミットすることになります。 作業コピー内の各ファイルには 追跡されている(tracked) ものと 追跡されてない (untracked) ものの⼆通りがあることを知っておきましょう。追跡されている ファイルと は、直近のスナップショットに存在したファイルのことです。これらのファイルについて は変更されていない(unmodified)」「変更されている(modified)」「ステージされている (staged)」の三つの状態があります。追跡されていないファイルは、そのどれでもありま せん。直近のスナップショットには存在せず、ステージングエリアにも存在しないファイ ルのことです。最初にプロジェクトをクローンした時点では、すべてのファイルは「追跡 されている」かつ「変更されていない」状態となります。チェックアウトしただけで何も 編集していない状態だからです。 ファイルを編集すると、Git はそれを「変更された」とみなします。直近のコミットの 後で変更が加えられたからです。変更されたファイルを ステージ し、それをコミットす る。この繰り返しです。ここまでの流れを図 2-1 にまとめました。 図 2.1: ファイルの状態の流れ 2.2.1 ファイルの状態の確認 どのファイルがどの状態にあるのかを知るために主に使うツールが git status コマンド です。このコマンドをクローン直後に実⾏すると、このような結果となるでしょう。 $ git status On branch master nothing to commit, working directory clean これは、クリーンな作業コピーである (つまり、追跡されているファイルの中に変更さ れているものがない) ことを意味します。また、追跡されていないファイルも存在しませ ん (もし追跡されていないファイルがあれば、Git はそれを表⽰します)。最後に、このコ マンドを実⾏するとあなたが今どのブランチにいるのかを知ることができます。現時点で 17 第2章 Git の基本 Scott Chacon Pro Git は常に master となります。これはデフォルトであり、ここでは特に気にする必要はありま せん。ブランチについては次の章で詳しく説明します。 ではここで、新しいファイルをプロジェクトに追加してみましょう。シンプルに、README ファイルを追加してみます。それ以前に README ファイルがなかった場合、git status を実⾏すると次のように表⽰されます。 $ vim README $ git status On branch master Untracked files: (use "git add <file>..." to include in what will be committed) README nothing added to commit but untracked files present (use "git add" to track) 出⼒結果の 『Untracked files』 欄に README ファイルがあることから、このファイルが 追跡されていないということがわかります。これは、Git が「前回のスナップショット (コミット) にはこのファイルが存在しなかった」とみなしたということです。明⽰的に指 ⽰しない限り、Git はコミット時にこのファイルを含めることはありません。⾃動⽣成さ れたバイナリファイルなど、コミットしたくないファイルを間違えてコミットしてしまう ⼼配はないということです。今回は README をコミットに含めたいわけですから、まず ファイルを追跡対象に含めるようにしましょう。 2.2.2 新しいファイルの追跡 新しいファイルの追跡を開始するには git add コマンドを使⽤します。README ファイル の追跡を開始する場合はこのようになります。 $ git add README 再び status コマンドを実⾏すると、README ファイルが追跡対象となり、ステージされ ていることがわかるでしょう。 $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: 18 README Scott Chacon Pro Git 2.2節 変更内容のリポジトリへの記録 ステージされていると判断できるのは、『Changes to be committed』 欄に表⽰され ているからです。ここでコミットを⾏うと、git add した時点の状態のファイルがスナッ プショットとして歴史に書き込まれます。先ほど git init をしたときに、ディレクトリ内 のファイルを追跡するためにその後 git add (ファイル) としたことを思い出すことでしょ う。git add コマンドには、ファイルあるいはディレクトリのパスを指定します。ディレ クトリを指定した場合は、そのディレクトリ以下にあるすべてのファイルを再帰的に追加 します。 2.2.3 変更したファイルのステージング すでに追跡対象となっているファイルを変更してみましょう。たとえば、すでに追跡対 象となっているファイル benchmarks.rb を変更して status コマンドを実⾏すると、結果は このようになります。 $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: README Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: benchmarks.rb benchmarks.rb ファイルは 『Changes not staged for commit』 という欄に表⽰されま す。これは、追跡対象のファイルが作業ディレクトリ内で変更されたけれどもまだステー ジされていないという意味です。ステージするには git add コマンドを実⾏します (この コマンドにはいろんな意味合いがあり、新しいファイルの追跡開始・ファイルのステージ ング・マージ時に衝突が発⽣したファイルに対する「解決済み」マーク付けなどで使⽤し ます)。では、git add で benchmarks.rb をステージしてもういちど git status を実⾏してみ ましょう。 $ git add benchmarks.rb $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: README modified: benchmarks.rb 19 第2章 Git の基本 Scott Chacon Pro Git 両⽅のファイルがステージされました。これで、次回のコミットに両⽅のファイルが含 まれるようになります。ここで、さらに benchmarks.rb にちょっとした変更を加えてからコ ミットしたくなったとしましょう。ファイルを開いて変更を終え、コミットの準備が整い ました。しかし、git status を実⾏してみると何か変です。 $ vim benchmarks.rb $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: README modified: benchmarks.rb Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: benchmarks.rb これはどういうことでしょう? benchmarks.rb が、ステージされているほうにもステージ されていないほうにも登場しています。こんなことってありえるんでしょうか? 要する に、Git は「git add コマンドを実⾏した時点の状態のファイル」をステージするというこ とです。ここでコミットをすると、実際にコミットされるのは git add を実⾏した時点の benchmarks.rb であり、git commit した時点の作業ディレクトリにある内容とは違うものに なります。git add した後にファイルを変更した場合に、最新版のファイルをステージし なおすにはもう⼀度 git add を実⾏します。 $ git add benchmarks.rb $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: README modified: benchmarks.rb 2.2.4 ファイルの無視 ある種のファイルについては、Git で⾃動的に追加してほしくないしそもそも「追跡さ れていない」と表⽰されるのも気になる。そんなことがよくあります。たとえば、ログ ファイルやビルドシステムが⽣成するファイルなどの⾃動⽣成されるファイルがそれにあ 20 Scott Chacon Pro Git 2.2節 変更内容のリポジトリへの記録 たるでしょう。そんな場合は、無視させたいファイルのパターンを並べた .gitignore とい うファイルを作成します。.gitignore ファイルは、たとえばこのようになります。 $ cat .gitignore *.[oa] *~ 最初の⾏は .o あるいは .a で終わる名前のファイル (コードをビルドする際にできる であろうオブジェクトファイルとアーカイブファイル) を無視するよう Git に伝えてい ます。次の⾏で Git に無視させているのは、チルダ (~) で終わる名前のファイルです。 Emacs をはじめとする多くのエディタが、この形式の⼀時ファイルを作成します。これ 以外には、たとえば log、tmp、pid といった名前のディレクトリや⾃動⽣成されるドキュ メントなどもここに含めることになるでしょう。実際に作業を始める前に .gitignore ファ イルを準備しておくことをお勧めします。そうすれば、予期せぬファイルを間違って Git リポジトリにコミットしてしまう事故を防げます。 .gitignore ファイルに記述するパターンの規則は、次のようになります。 • 空⾏あるいは # で始まる⾏は無視される • 標準の glob パターンを使⽤可能 • ディレクトリを指定するには、パターンの最後にスラッシュ (/) をつける • パターンを逆転させるには、最初に感嘆符 (!) をつける glob パターンとは、シェルで⽤いる簡易正規表現のようなものです。アスタリスク (*) は、ゼロ個以上の⽂字にマッチします。[abc] は、⾓括弧内の任意の⽂字 (この場合は a、b あるいは c) にマッチします。疑問符 (?) は⼀⽂字にマッチします。また、ハイフン区切 りの⽂字を⾓括弧で囲んだ形式 ([0-9]) は、ふたつの⽂字の間の任意の⽂字 (この場合は 0 から 9 までの間の⽂字) にマッチします。 では、.gitignore ファイルの例をもうひとつ⾒てみましょう。 # コメント。これは無視されます # .a ファイルは無視 *.a # しかし、lib.a ファイルだけは .a であっても追跡対象とします !lib.a # ルートディレクトリの TODO ファイルだけを無視し、サブディレクトリの TODO は無視しません /TODO # build/ ディレクトリのすべてのファイルを無視します build/ # doc/notes.txt は無視しますが、doc/server/arch.txt は無視しません doc/*.txt # doc/ ディレクトリの .txt ファイル全てを無視します doc/**/*.txt **/ 形式は 1.8.2 以降のGitで利⽤可能です。 21 第2章 Git の基本 Scott Chacon Pro Git 2.2.5 ステージされている変更 / されていない変更の閲覧 git status コマンドだけではよくわからない (どのファイルが変更されたのかだけでは なく、実際にどのように変わったのかが知りたい) という場合は git diff コマンドを使⽤ します。git diff コマンドについては後で詳しく解説します。おそらく、最もよく使う場 ⾯としては次の⼆つの問いに答えるときになるでしょう。「変更したけどまだステージし ていない変更は?」「コミット対象としてステージした変更は?」もちろん git status でも これらの質問に対するおおまかな答えは得られますが、git diff の場合は追加したり削除 したりした正確な⾏をパッチ形式で表⽰します。 先ほどの続きで、ふたたび README ファイルを編集してステージし、⼀⽅ benchmarks.rb ファイルは編集だけしてステージしない状態にあると仮定しましょう。ここで status コマ ンドを実⾏すると、次のような結果となります。 $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: README Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: benchmarks.rb 変更したけれどもまだステージしていない内容を⾒るには、引数なしで git diff を実⾏ します。 $ git diff diff --git a/benchmarks.rb b/benchmarks.rb index 3cb747f..da65585 100644 --- a/benchmarks.rb +++ b/benchmarks.rb @@ -36,6 +36,10 @@ def main @commit.parents[0].parents[0].parents[0] end + + + run_code(x, 'commits 1') do git.commits.size end + run_code(x, 'commits 2') do 22 Scott Chacon Pro Git 2.2節 変更内容のリポジトリへの記録 log = git.commits('master', 15) log.size このコマンドは、作業ディレクトリの内容とステージングエリアの内容を⽐較します。 この結果を⾒れば、あなたが変更した内容のうちまだステージされていないものを知るこ とができます。 次のコミットに含めるべくステージされた内容を知りたい場合は、git diff --cached を 使⽤します (Git バージョン 1.6.1 以降では git diff --staged も使えます。こちらのほうが 覚えやすいでしょう)。このコマンドは、ステージされている変更と直近のコミットの内 容を⽐較します。 $ git diff --cached diff --git a/README b/README new file mode 100644 index 0000000..03902a1 --- /dev/null +++ b/README2 @@ -0,0 +1,5 @@ +grit + by Tom Preston-Werner, Chris Wanstrath + http://github.com/mojombo/grit + +Grit is a Ruby library for extracting information from a Git repository git diff ⾃体は、直近のコミット以降のすべての変更を表⽰するわけではないことに注 意しましょう。あくまでもステージされていない変更だけの表⽰となります。これにはす こし⼾惑うかもしれません。変更内容をすべてステージしてしまえば git diff は何も出⼒ しなくなるわけですから。 もうひとつの例を⾒てみましょう。benchmarks.rb ファイルをいったんステージした 後に編集してみましょう。git diff を使⽤すると、ステージされたファイルの変更とまだ ステージされていないファイルの変更を⾒ることができます。 $ git add benchmarks.rb $ echo '# test line' >> benchmarks.rb $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: benchmarks.rb 23 第2章 Git の基本 Scott Chacon Pro Git Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: benchmarks.rb ここで git diff を使うと、まだステージされていない内容を知ることができます。 $ git diff diff --git a/benchmarks.rb b/benchmarks.rb index e445e28..86b2f7c 100644 --- a/benchmarks.rb +++ b/benchmarks.rb @@ -127,3 +127,4 @@ end main() ##pp Grit::GitRuby.cache_client.stats +# test line そして git diff --cached を使うと、これまでにステージした内容を知ることができま す。 $ git diff --cached diff --git a/benchmarks.rb b/benchmarks.rb index 3cb747f..e445e28 100644 --- a/benchmarks.rb +++ b/benchmarks.rb @@ -36,6 +36,10 @@ def main @commit.parents[0].parents[0].parents[0] end + + + run_code(x, 'commits 1') do git.commits.size end + run_code(x, 'commits 2') do log = git.commits('master', 15) log.size 24 Scott Chacon Pro Git 2.2節 変更内容のリポジトリへの記録 2.2.6 変更のコミット ステージングエリアの準備ができたら、変更内容をコミットすることができます。コ ミットの対象となるのはステージされたものだけ、つまり追加したり変更したりしただ けでまだ git add を実⾏していないファイルはコミットされないことを覚えておきましょ う。そういったファイルは、変更されたままの状態でディスク上に残ります。今回の場合 は、最後に git status を実⾏したときにすべてがステージされていることを確認していま す。つまり、変更をコミットする準備ができた状態です。コミットするための最もシンプ ルな⽅法は git commit と打ち込むことです。 $ git commit これを実⾏すると、指定したエディタが⽴ち上がります (シェルの $EDITOR 環境変数で 設定されているエディタ。通常は vim あるいは emacs でしょう。しかし、それ以外にも 第1章 で説明した git config --global core.editor コマンドでお好みのエディタを指定する こともできます)。 エディタには次のようなテキストが表⽰されています (これは Vim の画⾯の例です)。 # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Changes to be committed: # new file: README # modified: benchmarks.rb # ~ ~ ~ ".git/COMMIT_EDITMSG" 10L, 283C デフォルトのコミットメッセージとして、直近の git status コマンドの結果がコメント アウトして表⽰され、先頭に空⾏があることがわかるでしょう。このコメントを消して⾃ 分でコミットメッセージを書き⼊れていくこともできますし、何をコミットしようとして いるのかの確認のためにそのまま残しておいてもかまいません (何を変更したのかをより 明確に知りたい場合は、git commit に -v オプションを指定します。そうすると、diff の内 容がエディタに表⽰されるので何を⾏ったのかが正確にわかるようになります)。エディ タを終了させると、Git はそのメッセージつきのコミットを作成します (コメントおよび diff は削除されます)。 あるいは、コミットメッセージをインラインで記述することもできます。その場合 は、commit コマンドの後で -m フラグに続けて次のように記述します。 $ git commit -m "Story 182: Fix benchmarks for speed" 25 第2章 Git の基本 Scott Chacon Pro Git [master 463dc4f] Story 182: Fix benchmarks for speed 2 files changed, 3 insertions(+) create mode 100644 README これではじめてのコミットができました! 今回のコミットについて、「どのブランチに コミットしたのか (master)」「そのコミットの SHA-1 チェックサム (463dc4f)」「変更さ れたファイルの数」「そのコミットで追加されたり削除されたりした⾏数」といった情報 が表⽰されているのがわかるでしょう。 コミットが記録するのは、ステージングエリアのスナップショットであることを覚えて おきましょう。ステージしていない情報については変更された状態のまま残っています。 別のコミットで歴史にそれを書き加えるには、改めて add する必要があります。コミッ トするたびにプロジェクトのスナップショットが記録され、あとからそれを取り消したり 参照したりできるようになります。 2.2.7 ステージングエリアの省略 コミットの内容を思い通りに作り上げることができるという点でステージングエリアは ⾮常に便利なのですが、普段の作業においては必要以上に複雑に感じられることもある でしょう。ステージングエリアを省略したい場合のために、Git ではシンプルなショート カットを⽤意しています。git commit コマンドに -a オプションを指定すると、追跡対象と なっているファイルを⾃動的にステージしてからコミットを⾏います。つまり git add を 省略できるというわけです。 $ git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: benchmarks.rb no changes added to commit (use "git add" and/or "git commit -a") $ git commit -a -m 'added new benchmarks' [master 83e38c7] added new benchmarks 1 files changed, 5 insertions(+) この場合、コミットする前に benchmarks.rb を git add する必要がないことに注意しま しょう。 2.2.8 ファイルの削除 ファイルを Git から削除するには、追跡対象からはずし (より正確に⾔うとステージン グエリアから削除し)、そしてコミットします。git rm コマンドは、この作業を⾏い、そ 26 Scott Chacon Pro Git 2.2節 変更内容のリポジトリへの記録 して作業ディレクトリからファイルを削除します。つまり、追跡されていないファイルと して残り続けることはありません。 単に作業ディレクトリからファイルを削除しただけの場合は、git status の出⼒の中で は 『Changes not staged for commit』 (つまり ステージされていない) 欄に表⽰されま す。 $ rm grit.gemspec $ git status On branch master Changes not staged for commit: (use "git add/rm <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) deleted: grit.gemspec no changes added to commit (use "git add" and/or "git commit -a") git rm を実⾏すると、ファイルの削除がステージされます。 $ git rm grit.gemspec rm 'grit.gemspec' $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) deleted: grit.gemspec 次にコミットするときにファイルが削除され、追跡対象外となります。変更したファイ ルをすでにステージしている場合は、-f オプションで強制的に削除しなければなりませ ん。まだスナップショットに記録されていないファイルを誤って削除してしまうと Git で 復旧することができなくなってしまうので、それを防ぐための安全装置です。 ほかに「こんなことできたらいいな」と思われるであろう機能として、ファイル⾃体は 作業ツリーに残しつつステージングエリアからの削除だけを⾏うこともできます。つま り、ハードディスク上にはファイルを残しておきたいけれど、もう Git では追跡させたく ないというような場合のことです。これが特に便利なのは、.gitignore ファイルに書き⾜ すのを忘れたために巨⼤なログファイルや⼤量の .a ファイルがステージされてしまった などというときです。そんな場合は --cached オプションを使⽤します。 $ git rm --cached readme.txt 27 第2章 Git の基本 Scott Chacon Pro Git ファイル名やディレクトリ名、そしてファイル glob パターンを git rm コマンドに渡す ことができます。つまり、このようなこともできるということです。 $ git rm log/\*.log * の前にバックスラッシュ (\) があることに注意しましょう。これが必要なのは、シェ ルによるファイル名の展開だけでなく Git が⾃前でファイル名の展開を⾏うからです。た だしWindowsのコマンドプロンプトの場合は、バックスラッシュは取り除かなければなり ません。このコマンドは、log/ ディレクトリにある拡張⼦ .log のファイルをすべて削除し ます。あるいは、このような書き⽅もできます。 $ git rm \*~ このコマンドは、~ で終わるファイル名のファイルをすべて削除します。 2.2.9 ファイルの移動 他の多くの VCS とは異なり、Git はファイルの移動を明⽰的に追跡することはありま せん。Git の中でファイル名を変更しても、「ファイル名を変更した」というメタデータ は Git には保存されないのです。しかし Git は賢いので、ファイル名が変わったことを知 ることができます。ファイルの移動を検出する仕組みについては後ほど説明します。 しかし Git には mv コマンドがあります。ちょっと混乱するかもしれませんね。Git の中 でファイル名を変更したい場合は次のようなコマンドを実⾏します。 $ git mv file_from file_to このようなコマンドを実⾏してから status を確認すると、Git はそれをファイル名が変 更されたと解釈していることがわかるでしょう。 $ git mv README.txt README $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) renamed: README.txt -> README しかし、実際のところこれは、次のようなコマンドを実⾏するのと同じ意味となりま す。 28 Scott Chacon Pro Git 2.3節 コミット履歴の閲覧 $ mv README.txt README $ git rm README.txt $ git add README Git はこれが暗黙的なファイル名の変更であると理解するので、この⽅法であろうが mv コマンドを使おうがどちらでもかまいません。唯⼀の違いは、この⽅法だと 3 つのコマ ンドが必要になるかわりに mv だとひとつのコマンドだけで実⾏できるという点です。よ り重要なのは、ファイル名の変更は何でもお好みのツールで⾏えるということです。あと でコミットする前に add/rm を指⽰してやればいいのです。 2.3 コミット履歴の閲覧 何度かコミットを繰り返すと、あるいはコミット履歴つきの既存のリポジトリをクロー ンすると、過去に何が起こったのかを振り返りたくなることでしょう。そのために使⽤す るもっとも基本的かつパワフルな道具が git log コマンドです。 ここからの例では、simplegit という⾮常にシンプルなプロジェクトを使⽤します。これ は、私が説明⽤によく⽤いているプロジェクトで、次のようにして取得できます。 git clone git://github.com/schacon/simplegit-progit.git このプロジェクトで git log を実⾏すると、このような結果が得られます。 $ git log commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <[email protected]> Date: Mon Mar 17 21:52:11 2008 -0700 changed the version number commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 Author: Scott Chacon <[email protected]> Date: Sat Mar 15 16:40:33 2008 -0700 removed unnecessary test code commit a11bef06a3f659402fe7563abf99ad00de2209e6 Author: Scott Chacon <[email protected]> Date: Sat Mar 15 10:31:28 2008 -0700 first commit 29 第2章 Git の基本 Scott Chacon Pro Git デフォルトで引数を何も指定しなければ、git log はそのリポジトリでのコミットを新 しい順に表⽰します。つまり、直近のコミットが最初に登場するということです。ごらん のとおり、このコマンドは各コミットについて SHA-1 チェックサム・作者の名前とメー ルアドレス・コミット⽇時・コミットメッセージを⼀覧表⽰します。 git log コマンドには数多くのバラエティに富んだオプションがあり、あなたが本当に ⾒たいものを表⽰させることができます。ここでは、よく⽤いられるオプションのいくつ かをご覧に⼊れましょう。 もっとも便利なオプションのひとつが -p で、これは各コミットの diff を表⽰します。 また -2 は、直近の 2 エントリだけを出⼒します。 $ git log -p -2 commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <[email protected]> Date: Mon Mar 17 21:52:11 2008 -0700 changed the version number diff --git a/Rakefile b/Rakefile index a874b73..8f94139 100644 --- a/Rakefile +++ b/Rakefile @@ -5,5 +5,5 @@ require 'rake/gempackagetask' spec = Gem::Specification.new do |s| s.name = "simplegit" - s.version = "0.1.0" + s.version = "0.1.1" s.author = "Scott Chacon" s.email = "[email protected] commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 Author: Scott Chacon <[email protected]> Date: Sat Mar 15 16:40:33 2008 -0700 removed unnecessary test code diff --git a/lib/simplegit.rb b/lib/simplegit.rb index a0a60ae..47c6340 100644 --- a/lib/simplegit.rb +++ b/lib/simplegit.rb @@ -18,8 +18,3 @@ class SimpleGit end end - 30 Scott Chacon Pro Git 2.3節 コミット履歴の閲覧 -if $0 == __FILE__ - git = SimpleGit.new - puts git.show -end \ No newline at end of file このオプションは、先ほどと同じ情報を表⽰するとともに、各エントリの直後にその diff を表⽰します。これはコードレビューのときに⾮常に便利です。また、他のメンバー が⼀連のコミットで何を⾏ったのかをざっと眺めるのにも便利でしょう。 コードレビューの際、⾏単位ではなく単語単位でレビューするほうが容易な場合もある でしょう。git log -p コマンドのオプション --word-diff を使えば、通常の⾏単位diffではな く、単語単位のdiffを表⽰させることができます。単語単位のdiffはソースコードのレビュー に⽤いても役に⽴ちませんが、書籍や論⽂など、⻑⽂テキストファイルのレビューを⾏う際 は便利です。こんな⾵に使⽤します。 $ git log -U1 --word-diff commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <[email protected]> Date: Mon Mar 17 21:52:11 2008 -0700 changed the version number diff --git a/Rakefile b/Rakefile index a874b73..8f94139 100644 --- a/Rakefile +++ b/Rakefile @@ -7,3 +7,3 @@ spec = Gem::Specification.new do |s| s.name = "simplegit" s.version = [-"0.1.0"-]{+"0.1.1"+} s.author = "Scott Chacon" ご覧のとおり、通常のdiffにある「追加⾏や削除⾏の表⽰」はありません。その代わりに、変 更点はインラインで表⽰されることになります。追加された単語は {+ +} で、削除された単 語は [- -] で囲まれます。また、着⽬すべき点が⾏ではなく単語なので、diffの出⼒を通常の 「変更⾏前後3⾏ずつ」から「変更⾏前後1⾏ずつ」に減らしたほうがよいかもしれません。上 記の例で使⽤した -U1 オプションを使えば⾏数を減らせます。 また、git log では「まとめ」系のオプションを使うこともできます。たとえば、各コ ミットに関するちょっとした統計情報を⾒たい場合は --stat オプションを使⽤します。 $ git log --stat commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <[email protected]> 31 第2章 Git の基本 Date: Scott Chacon Pro Git Mon Mar 17 21:52:11 2008 -0700 changed the version number Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 Author: Scott Chacon <[email protected]> Date: Sat Mar 15 16:40:33 2008 -0700 removed unnecessary test code lib/simplegit.rb | 5 ----- 1 file changed, 5 deletions(-) commit a11bef06a3f659402fe7563abf99ad00de2209e6 Author: Scott Chacon <[email protected]> Date: Sat Mar 15 10:31:28 2008 -0700 first commit README | Rakefile | lib/simplegit.rb | 6 ++++++ 23 +++++++++++++++++++++++ 25 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+) ごらんの通り --stat オプションは、各コミットエントリに続けて変更されたファイルの ⼀覧と変更されたファイルの数、追加・削除された⾏数が表⽰されます。また、それらの 情報のまとめを最後に出⼒します。もうひとつの便利なオプションが --pretty です。これ は、ログをデフォルトの書式以外で出⼒します。あらかじめ⽤意されているいくつかのオ プションを指定することができます。oneline オプションは、各コミットを⼀⾏で出⼒し ます。これは、⼤量のコミットを⾒る場合に便利です。さらに short や full そして fuller といったオプションもあり、これは標準とほぼ同じ書式だけれども情報量がそれぞれ少な めあるいは多めになります。 $ git log --pretty=oneline ca82a6dff817ec66f44342007202690a93763949 changed the version number 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test code a11bef06a3f659402fe7563abf99ad00de2209e6 first commit もっとも興味深いオプションは format で、これは独⾃のログ出⼒フォーマットを指定す ることができます。これは、出⼒結果を機械にパースさせる際に⾮常に便利です。⾃分で 32 Scott Chacon Pro Git 2.3節 コミット履歴の閲覧 フォーマットを指定しておけば、将来 Git をアップデートしても結果が変わらないように できるからです。 $ git log --pretty=format:"%h - %an, %ar : %s" ca82a6d - Scott Chacon, 11 months ago : changed the version number 085bb3b - Scott Chacon, 11 months ago : removed unnecessary test code a11bef0 - Scott Chacon, 11 months ago : first commit 表 2-1 は、format で使⽤できる便利なオプションをまとめたものです。 オプション 出⼒される内容 %H コミットのハッシュ %h コミットのハッシュ (短縮版) %T ツリーのハッシュ %t ツリーのハッシュ (短縮版) %P 親のハッシュ %p 親のハッシュ (短縮版) %an Author の名前 %ae Author のメールアドレス %ad Author の⽇付 (–date= オプションに従った形式) %ar Author の相対⽇付 %cn Committer の名前 %ce Committer のメールアドレス %cd Committer の⽇付 %cr Committer の相対⽇付 %s 件名 author と committer は何が違うのか気になる⽅もいるでしょう。author とはその作 業をもともと⾏った⼈、committer とはその作業を適⽤した⼈のことを指します。あな たがとあるプロジェクトにパッチを送り、コアメンバーのだれかがそのパッチを適⽤した としましょう。この場合、両⽅がクレジットされます (あなたが author、コアメンバーが committer です)。この区別については 第5章 でもう少し詳しく説明します。 oneline オプションおよび format オプションは、log のもうひとつのオプションである --graph と組み合わせるとさらに便利です。このオプションは、ちょっといい感じのアス キーグラフでブランチやマージの歴史を表⽰します。Grit プロジェクトのリポジトリなら このようになります。 33 第2章 Git の基本 Scott Chacon Pro Git $ git log --pretty=format:"%h %s" --graph * 2d3acf9 ignore errors from SIGCHLD on trap * 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit |\ | * 420eac9 Added a method for getting the current branch. * | 30e367c timeout code and tests * | 5a09431 add timeout protection to grit * | e1193f8 support for heads with slashes in them |/ * d6016bc require time for xmlschema * 11d191e Merge branch 'defunkt' into local これらは git log の出⼒フォーマット指定のほんの⼀部でしかありません。まだまだオ プションはあります。表 2-2 に、今まで取り上げたオプションとそれ以外によく使われる オプション、そしてそれぞれがlogの出⼒をどのように変えるのかをまとめました。 オプション -p --word-diff 各コミ --stat –stat コマ --shortstat コミット --name-only 変更さ --name-status SHA-1 チェックサムの全体 --abbrev-commit 完全な⽇付フォーマットではなく、相対フォー --relative-date ブランチやマージの歴史 --graph --pretty コミットを別のフォーマットで表⽰する。オプションとして oneline, short, full, fuller そして --oneline 2.3.1 ログ出力の制限 出⼒のフォーマット⽤オプションだけでなく、 git log にはログの制限⽤の便利なオプ ションもあります。コミットの⼀部だけを表⽰するようなオプションのことです。既にひ とつだけ紹介していますね。-2 オプション、これは直近のふたつのコミットだけを表⽰ するものです。実は -<n> の n には任意の整数値を指定することができ、直近の n 件のコ ミットだけを表⽰させることができます。ただ、実際のところはこれを使うことはあまり ないでしょう。というのも、Git はデフォルトですべての出⼒をページャにパイプするの で、ログを⼀度に 1 ページだけ⾒ることになるからです。 しかし --since や --until のような時間制限のオプションは⾮常に便利です。たとえばこ 34 --pretty=one Scott Chacon Pro Git 2.3節 コミット履歴の閲覧 のコマンドは、過去⼆週間のコミットの⼀覧を取得します。 $ git log --since=2.weeks このコマンドはさまざまな書式で動作します。特定の⽇を指定する (『2008-01-15』) こともできますし、相対⽇付を『2 years 1 day 3 minutes ago』のように指定すること も可能です。 コミット⼀覧から検索条件にマッチするものだけを取り出すこともできます。--author オプションは特定の author のみを抜き出し、--grep オプションはコミットメッセージの 中のキーワードを検索します (author と grep を両⽅指定すると、両⽅にマッチするもの だけが対象になります)。 grep を複数指定したい場合は、--all-match を追加しないといけません。そうしない と、どちらか⼀⽅にだけマッチするものも対象になってしまいます。 最後に紹介する git log のフィルタリング⽤オプションは、パスです。ディレクトリ名 あるいはファイル名を指定すると、それを変更したコミットのみが対象となります。この オプションは常に最後に指定し、⼀般にダブルダッシュ (--) の後に記述します。このダ ブルダッシュが他のオプションとパスの区切りとなります。 表 2-3 に、これらのオプションとその他の⼀般的なオプションをまとめました。 オプション 説明 直近の n 件のコミットのみを表⽰する -(n) --since, --after 指定した⽇付/時刻以降のCommitDateのコミットのみに制限する --until, --before 指定した⽇付/時刻以前のCommitDateのコミットのみに制限する --author エントリが指定した⽂字列にマッチするコミットのみを表⽰する --committer エントリが指定した⽂字列にマッチするコミットのみを表⽰する 2.3.2 日時にもとづくログ出力の制限 Git のリポジトリ(git://git.kernel.org/pub/scm/git/git.git)からCommitDateを使って コミットを検索してみましょう。パソコンに設定されたタイムゾーンにおける2014/04/29 のコミットを検索するには、以下のコマンドを実⾏します。 $ git log --after="2014-04-29 00:00:00" --before="2014-04-29 23:59:59" \ --pretty=fuller この場合、コマンドの結果はパソコンのタイムゾーン設定ごとに異なってしまいます。 それを避けるには、タイムゾーンを含むISO 8601フォーマットのような⽇時を --after や --before の引数に指定するといいでしょう。そうすれば、上述のケースのようにコマンド 実⾏結果が異なる可能性がなくなります。 特定⽇時(例として、中央ヨーロッパ時間で2013/04/29 17:07:22)を指定してコミッ トを検索するには、以下のコマンドを使います。 35 第2章 Git の基本 $ git log Scott Chacon Pro Git --after="2013-04-29T17:07:22+0200" \ --before="2013-04-29T17:07:22+0200" --pretty=fuller commit de7c201a10857e5d424dbd8db880a6f24ba250f9 Author: Ramkumar Ramachandra <[email protected]> AuthorDate: Mon Apr 29 18:19:37 2013 +0530 Commit: Junio C Hamano <[email protected]> CommitDate: Mon Apr 29 08:07:22 2013 -0700 git-completion.bash: lexical sorting for diff.statGraphWidth df44483a (diff --stat: add config option to limit graph width, 2012-03-01) added the option diff.startGraphWidth to the list of configuration variables in git-completion.bash, but failed to notice that the list is sorted alphabetically. Move it to its rightful place in the list. Signed-off-by: Ramkumar Ramachandra <[email protected]> Signed-off-by: Junio C Hamano <[email protected]> これらの⽇時(AuthorDate と CommitDate)はGitのデフォルトフォーマット(--date=default オ プション相当)です。作者とコミッター、それぞれのタイムゾーン情報を表⽰します。 ⽇時フォーマットの指定は他にも --date=iso (ISO 8601)、--date=rfc (RFC 2822)、-date=raw (Unix時間)、--date=local (端末のタイムゾーン)、--date=relative(『2 hours ago』 のように相対的な指定)などがあります。 また、 git log 実⾏時に⽇時指定を省略すると、パソコンの時計をもとにコマンド実⾏ ⽇時を指定⽇時として使⽤します(協定標準時からの時差も同⼀になります)。 具体的には、仮にパソコンの時計が09:00を指していて、かつタイムゾーン設定が協定 標準時プラス3時間の場合、以下の git log コマンドの⽇時指定は同⼀として扱われます。 $ git log --after=2008-06-01 --before=2008-07-01 $ git log --after="2008-06-01T09:00:00+0300" \ --before="2008-07-01T09:00:00+0300" もう⼀つ例を挙げておきましょう。Git ソースツリーのテストファイルに対する変更が あったコミットのうち、Junio Hamano がコミットしたもの (マージは除く) で 2008 年 10 ⽉(ニューヨークのタイムゾーン)に⾏われたものを知りたければ次のように指定しま す。 $ git log --pretty="%h - %s" --author=gitster \ --after="2008-10-01T00:00:00-0400" 36 \ Scott Chacon Pro Git 2.4節 作業のやり直し --before="2008-10-31T23:59:59-0400" --no-merges -- t/ 5610e3b - Fix testcase failure when extended attribute acd3b9e - Enhance hold_lock_file_for_{update,append}() f563754 - demonstrate breakage of detached checkout wi d1a43f2 - reset --hard/read-tree --reset -u: remove un 51a94af - Fix "checkout --track -b newbranch" on detac b0ad11e - pull: allow "git pull origin $something:$cur 約 36,000 件におよぶ Git ソースコードのコミットの歴史の中で、このコマンドの条件 にマッチするのは 6 件となります。 2.3.3 GUI による歴史の可視化 もう少しグラフィカルなツールでコミットの歴史を⾒たい場合は、Tcl/Tk のプログラ ムである gitk を⾒てみましょう。これは Git に同梱されています。gitk は、簡単に⾔う とビジュアルな git log ツールです。git log で使えるフィルタリングオプションにはほぼ すべて対応しています。プロジェクトのコマンドラインで gitk と打ち込むと、図 2-2 の ような画⾯があらわれるでしょう。 図 2.2: gitk history visualizer ウィンドウの上半分に、コミットの歴史がきれいな家系図とともに表⽰されます。ウィ ンドウの下半分には diff ビューアがあり、任意のコミットをクリックしてその変更内容を 確認することができます。 2.4 作業のやり直し どんな場⾯であっても、何かをやり直したくなることはあります。ここでは、⾏った変 更を取り消すための基本的なツールについて説明します。注意点は、ここで扱う内容の中 には「やり直しの取り消し」ができないものもあるということです。Git で何か間違えた ときに作業内容を失ってしまう数少ない例がここにあります。 37 第2章 Git の基本 Scott Chacon Pro Git 2.4.1 直近のコミットの変更 やり直しを⾏う場⾯としてもっともよくあるのは、「コミットを早まりすぎて追加すべ きファイルを忘れてしまった」「コミットメッセージが変になってしまった」などです。 そのコミットをもう⼀度やりなおす場合は、--amend オプションをつけてもう⼀度コミッ トします。 $ git commit --amend このコマンドは、ステージングエリアの内容をコミットに使⽤します。直近のコミット 以降に何も変更をしていない場合 (たとえば、コミットの直後にこのコマンドを実⾏した ような場合)、スナップショットの内容はまったく同じでありコミットメッセージを変更 することになります。 コミットメッセージのエディタが同じように⽴ち上がりますが、既に前回のコミット時 のメッセージが書き込まれた状態になっています。ふだんと同様にメッセージを編集でき ますが、前回のコミット時のメッセージがその内容で上書きされます。 たとえば、いったんコミットした後、何かのファイルをステージするのを忘れていたの に気づいたとしましょう。そんな場合はこのようにします。 $ git commit -m '初期コミット' $ git add 忘れてたファイル $ git commit --amend これら 3 つのコマンドの実⾏後、最終的にできあがるのはひとつのコミットです。⼆ 番⽬のコミットが、最初のコミットの結果を上書きするのです。 2.4.2 ステージしたファイルの取り消し 続くふたつのセクションでは、ステージングエリアと作業ディレクトリの変更に関する 作業を扱います。すばらしいことに、これらふたつの場所の状態を表⽰するコマンドを 使⽤すると、変更内容を取り消す⽅法も同時に表⽰されます。たとえば、ふたつのファイ ルを変更し、それぞれを別のコミットとするつもりだったのに間違えて git add * と打ち 込んでしまったときのことを考えましょう。ファイルが両⽅ともステージされてしまいま した。ふたつのうちの⼀⽅だけのステージを解除するにはどうすればいいでしょう? git status コマンドが教えてくれます。 $ git add . $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) 38 Scott Chacon Pro Git modified: README.txt modified: benchmarks.rb 2.4節 作業のやり直し 『Changes to be committed』 の直後に、『use git reset HEAD <file>... to unstage』 と書かれています。では、アドバイスに従って benchmarks.rb ファイルのステージを解除し てみましょう。 $ git reset HEAD benchmarks.rb Unstaged changes after reset: M benchmarks.rb $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: README.txt Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: benchmarks.rb ちょっと奇妙に⾒えるコマンドですが、きちんと動作します。benchmarks.rb ファイル は、変更されたもののステージされていない状態に戻りました。 2.4.3 ファイルへの変更の取り消し benchmarks.rb に加えた変更が、実は不要なものだったとしたらどうしますか? 変更を取 り消す (直近のコミット時点の状態、あるいは最初にクローンしたり最初に作業ディレク トリに取得したときの状態に戻す) 最も簡単な⽅法は? 幸いなことに、またもや git status がその⽅法を教えてくれます。先ほどの例の出⼒結果で、ステージされていないファイル ⼀覧の部分を⾒てみましょう。 Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: benchmarks.rb とても明確に、変更を取り消す⽅法が書かれています (少なくとも、バージョン 1.6.1 以降の新しい Git ではこのようになります。もし古いバージョンを使⽤しているのなら、 39 第2章 Git の基本 Scott Chacon Pro Git アップグレードしてこのすばらしい機能を活⽤することをおすすめします)。ではそのと おりにしてみましょう。 $ git checkout -- benchmarks.rb $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: README.txt 変更が取り消されたことがわかります。また、これが危険なコマンドであることも知っ ておかねばなりません。あなたがファイルに加えた変更はすべて消えてしまいます。変更 した内容を、別のファイルで上書きしたのと同じことになります。そのファイルが不要で あることが確実にわかっているとき以外は、このコマンドを使わないようにしましょう。 単にファイルを⽚付けたいだけなら、次の章で説明する stash やブランチを調べてみま しょう。⼀般にこちらのほうがおすすめの⽅法です。 Git にコミットした内容のすべては、ほぼ常に取り消しが可能であることを覚えておき ましょう。削除したブランチへのコミットや --amend コミットで上書きされた元のコミッ トでさえも復旧することができます (データの復元⽅法については 第9章 を参照くださ い)。しかし、まだコミットしていない内容を失ってしまうと、それは⼆度と取り戻せま せん。 2.5 リモートでの作業 Git を使ったプロジェクトで共同作業を進めていくには、リモートリポジトリの扱い⽅ を知る必要があります。リモートリポジトリとは、インターネット上あるいはその他ネッ トワーク上のどこかに存在するプロジェクトのことです。複数のリモートリポジトリを持 つこともできますし、それぞれを読み込み専⽤にしたり読み書き可能にしたりすること もできます。他のメンバーと共同作業を進めていくにあたっては、これらのリモートリポ ジトリを管理し、必要に応じてデータのプル・プッシュを⾏うことで作業を分担していく ことになります。リモートリポジトリの管理には「リモートリポジトリの追加」「不要に なったリモートリポジトリの削除」「リモートブランチの管理や追跡対象/追跡対象外の 設定」などさまざまな作業が含まれます。このセクションでは、これらの作業について説 明します。 2.5.1 リモートの表示 今までにどのリモートサーバーを設定したのかを知るには git remote コマンドを実⾏し ます。これは、今までに設定したリモートハンドルの名前を⼀覧表⽰します。リポジト リをクローンしたのなら、少なくとも origin という名前が⾒えるはずです。これは、ク ローン元のサーバーに対して Git がデフォルトでつける名前です。 40 Scott Chacon Pro Git 2.5節 リモートでの作業 $ git clone git://github.com/schacon/ticgit.git Cloning into 'ticgit'... remote: Reusing existing pack: 1857, done. remote: Total 1857 (delta 0), reused 0 (delta 0) Receiving objects: 100% (1857/1857), 374.35 KiB | 193.00 KiB/s, done. Resolving deltas: 100% (772/772), done. Checking connectivity... done. $ cd ticgit $ git remote origin -v を指定すると、その名前に対応する URL を表⽰します。 $ git remote -v origin git://github.com/schacon/ticgit.git (fetch) origin git://github.com/schacon/ticgit.git (push) 複数のリモートを設定している場合は、このコマンドはそれをすべて表⽰します。たと えば、私の Grit リポジトリの場合はこのようになっています。 $ cd grit $ git remote -v bakkdoor git://github.com/bakkdoor/grit.git cho45 git://github.com/cho45/grit.git defunkt git://github.com/defunkt/grit.git koke git://github.com/koke/grit.git origin [email protected]:mojombo/grit.git つまり、これらのユーザーによる変更を容易にプルして取り込めるということです。こ こで、origin リモートだけが SSH の URL であることに注⽬しましょう。私がプッシュ できるのは origin だけだということになります (なぜそうなるのかについては 第4章 で 説明します)。 2.5.2 リモートリポジトリの追加 これまでのセクションでも何度かリモートリポジトリの追加を⾏ってきましたが、ここ で改めてその⽅法をきちんと説明しておきます。新しいリモート Git リポジトリにアクセ スしやすいような名前をつけて追加するには、git remote add [shortname] [url] を実⾏しま す。 41 第2章 Git の基本 Scott Chacon Pro Git $ git remote origin $ git remote add pb git://github.com/paulboone/ticgit.git $ git remote -v origin git://github.com/schacon/ticgit.git pb git://github.com/paulboone/ticgit.git これで、コマンドラインに URL を全部打ち込むかわりに pb という⽂字列を指定するだ けでよくなりました。たとえば、Paul が持つ情報の中で⾃分のリポジトリにまだ存在し ないものをすべて取得するには、git fetch pb を実⾏すればよいのです。 $ git fetch pb remote: Counting objects: 58, done. remote: Compressing objects: 100% (41/41), done. remote: Total 44 (delta 24), reused 1 (delta 0) Unpacking objects: 100% (44/44), done. From git://github.com/paulboone/ticgit * [new branch] master -> pb/master * [new branch] ticgit -> pb/ticgit Paul の master ブランチは、ローカルでは pb/master としてアクセスできます。これを ⾃分のブランチにマージしたり、ローカルブランチとしてチェックアウトして中⾝を調べ たりといったことが可能となります。 2.5.3 リモートからのフェッチ、そしてプル ごらんいただいたように、データをリモートリポジトリから取得するには次のコマンド を実⾏します。 $ git fetch [remote-name] このコマンドは、リモートプロジェクトのすべてのデータの中からまだあなたが持って いないものを引き出します。実⾏後は、リモートにあるすべてのブランチを参照できるよ うになり、いつでもそれをマージしたり中⾝を調べたりすることが可能となります (ブラ ンチとは何なのか、どのように使うのかについては、第3章 でより詳しく説明します)。 リポジトリをクローンしたときには、リモートリポジトリに対して⾃動的に origin と いう名前がつけられます。つまり、git fetch origin とすると、クローンしたとき (あるい は直近でフェッチを実⾏したとき) 以降にサーバーにプッシュされた変更をすべて取得す ることができます。ひとつ注意すべき点は、fetch コマンドはデータをローカルリポジト リに引き出すだけだということです。ローカルの環境にマージされたり作業中の内容を書 42 Scott Chacon Pro Git 2.5節 リモートでの作業 き換えたりすることはありません。したがって、必要に応じて⾃分でマージをする必要が あります。 リモートブランチを追跡するためのブランチを作成すれば (次のセクションと 第3章 で 詳しく説明します)、git pull コマンドを使うことができます。これは、⾃動的にフェッ チを⾏い、リモートブランチの内容を現在のブランチにマージします。おそらくこのほ うが、よりお⼿軽で使いやすいことでしょう。またデフォルトで、git clone コマンドは ローカルの master ブランチが (取得元サーバー上の) リモートの master ブランチを追跡 するよう⾃動設定します (リモートに master ブランチが存在することを前提としていま す)。git pull を実⾏すると、通常は最初にクローンしたサーバーからデータを取得し、現 在作業中のコードへのマージを試みます。 2.5.4 リモートへのプッシュ あなたのプロジェクトがみんなと共有できる状態に達したら、それを上流にプッシュ しなければなりません。そのためのコマンドが git push [remote-name] [branch-name] です。 master ブランチの内容を origin サーバー (何度も⾔いますが、クローンした地点でこの ブランチ名とサーバー名が⾃動設定されます) にプッシュしたい場合は、このように実⾏ します。 $ git push origin master このコマンドが動作するのは、⾃分が書き込みアクセス権を持つサーバーからクローン し、かつその後だれもそのサーバーにプッシュしていない場合のみです。あなた以外の誰 かが同じサーバーからクローンし、誰かが上流にプッシュした後で⾃分がプッシュしよう とすると、それは拒否されます。拒否された場合は、まず誰かがプッシュした作業内容を 引き出してきてローカル環境で調整してからでないとプッシュできません。リモートサー バーへのプッシュ⽅法の詳細については 第3章 を参照ください。 2.5.5 リモートの調査 特定のリモートの情報をより詳しく知りたい場合は git remote show [remote-name] コマン ドを実⾏します。たとえば origin のように名前を指定すると、このような結果が得られま す。 $ git remote show origin * remote origin URL: git://github.com/schacon/ticgit.git Remote branch merged with 'git pull' while on branch master master Tracked remote branches master ticgit 43 第2章 Git の基本 Scott Chacon Pro Git リモートリポジトリの URL と、追跡対象になっているブランチの情報が表⽰されま す。また、ご丁寧にも「master ブランチ上で git pull すると、リモートの情報を取得し た後で⾃動的にリモートの master ブランチの内容をマージする」という説明がありま す。また、引き出してきたすべてのリモート情報も⼀覧表⽰されます。 Git をもっと使い込むようになると、git remote show で得られる情報はどんどん増えて いきます。たとえば次のような結果を得ることになるかもしれません。 $ git remote show origin * remote origin URL: [email protected]:defunkt/github.git Remote branch merged with 'git pull' while on branch issues issues Remote branch merged with 'git pull' while on branch master master New remote branches (next fetch will store in remotes/origin) caching Stale tracking branches (use 'git remote prune') libwalker walker2 Tracked remote branches acl apiv2 dashboard2 issues master postgres Local branch pushed with 'git push' master:master このコマンドは、特定のブランチ上で git push したときにどのブランチに⾃動プッシュ されるのかを表⽰しています。また、サーバー上のリモートブランチのうちまだ⼿元に 持っていないもの、⼿元にあるブランチのうちすでにサーバー上では削除されているも の、git pull を実⾏したときに⾃動的にマージされるブランチなども表⽰されています。 2.5.6 リモートの削除・リネーム リモートを参照する名前を変更したい場合、新しいバージョンの Git では git remote rename を使うことができます。たとえば pb を paul に変更したい場合は git remote rename をこのように実⾏します。 $ git remote rename pb paul $ git remote origin paul 44 Scott Chacon Pro Git 2.6節 タグ これは、リモートブランチ名も変更することを付け加えておきましょう。これまで pb/ master として参照していたブランチは、これからは paul/master となります。 何らかの理由でリモートの参照を削除したい場合 (サーバーを移動したとか特定のミ ラーを使わなくなったとか、あるいはプロジェクトからメンバーが抜けたとかいった場 合) は git remote rm を使⽤します。 $ git remote rm paul $ git remote origin 2.6 タグ 多くの VCS と同様に Git にもタグ機能があり、歴史上の重要なポイントに印をつける ことができます。⼀般に、この機能は (v 1.0 など) リリースポイントとして使われていま す。このセクションでは、既存のタグ⼀覧の取得や新しいタグの作成、さまざまなタグの 形式などについて扱います。 2.6.1 タグの一覧表示 Git で既存のタグの⼀覧を表⽰するのは簡単で、単に git tag と打ち込むだけです。 $ git tag v0.1 v1.3 このコマンドは、タグをアルファベット順に表⽰します。この表⽰順に深い意味はあり ません。 パターンを指定してタグを検索することもできます。Git のソースリポジトリを例にと ると、240 以上のタグが登録されています。その中で 1.4.2 系のタグのみを⾒たい場合 は、このようにします。 $ git tag -l 'v1.4.2.*' v1.4.2.1 v1.4.2.2 v1.4.2.3 v1.4.2.4 45 第2章 Git の基本 Scott Chacon Pro Git 2.6.2 タグの作成 Git のタグには、軽量 (lightweight) 版と注釈付き (annotated) 版の⼆通りがあります。 軽量版のタグは、変更のないブランチのようなものです。特定のコミットに対する単な るポインタでしかありません。しかし注釈付きのタグは、Git データベース内に完全なオ ブジェクトとして格納されます。チェックサムが付き、タグを作成した⼈の名前・メール アドレス・作成⽇時・タグ付け時のメッセージなども含まれます。また、署名をつけて GNU Privacy Guard (GPG) で検証することもできます。⼀般的には、これらの情報を含 められる注釈付きのタグを使うことをおすすめします。しかし、⼀時的に使うだけのタグ である場合や何らかの理由で情報を含めたくない場合は、軽量版のタグも使⽤可能です。 2.6.3 注釈付きのタグ Git では、注釈付きのタグをシンプルな⽅法で作成できます。もっとも簡単な⽅法 は、tag コマンドの実⾏時に -a を指定することです。 $ git tag -a v1.4 -m 'my version 1.4' $ git tag v0.1 v1.3 v1.4 -m で、タグ付け時のメッセージを指定します。これはタグとともに格納されます。注釈 付きタグの作成時にメッセージを省略すると、エディタが⽴ち上がるのでそこでメッセー ジを記⼊します。 タグのデータとそれに関連づけられたコミットを⾒るには git show コマンドを使⽤しま す。 $ git show v1.4 tag v1.4 Tagger: Scott Chacon <[email protected]> Date: Mon Feb 9 14:45:11 2009 -0800 my version 1.4 commit 15027957951b64cf874c3557a0f3547bd83b3ff6 Merge: 4a447f7... a6b4c97... Author: Scott Chacon <[email protected]> Date: Sun Feb 8 19:02:46 2009 -0800 Merge branch 'experiment' タグ付けした⼈の情報とその⽇時、そして注釈メッセージを表⽰したあとにコミットの 情報が続きます。 46 Scott Chacon Pro Git 2.6節 タグ 2.6.4 署名付きのタグ GPG 秘密鍵を持っていれば、タグに署名をすることができます。その場合は -a の代わ りに -s を指定すればいいだけです。 $ git tag -s v1.5 -m 'my signed 1.5 tag' You need a passphrase to unlock the secret key for user: "Scott Chacon <[email protected]>" 1024-bit DSA key, ID F721C45A, created 2009-02-09 このタグに対して git show を実⾏すると、あなたの GPG 署名が表⽰されます。 $ git show v1.5 tag v1.5 Tagger: Scott Chacon <[email protected]> Date: Mon Feb 9 15:22:20 2009 -0800 my signed 1.5 tag -----BEGIN PGP SIGNATURE----Version: GnuPG v1.4.8 (Darwin) iEYEABECAAYFAkmQurIACgkQON3DxfchxFr5cACeIMN+ZxLKggJQf0QYiQBwgySN Ki0An2JeAVUCAiJ7Ox6ZEtK+NvZAj82/ =WryJ -----END PGP SIGNATURE----commit 15027957951b64cf874c3557a0f3547bd83b3ff6 Merge: 4a447f7... a6b4c97... Author: Scott Chacon <[email protected]> Date: Sun Feb 8 19:02:46 2009 -0800 Merge branch 'experiment' タグの署名を検証する⽅法については後ほど説明します。 2.6.5 軽量版のタグ コミットにタグをつけるもうひとつの⽅法が、軽量版のタグです。これは基本的に、コ ミットのチェックサムだけを保持するもので、それ以外の情報は含まれません。軽量版の タグを作成するには -a、-s あるいは -m といったオプションをつけずにコマンドを実⾏し ます。 $ git tag v1.4-lw 47 第2章 Git の基本 Scott Chacon Pro Git $ git tag v0.1 v1.3 v1.4 v1.4-lw v1.5 このタグに対して git show を実⾏しても、先ほどのような追加情報は表⽰されません。 単に、対応するコミットの情報を表⽰するだけです。 $ git show v1.4-lw commit 15027957951b64cf874c3557a0f3547bd83b3ff6 Merge: 4a447f7... a6b4c97... Author: Scott Chacon <[email protected]> Date: Sun Feb 8 19:02:46 2009 -0800 Merge branch 'experiment' 2.6.6 タグの検証 署名付きのタグを検証するには git tag -v [tag-name] を使⽤します。このコマンドは、 GPG を使って署名を検証します。これを正しく実⾏するには、署名者の公開鍵があなた の鍵リングに含まれている必要があります。 $ git tag -v v1.4.2.1 object 883653babd8ee7ea23e6a5c392bb739348b1eb61 type commit tag v1.4.2.1 tagger Junio C Hamano <[email protected]> 1158138501 -0700 GIT 1.4.2.1 Minor fixes since 1.4.2, including git-mv and git-http with alternates. gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A gpg: Good signature from "Junio C Hamano <[email protected]>" gpg: aka "[jpeg image of size 1513]" Primary key fingerprint: 3565 2A26 2040 E066 C9A7 4A7D C0C6 D9A4 F311 9B9A 署名者の公開鍵を持っていない場合は、このようなメッセージが表⽰されます。 48 Scott Chacon Pro Git 2.6節 タグ gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A gpg: Can't check signature: public key not found error: could not verify the tag 'v1.4.2.1' 2.6.7 後からのタグ付け 過去にさかのぼってコミットにタグ付けすることもできます。仮にあなたのコミットの 歴史が次のようなものであったとしましょう。 $ git log --pretty=oneline 15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment' a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support 0d52aaab4479697da7686c15f77a3d64d9165190 one more thing 6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment' 0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc added a commit function 4682c3261057305bdd616e23b64b0857d832627b added a todo file 166ae0c4d3f420721acbb115cc33848dfcc2121a started write support 9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile 964f16d36dfccde844893cac5b347e7b3d44abbc commit the todo 8a5cbc430f1a9c3d00faaeffd07798508422908a updated readme 今になって、このプロジェクトに v1.2 のタグをつけるのを忘れていたことに気づきま した。本来なら 『updated rakefile』 のコミットにつけておくべきだったものです。し かし今からでも遅くありません。特定のコミットにタグをつけるには、そのコミットの チェックサム (あるいはその⼀部) をコマンドの最後に指定します。 $ git tag -a v1.2 -m 'version 1.2' 9fceb02 これで、そのコミットにタグがつけられたことが確認できます。 $ git tag v0.1 v1.2 v1.3 v1.4 v1.4-lw v1.5 $ git show v1.2 49 第2章 Git の基本 Scott Chacon Pro Git tag v1.2 Tagger: Scott Chacon <[email protected]> Date: Mon Feb 9 15:32:16 2009 -0800 version 1.2 commit 9fceb02d0ae598e95dc970b74767f19372d61af8 Author: Magnus Chacon <[email protected]> Date: Sun Apr 27 20:43:35 2008 -0700 updated rakefile ... 2.6.8 タグの共有 デフォルトでは、git push コマンドはタグ情報をリモートに送りません。タグを作った ら、タグをリモートサーバーにプッシュするよう明⽰する必要があります。その⽅法は、 リモートブランチを共有するときと似ています。git push origin [tagname] を実⾏するので す。 $ git push origin v1.5 Counting objects: 50, done. Compressing objects: 100% (38/38), done. Writing objects: 100% (44/44), 4.56 KiB, done. Total 44 (delta 18), reused 8 (delta 1) To [email protected]:schacon/simplegit.git * [new tag] v1.5 -> v1.5 多くのタグを⼀度にプッシュしたい場合は、git push コマンドのオプション --tags を使 ⽤します。これは、⼿元にあるタグのうちまだリモートサーバーに存在しないものをすべ て転送します。 $ git push origin --tags Counting objects: 50, done. Compressing objects: 100% (38/38), done. Writing objects: 100% (44/44), 4.56 KiB, done. Total 44 (delta 18), reused 8 (delta 1) To [email protected]:schacon/simplegit.git * [new tag] v0.1 -> v0.1 * [new tag] v1.2 -> v1.2 * [new tag] v1.4 -> v1.4 * [new tag] v1.4-lw -> v1.4-lw * [new tag] v1.5 -> v1.5 50 Scott Chacon Pro Git 2.7節 ヒントと裏技 これで、誰か他の⼈がリポジトリのクローンやプルを⾏ったときにすべてのタグを取得 できるようになりました。 2.7 ヒントと裏技 Git の基本を説明した本章を終える前に、ほんの少しだけヒントと裏技を披露しましょ う。これを知っておけば、Git をよりシンプルかつお⼿軽に使えるようになり、Git にな じみやすくなることでしょう。ほとんどの⼈はこれらのことを知らずに Git を使っていま す。別にどうでもいいことですし本書の後半でこれらの技を使うわけでもないのですが、 その⽅法ぐらいは知っておいたほうがよいでしょう。 2.7.1 自動補完 Bash シェルを使っているのなら、Git にはよくできた⾃動補完スクリプトが付属し ています。Git のソースコードをダウンロードし、contrib/completion ディレクトリを⾒て みましょう。git-completion.bash というファイルがあるはずです。このファイルをホーム ディレクトリにコピーし、それを .bashrc ファイルに追加しましょう。 source ~/.git-completion.bash すべてのユーザーに対して Git ⽤の Bash シェル補完を使わせたい場合は、Mac なら / opt/local/etc/bash_completion.d ディレクトリ、Linux 系なら /etc/bash_completion.d/ ディレ クトリにこのスクリプトをコピーします。Bash は、これらのディレクトリにあるスクリ プトを⾃動的に読み込んでシェル補完を⾏います。 Windows で Git Bash を使⽤している⼈は、msysGit で Windows 版 Git をインストー ルした際にデフォルトでこの機能が有効になっています。 Git コマンドの⼊⼒中にタブキーを押せば、補完候補があらわれて選択できるようにな ります。 $ git co<tab><tab> commit config ここでは、git co と打ち込んだ後にタブキーを⼆度押してみました。すると commit と config という候補があらわれました。さらに m<tab> と⼊⼒すると、⾃動的に git commit と 補完されます。 これは、コマンドのオプションに対しても機能します。おそらくこっちのほうがより有 ⽤でしょう。たとえば、git log を実⾏しようとしてそのオプションを思い出せなかった 場合、タブキーを押せばどんなオプションを使えるのかがわかります。 51 第2章 Git の基本 Scott Chacon Pro Git $ git log --s<tab> --shortstat --since= --src-prefix= --stat --summary この裏技を使えば、ドキュメントを調べる時間を節約できることでしょう。 2.7.2 Git エイリアス Git は、コマンドの⼀部だけが⼊⼒された状態でそのコマンドを推測することはありま せん。Git の各コマンドをいちいち全部⼊⼒するのがいやなら、git config でコマンドの エイリアスを設定することができます。たとえばこんなふうに設定すると便利かもしれま せん。 $ git config --global alias.co checkout $ git config --global alias.br branch $ git config --global alias.ci commit $ git config --global alias.st status こうすると、たとえば git commit と同じことが単に git ci と⼊⼒するだけでできるよ うになります。Git を使い続けるにつれて、よく使うコマンドがさらに増えてくることで しょう。そんな場合は、きにせずどんどん新しいエイリアスを作りましょう。 このテクニックは、「こんなことできたらいいな」というコマンドを作る際にも便利で す。たとえば、ステージを解除するときにどうしたらいいかいつも迷うという⼈なら、こ んなふうに⾃分で unstage エイリアスを追加してしまえばいいのです。 $ git config --global alias.unstage 'reset HEAD --' こうすれば、次のふたつのコマンドが同じ意味となります。 $ git unstage fileA $ git reset HEAD fileA 少しはわかりやすくなりましたね。あるいは、こんなふうに last コマンドを追加する こともできます。 $ git config --global alias.last 'log -1 HEAD' こうすれば、直近のコミットの情報を⾒ることができます。 52 Scott Chacon Pro Git 2.8節 まとめ $ git last commit 66938dae3329c7aebe598c2246a8e6af90d04646 Author: Josh Goebel <[email protected]> Date: Tue Aug 26 19:48:51 2008 +0800 test for current head Signed-off-by: Scott Chacon <[email protected]> Git が単に新しいコマンドをエイリアスで置き換えていることがわかります。しかし、 時には Git のサブコマンドではなく外部コマンドを実⾏したくなることもあるでしょう。 そんな場合は、コマンドの先頭に ! をつけます。これは、Git リポジトリ上で動作する⾃ 作のツールを書くときに便利です。例として、git visual で gitk が起動するようにしてみ ましょう。 $ git config --global alias.visual '!gitk' 2.8 まとめ これで、ローカルでの Git の基本的な操作がこなせるようになりました。リポジトリの 作成やクローン、リポジトリへの変更・ステージ・コミット、リポジトリのこれまでの変 更履歴の閲覧などです。次は、Git の強⼒な機能であるブランチモデルについて説明しま しょう。 53 第3章 Git のブランチ機能 ほぼすべてと⾔っていいほどの VCS が、何らかの形式でブランチ機能に対応していま す。ブランチとは、開発の本流から分岐し、本流の開発を邪魔することなく作業を続ける 機能のことです。多くの VCS ツールでは、これは多少コストのかかる処理になっていま す。ソースコードディレクトリを新たに作る必要があるなど、巨⼤なプロジェクトでは⾮ 常に時間がかかってしまうことがよくあります。 Git のブランチモデルは、Git の機能の中でもっともすばらしいものだという⼈もいる ほどです。そしてこの機能こそが Git を他の VCS とは⼀線を画すものとしています。何 がそんなにすばらしいのでしょう? Git のブランチ機能は圧倒的に軽量です。ブランチの 作成はほぼ⼀瞬で完了しますし、ブランチの切り替えも⾼速に⾏えます。その他⼤勢の VCS とは異なり、Git では頻繁にブランチ作成とマージを繰り返すワークフローを推奨し ています。⼀⽇に複数のブランチを切ることさえ珍しくありません。この機能を理解して ⾝につけることで、あなたはパワフルで他に類を⾒ないツールを⼿に⼊れることになりま す。これは、あなたの開発⼿法を⽂字通り⼀変させてくれるでしょう。 3.1 ブランチとは Git のブランチの仕組みについてきちんと理解するには、少し後戻りして Git がデータ を格納する⽅法を知っておく必要があります。第1章 で説明したように、Git はチェンジ セットや差分としてデータを保持しているのではありません。そうではなく、スナップ ショットとして保持しています。 Git にコミットすると、Git はコミットオブジェクトを作成して格納します。このオブ ジェクトには、あなたがステージしたスナップショットへのポインタや作者・メッセージ のメタデータ、そしてそのコミットの直接の親となるコミットへのポインタが含まれてい ます。最初のコミットの場合は親はいません。通常のコミットの場合は親がひとつ存在し ます。複数のブランチからマージした場合は、親も複数となります。 これを視覚化して考えるために、ここに 3 つのファイルを含むディレクトリがあると 仮定しましょう。3 つのファイルをすべてステージしてコミットしたところです。ステー ジしたファイルについてチェックサム (第1章 で説明した SHA-1 ハッシュ) を計算し、そ のバージョンのファイルを Git ディレクトリに格納し (Git はファイルを blob として扱い ます)、そしてそのチェックサムをステージングエリアに追加します。 55 第3章 Git のブランチ機能 Scott Chacon Pro Git $ git add README test.rb LICENSE $ git commit -m 'initial commit of my project' git commit を実⾏すると、プロジェクト内全ディレクトリのチェックサムが計算さ れ、tree オブジェクトとして Git リポジトリに格納されます。続いて、メタデータおよび さきほどの tree オブジェクトへのポインタを含むコミットオブジェクトを作成します。 これで、必要に応じてこのスナップショットを再作成できるようになります。 この時点で、Git リポジトリには 5 つのオブジェクトが含まれています。3 つのファイ ルそれぞれの中⾝をあらわす blob オブジェクト、ディレクトリの中⾝の⼀覧とどのファ イルがどの blob に対応するかをあらわすツリーオブジェクト、そしてそのルートツリー およびすべてのメタデータへのポインタを含むコミットオブジェクトです。Git リポジト リ内のデータを概念図であらわすと、図 3-1 のようになります。 図 3.1: ひとつのコミットをあらわすリポジトリ上のデータ なんらかの変更を終えて再びコミットすると、次のコミットには直近のコミットへのポ インタが格納されます。さらに 2 回のコミットを終えた後の履歴は、図 3-2 のようにな るでしょう。 図 3.2: 複数のコミットに対応する Git オブジェクト Git におけるブランチとは、単にこれら三つのコミットを指す軽量なポインタに過ぎま せん。Git のデフォルトのブランチ名は master です。最初にコミットした時点で、直近 のコミットを指す master ブランチが作られます。その後コミットを繰り返すたびに、この ポインタは⾃動的に進んでいきます。 新しいブランチを作成したら、いったいどうなるのでしょうか? 単に新たな移動先を指 56 Scott Chacon Pro Git 3.1節 ブランチとは 図 3.3: コミットデータの歴史を指すブランチ す新しいポインタが作られるだけです。では、新しい testing ブランチを作ってみましょ う。次の git branch コマンドを実⾏します。 $ git branch testing これで、新しいポインタが作られます。現時点ではふたつのポインタは同じ位置を指し ています (図 3-4 を参照ください)。 図 3.4: 複数のブランチがコミットデータの履歴を指す例 Git は、あなたが今どのブランチで作業しているのかをどうやって知るのでしょうか? それを保持する特別なポインタが HEAD と呼ばれるものです。これは、Subversion や CVS といった他の VCS における HEAD の概念とはかなり違うものであることに注意し ましょう。Git では、HEAD はあなたが作業しているローカルブランチへのポインタとな ります。今回の場合は、あなたはまだ master ブランチにいます。git branch コマンドは 新たにブランチを作成するだけであり、そのブランチに切り替えるわけではありません (図 3-5 を参照ください)。 ブランチを切り替えるには git checkout コマンドを実⾏します。それでは、新しい testing ブランチに移動してみましょう。 $ git checkout testing これで、HEAD は testing ブランチを指すようになります (図 3-6 を参照ください)。 それがどうしたって? では、ここで別のコミットをしてみましょう。 57 第3章 Git のブランチ機能 Scott Chacon Pro Git 図 3.5: 現在作業中のブランチを指す HEAD 図 3.6: ブランチを切り替えると、HEAD の指す先が移動する $ vim test.rb $ git commit -a -m 'made a change' 図 3-7 にその結果を⽰します。 図 3.7: HEAD が指すブランチが、コミットによって移動する 興味深いことに、testing ブランチはひとつ進みましたが master ブランチは変わってい ません。git checkout でブランチを切り替えたときの状態のままです。それでは master ブ ランチに戻ってみましょう。 58 Scott Chacon Pro Git 3.1節 ブランチとは $ git checkout master 図 3-8 にその結果を⽰します。 図 3.8: チェックアウトによって HEAD が別のブランチに移動する このコマンドは⼆つの作業をしています。まず HEAD ポインタが指す先を master ブラ ンチに戻し、そして作業ディレクトリ内のファイルを master が指すスナップショットの状 態に戻します。つまり、この時点以降に⾏った変更は、これまでのプロジェクトから分岐 した状態になるということです。これは、testing ブランチで⼀時的に⾏った作業を巻き 戻したことになります。ここから改めて別の⽅向に進めるということになります。 それでは、ふたたび変更を加えてコミットしてみましょう。 $ vim test.rb $ git commit -a -m 'made other changes' これで、プロジェクトの歴史が⼆つに分かれました (図 3-9 を参照ください)。新たなブ ランチを作成してそちらに切り替え、何らかの作業を⾏い、メインブランチに戻って別の 作業をした状態です。どちらの変更も、ブランチごとに分離しています。ブランチを切り 替えつつそれぞれの作業を進め、必要に応じてマージすることができます。これらをすべ て、シンプルに branch コマンドと checkout コマンドで⾏えるのです。 Git におけるブランチとは、実際のところ特定のコミットを指す 40 ⽂字の SHA-1 チェックサムだけを記録したシンプルなファイルです。したがって、ブランチを作成した り破棄したりするのは⾮常にコストの低い作業となります。新たなブランチの作成は、単 に 41 バイト (40 ⽂字と改⾏⽂字) のデータをファイルに書き込むのと同じくらい⾼速に ⾏えます。 これが他の⼤半の VCS ツールのブランチと対照的なところです。他のツールでは、プ ロジェクトのすべてのファイルを新たなディレクトリにコピーしたりすることになりま す。プロジェクトの規模にもよりますが、これには数秒から数分の時間がかかることで しょう。Git ならこの処理はほぼ瞬時に⾏えます。また、コミットの時点で親オブジェク トを記録しているので、マージの際にもどこを基準にすればよいのかを⾃動的に判断して くれます。そのためマージを⾏うのも⾮常に簡単です。これらの機能のおかげで、開発者 が気軽にブランチを作成して使えるようになっています。 では、なぜブランチを切るべきなのかについて⾒ていきましょう。 59 第3章 Git のブランチ機能 Scott Chacon Pro Git 図 3.9: ブランチの歴史が分裂した 3.2 ブランチとマージの基本 実際の作業に使うであろう流れを例にとって、ブランチとマージの処理を⾒てみましょ う。次の⼿順で進めます。 1. ウェブサイトに関する作業を⾏っている 2. 新たな作業⽤にブランチを作成する 3. そのブランチで作業を⾏う ここで、重⼤な問題が発⽣したので⾄急対応してほしいという連絡を受けました。その 後の流れは次のようになります。 1. 実運⽤環境⽤のブランチに戻る 2. 修正を適⽤するためのブランチを作成する 3. テストをした後で修正⽤ブランチをマージし、実運⽤環境⽤のブランチにプッシュ する 4. 元の作業⽤ブランチに戻り、作業を続ける 3.2.1 ブランチの基本 まず、すでに数回のコミットを済ませた状態のプロジェクトで作業をしているものと仮 定します (図 3-10 を参照ください)。 図 3.10: 短くて単純なコミットの歴史 ここで、あなたの勤務先で使っている何らかの問題追跡システムに登録されている問題 番号 53 への対応を始めることにしました。念のために⾔っておくと、Git は何かの問題 追跡システムと連動しているわけではありません。しかし、今回の作業はこの問題番号 53 に対応するものであるため、作業⽤に新しいブランチを作成します。ブランチの作成 60 Scott Chacon Pro Git 3.2節 ブランチとマージの基本 と新しいブランチへの切り替えを同時に⾏うには、git checkout コマンドに -b スイッチを つけて実⾏します。 $ git checkout -b iss53 Switched to a new branch 'iss53' これは、次のコマンドのショートカットです。 $ git branch iss53 $ git checkout iss53 図 3-11 に結果を⽰します。 図 3.11: 新たなブランチポインタの作成 ウェブサイト上で何らかの作業をしてコミットします。そうすると iss53 ブランチが先 に進みます。このブランチをチェックアウトしているからです (つまり、HEAD が iss53 ブランチを指しているということです。図 3-12 を参照ください)。 $ vim index.html $ git commit -a -m 'added a new footer [issue 53]' 図 3.12: 作業した結果、iss53 ブランチが移動した ここで、ウェブサイトに別の問題が発⽣したという連絡を受けました。そっちのほうを 優先して対応する必要があるとのことです。Git を使っていれば、ここで iss53 に関する 変更をリリースしてしまう必要はありません。また、これまでの作業をいったん元に戻し てから改めて優先度の⾼い作業にとりかかるなどという⼤変な作業も不要です。ただ単 に、master ブランチに戻るだけでよいのです。 61 第3章 Git のブランチ機能 Scott Chacon Pro Git しかしその前に注意すべき点があります。作業ディレクトリやステージングエリアに未 コミットの変更が残っている場合、それがもしチェックアウト先のブランチと衝突する 内容ならブランチの切り替えはできません。ブランチを切り替える際には、クリーンな 状態にしておくのが⼀番です。これを回避する⽅法もあります (stash およびコミットの amend という処理です) が、また後ほど説明します。今回はすべての変更をコミットし 終えているので、master ブランチに戻ることができます。 $ git checkout master Switched to branch 'master' 作業ディレクトリは問題番号 53 の対応を始める前とまったく同じ状態に戻りました。 これで、緊急の問題対応に集中できます。ここで覚えておくべき重要な点は、Git が作業 ディレクトリの状態をリセットし、チェックアウトしたブランチが指すコミットの時と同 じ状態にするということです。そのブランチにおける直近のコミットと同じ状態にするた め、ファイルの追加・削除・変更を⾃動的に⾏います。 次に、緊急の問題対応を⾏います。緊急作業⽤に hotfix ブランチを作成し、作業をそ こで進めるようにしましょう (図 3-13 を参照ください)。 $ git checkout -b hotfix Switched to a new branch 'hotfix' $ vim index.html $ git commit -a -m 'fixed the broken email address' [hotfix 3a0874c] fixed the broken email address 1 files changed, 1 deletion(-) 図 3.13: master ブランチから新たに作成した hotfix ブランチ テストをすませて修正がうまくいったことを確認したら、master ブランチにそれを マージしてリリースします。ここで使うのが git merge コマンドです。 $ git checkout master $ git merge hotfix 62 Scott Chacon Pro Git 3.2節 ブランチとマージの基本 Updating f42c576..3a0874c Fast-forward README | 1 1 file changed, 1 deletion(-) このマージ処理で 『Fast-forward』 というフレーズが登場したのにお気づきでしょう か。マージ先のブランチが指すコミットがマージ元のコミットの直接の親であるため、 Git がポインタを前に進めたのです。⾔い換えると、あるコミットに対してコミット履歴 上で直接到達できる別のコミットをマージしようとした場合、Git は単にポインタを前に 進めるだけで済ませます。マージ対象が分岐しているわけではないからです。この処理の ことを 『fast forward』 と⾔います。 変更した内容が、これで master ブランチの指すスナップショットに反映されました。こ れで変更をリリースできます (図 3-14 を参照ください)。 図 3.14: マージした結果、master ブランチの指す先が hotfix ブランチと同じ場所になっ た 超重要な修正作業が終わったので、横やりが⼊る前にしていた作業に戻ることができ ます。しかしその前に、まずは hotfix ブランチを削除しておきましょう。master ブランチ が同じ場所を指しているので、もはやこのブランチは不要だからです。削除するには git branch で -d オプションを指定します。 $ git branch -d hotfix Deleted branch hotfix (was 3a0874c). では、先ほどまで問題番号 53 の対応をしていたブランチに戻り、作業を続けましょう (図 3-15 を参照ください)。 $ git checkout iss53 Switched to branch 'iss53' $ vim index.html $ git commit -a -m 'finished the new footer [issue 53]' 63 第3章 Git のブランチ機能 Scott Chacon Pro Git [iss53 ad82d7a] finished the new footer [issue 53] 1 file changed, 1 insertion(+) 図 3.15: iss53 ブランチは独立して進めることができる ここで、hotfix ブランチ上で⾏った作業は iss53 ブランチには含まれていないことに注 意しましょう。もしそれを取得する必要があるのなら、⽅法はふたつあります。ひとつ は git merge master で master ブランチの内容を iss53 ブランチにマージすること。そしても うひとつはそのまま作業を続け、いつか iss53 ブランチの内容を master に適⽤することに なった時点で統合することです。 3.2.2 マージの基本 問題番号 53 の対応を終え、master ブランチにマージする準備ができたとしましょ う。iss53 ブランチのマージは、先ほど hotfix ブランチをマージしたときとまったく同じ ような⼿順でできます。つまり、マージ先のブランチに切り替えてから git merge コマン ドを実⾏するだけです。 $ git checkout master $ git merge iss53 Auto-merging README Merge made by the 'recursive' strategy. README | 1 + 1 file changed, 1 insertion(+) 先ほどの hotfix のマージとはちょっとちがう感じですね。今回の場合、開発の歴史が過 去のとある時点で分岐しています。マージ先のコミットがマージ元のコミットの直系の先 祖ではないため、Git 側でちょっとした処理が必要だったのです。ここでは、各ブランチ が指すふたつのスナップショットとそれらの共通の先祖との間で三⽅向のマージを⾏いま した。図 3-16 に、今回のマージで使⽤した三つのスナップショットを⽰します。 単にブランチのポインタを先に進めるのではなく、Git はこの三⽅向のマージ結果から 新たなスナップショットを作成し、それを指す新しいコミットを⾃動作成します (図 3-17 を参照ください)。これはマージコミットと呼ばれ、複数の親を持つ特別なコミットとな ります。 64 Scott Chacon Pro Git 3.2節 ブランチとマージの基本 図 3.16: Git が共通の先祖を自動的に見つけ、ブランチのマージに使用する マージの基点として使⽤する共通の先祖を Git が⾃動的に判別するというのが特筆すべ き点です。CVS や Subversion (バージョン 1.5 より前のもの) は、マージの基点となる ポイントを⾃分で⾒つける必要があります。これにより、他のシステムに⽐べて Git の マージが⾮常に簡単なものとなっているのです。 図 3.17: マージ作業の結果から、Git が自動的に新しいコミットオブジェクトを作成する これで、今までの作業がマージできました。もはや iss53 ブランチは不要です。削除し てしまい、問題追跡システムのチケットもクローズしておきましょう。 $ git branch -d iss53 3.2.3 マージ時のコンフリクト 物事は常にうまくいくとは限りません。同じファイルの同じ部分をふたつのブランチで 別々に変更してそれをマージしようとすると、Git はそれをうまくマージする⽅法を⾒つ けられないでしょう。問題番号 53 の変更が仮に hotfix ブランチと同じところを扱ってい たとすると、このようなコンフリクトが発⽣します。 65 第3章 Git のブランチ機能 Scott Chacon Pro Git $ git merge iss53 Auto-merging index.html CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result. Git は新たなマージコミットを⾃動的には作成しませんでした。コンフリクトを解決す るまで、処理は中断されます。コンフリクトが発⽣してマージできなかったのがどのファ イルなのかを知るには git status を実⾏します。 $ git status On branch master You have unmerged paths. (fix conflicts and run "git commit") Unmerged paths: (use "git add <file>..." to mark resolution) both modified: index.html no changes added to commit (use "git add" and/or "git commit -a") コンフリクトが発⽣してまだ解決されていないものについては unmerged として表⽰ されます。Git は、標準的なコンフリクトマーカーをファイルに追加するので、ファイル を開いてそれを解決することにします。コンフリクトが発⽣したファイルの中には、この ような部分が含まれています。 <<<<<<< HEAD <div id="footer">contact : [email protected]</div> ======= <div id="footer"> please contact us at [email protected] </div> >>>>>>> iss53 これは、HEAD (merge コマンドを実⾏したときにチェックアウトしていたブランチな ので、ここでは master となります) の内容が上の部分 (======= の上にある内容)、そして iss53 ブランチの内容が下の部分であるということです。コンフリクトを解決するには、 どちらを採⽤するかをあなたが判断することになります。たとえば、ひとつの解決法とし てブロック全体を次のように書き換えます。 66 Scott Chacon Pro Git 3.2節 ブランチとマージの基本 <div id="footer"> please contact us at [email protected] </div> このような解決を各部分に対して⾏い、<<<<<<< や ======= そして >>>>>>> の⾏をすべて 除去します。そしてすべてのコンフリクトを解決したら、各ファイルに対して git add を 実⾏して解決済みであることを通知します。ファイルをステージすると、Git はコンフリ クトが解決されたと⾒なします。コンフリクトの解決をグラフィカルに⾏いたい場合は git mergetool を実⾏します。これは、適切なビジュアルマージツールを⽴ち上げてコンフ リクトの解消を⾏います。 $ git mergetool This message is displayed because 'merge.tool' is not configured. See 'git mergetool --tool-help' or 'git help config' for more details. 'git mergetool' will now attempt to use one of the following tools: opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff Merging: index.html Normal merge conflict for 'index.html': {local}: modified file {remote}: modified file Hit return to start merge resolution tool (opendiff): デフォルトのツール (Git は opendiff を選びました。私がこのコマンドを Mac で実⾏し たからです) 以外のマージツールを使いたい場合は、『… one of the following tools:』 にあるツール⼀覧を⾒ましょう。そして、使いたいツールの名前を打ち込みます。第7章 で、環境にあわせてこのデフォルトを変更する⽅法を説明します。 マージツールを終了させると、マージに成功したかどうかを Git が聞いてきます。成功 したと伝えると、ファイルを⾃動的にステージしてコンフリクトが解決したことを⽰しま す。 再び git status を実⾏すると、すべてのコンフリクトが解決したことを確認できます。 $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: index.html 67 第3章 Git のブランチ機能 Scott Chacon Pro Git 結果に満⾜し、すべてのコンフリクトがステージされていることが確認できたら、git commit を実⾏してマージコミットを完了させます。デフォルトのコミットメッセージは、 このようになります。 Merge branch 'iss53' Conflicts: index.html # # It looks like you may be committing a merge. # If this is not correct, please remove the file # .git/MERGE_HEAD # and try again. # このメッセージを変更して、どのようにして衝突を解決したのかを詳しく説明しておく のもよいでしょう。後から他の⼈がそのマージを⾒たときに、あなたがなぜそのようにし たのかがわかりやすくなります。 3.3 ブランチの管理 これまでにブランチの作成、マージ、そして削除を⾏いました。ここで、いくつかのブ ランチ管理ツールについて⾒ておきましょう。今後ブランチを使い続けるにあたって、こ れらのツールが便利に使えるでしょう。 git branch コマンドは、単にブランチを作ったり削除したりするだけのものではありま せん。何も引数を渡さずに実⾏すると、現在のブランチの⼀覧を表⽰します。 $ git branch iss53 * master testing * という⽂字が master ブランチの先頭についていることに注⽬しましょう。これは、 現在チェックアウトされているブランチを意味します。つまり、ここでコミットを⾏う と、master ブランチがひとつ先に進むということです。各ブランチにおける直近のコミッ トを調べるには git branch -v を実⾏します。 $ git branch -v iss53 93b412c fix javascript issue * master 7a98805 Merge branch 'iss53' testing 782fd34 add scott to the author list in the readmes 68 Scott Chacon Pro Git 3.4節 ブランチでの作業の流れ 各ブランチの状態を知るために便利なもうひとつの機能として、現在作業中のブランチ にマージ済みかそうでないかによる絞り込みができるようになっています。Git には、そ のための便利なオプション --merged と --no-merged があります。現在作業中のブランチに マージ済みのブランチを調べるには git branch --merged を実⾏します。 $ git branch --merged iss53 * master すでに先ほど iss53 ブランチをマージしているので、この⼀覧に表⽰されています。 このリストにあがっているブランチのうち先頭に * がついていないものは、通常は git branch -d で削除してしまって問題ないブランチです。すでにすべての作業が別のブランチ に取り込まれているので、もはや何も失うことはありません。 まだマージされていない作業を持っているすべてのブランチを知るには、git branch -no-merged を実⾏します。 $ git branch --no-merged testing 先ほどのブランチとは別のブランチが表⽰されます。まだマージしていない作業が残っ ているので、このブランチを git branch -d で削除しようとしても失敗します。 $ git branch -d testing error: The branch 'testing' is not fully merged. If you are sure you want to delete it, run 'git branch -D testing'. 本当にそのブランチを消してしまってよいのなら -D で強制的に消すこともできます。 ……と、親切なメッセージで教えてくれていますね。 3.4 ブランチでの作業の流れ ブランチとマージの基本操作はわかりましたが、ではそれを実際にどう使えばいいので しょう? このセクションでは、気軽にブランチを切れることでどういった作業ができるよ うになるのかを説明します。みなさんのふだんの開発サイクルにうまく取り込めるかどう かの判断材料としてください。 3.4.1 長期稼働用ブランチ Git では簡単に三⽅向のマージができるので、あるブランチから別のブランチへのマー ジを⻑期間にわたって繰り返すのも簡単なことです。つまり、複数のブランチを常にオー 69 第3章 Git のブランチ機能 Scott Chacon Pro Git プンさせておいて、それぞれ開発サイクルにおける別の場⾯⽤に使うということもできま す。定期的にブランチ間でのマージを⾏うことが可能です。 Git 開発者の多くはこの考え⽅にもとづいた作業の流れを採⽤しています。つまり、完 全に安定したコードのみを master ブランチに置き、いつでもリリースできる状態にしてい るのです。それ以外に並⾏して develop や next といった名前のブランチを持ち、安定性 をテストするためにそこを使⽤します。常に安定している必要はありませんが、安定した 状態になったらそれを master にマージすることになります。また、時にはトピックブラン チ (先ほどの例の iss53 ブランチのような短期間のブランチ) を作成し、すべてのテストに 通ることやバグが発⽣していないことを確認することもあります。 実際のところ今話している内容は、⼀連のコミットの中のどの部分をポインタが指して いるかということです。安定版のブランチはコミット履歴上の奥深くにあり、最前線のブ ランチは履歴上の先端にいます (図 3-18 を参照ください)。 図 3.18: 安定したブランチほど、一般的にコミット履歴の奥深くに存在する 各ブランチを作業⽤のサイロと考えることもできます。⼀連のコミットが、完全にテス トを通るようになった時点でより安定したサイロに移動するのです (図 3-19 を参照くだ さい)。 図 3.19: ブランチをサイロとして考えるとわかりやすいかも 同じようなことを、安定性のレベルを何段階かにして⾏うこともできます。⼤規模な プロジェクトでは、proposed あるいは pu (proposed updates) といったブランチを⽤意し て、next ブランチあるいは master ブランチに投⼊する前にそこでいったんブランチを統合 するというようにしています。安定性のレベルに応じて何段階かのブランチを作成し、安 定性が⼀段階上がった時点で上位レベルのブランチにマージしていくという考え⽅です。 念のために⾔いますが、このように複数のブランチを常時稼働させることは必須ではあり ません。しかし、巨⼤なプロジェクトや複雑なプロジェクトに関わっている場合は便利な ことでしょう。 3.4.2 トピックブランチ ⼀⽅、トピックブランチはプロジェクトの規模にかかわらず便利なものです。トピック ブランチとは、短期間だけ使うブランチのことで、何か特定の機能やそれに関連する作業 70 Scott Chacon Pro Git 3.4節 ブランチでの作業の流れ を⾏うために作成します。これは、今までの VCS では実現不可能に等しいことでした。 ブランチを作成したりマージしたりという作業が⾮常に⼿間のかかることだったからで す。Git では、ブランチを作成して作業をし、マージしてからブランチを削除するという 流れを⼀⽇に何度も繰り返すことも珍しくありません。 先ほどのセクションで作成した iss53 ブランチや hotfix ブランチが、このトピックブラ ンチにあたります。ブランチ上で数回コミットし、それをメインブランチにマージしたら すぐに削除しましたね。この⽅法を使えば、コンテキストの切り替えを⼿早く完全に⾏う ことができます。それぞれの作業が別のサイロに分離されており、そのブランチ内の変更 は特定のトピックに関するものだけなのですから、コードレビューなどの作業が容易にな ります。⼀定の間ブランチで保持し続けた変更は、マージできるようになった時点で (ブ ランチを作成した順や作業した順に関係なく) すぐにマージしていきます。 次のような例を考えてみましょう。まず (master で) 何らかの作業をし、問題対応のため に (iss91 に) ブランチを移動し、そこでなにがしかの作業を⾏い、「あ、こっちのほうが よかったかも」と気づいたので新たにブランチを作成 (iss91v2) して思いついたことをそ こで試し、いったん master ブランチに戻って作業を続け、うまくいくかどうかわからな いちょっとしたアイデアを試すために新たなブランチ (dumbidea ブランチ) を切りました。 この時点で、コミットの歴史は図 3-20 のようになります。 図 3.20: 複数のトピックブランチを作成した後のコミットの歴史 最終的に、問題を解決するための⽅法としては⼆番⽬ (iss91v2) のほうがよさげだとわ かりました。また、ちょっとした思いつきで試してみた dumbidea ブランチが意外とよさげ で、これはみんなに公開すべきだと判断しました。最初の iss91 ブランチは放棄してしま い (コミット C5 と C6 の内容は失われます)、他のふたつのブランチをマージしました。 この時点で、歴史は図 3-21 のようになっています。 ここで重要なのは、これまで作業してきたブランチが完全にローカル環境に閉じてい たということです。ブランチを作ったりマージしたりといった作業は、すべてみなさんの Git リポジトリ内で完結しており、サーバーとのやりとりは発⽣していません。 71 第3章 Git のブランチ機能 Scott Chacon Pro Git 図 3.21: dumbidea と iss91v2 をマージした後の歴史 3.5 リモートブランチ リモートブランチは、リモートリポジトリ上のブランチの状態を指すものです。ネット ワーク越しの操作をしたときに⾃動的に移動します。リモートブランチは、前回リモート リポジトリに接続したときにブランチがどの場所を指していたかを⽰すブックマークのよ うなものです。 ブランチ名は (remote)/(branch) のようになります。たとえば、origin サーバーに最後に 接続したときの master ブランチの状態を知りたければ origin/master ブランチをチェック します。誰かほかの⼈と共同で問題に対応しており、相⼿が iss53 ブランチにプッシュし たとしましょう。あなたの⼿元にはローカルの iss53 ブランチがあります。しかし、サー バー側のブランチは origin/iss53 のコミットを指しています。 ……ちょっと混乱してきましたか? では、具体例で考えてみましょう。ネットワーク上 の git.ourcompany.com に Git サーバーがあるとします。これをクローンすると、Git はそれ に origin という名前をつけ、すべてのデータを引き出し、master ブランチを指すポインタ を作成し、そのポインタにローカルで origin/master という名前をつけます。それを⾃分で 移動させることはできません。Git はまた、master というブランチも作成します。これは origin の master ブランチと同じ場所を指しており、ここから何らかの作業を始めます (図 3-22 を参照ください)。 ローカルの master ブランチで何らかの作業をしている間に、誰かが git.ourcompany.com にプッシュして master ブランチを更新したとしましょう。この時点であなたの歴史とは ことなる状態になってしまいます。また、origin サーバーと再度接続しない限り、origin/ master が指す先は移動しません (図 3-23 を参照ください)。 ⼿元での作業を同期させるには、git fetch origin コマンドを実⾏します。このコマンド は、まず origin が指すサーバー (今回の場合は git.ourcompany.com) を探し、まだ⼿元にな いデータをすべて取得し、ローカルデータベースを更新し、origin/master が指す先を最新 の位置に変更します (図 3-24 を参照ください)。 複数のリモートサーバーがあった場合にリモートのブランチがどのようになるのか 72 Scott Chacon Pro Git 3.5節 リモートブランチ 図 3.22: git clone により、ローカルの master ブランチのほかに origin の master ブラ ンチを指す origin/master が作られる 図 3.23: ローカルで作業している間に誰かがリモートサーバーにプッシュすると、両者 の歴史が異なるものとなる を知るために、もうひとつ Git サーバーがあるものと仮定しましょう。こちらのサー バーは、チームの⼀部のメンバーが開発⽬的にのみ使⽤しています。このサーバーは git.team1.ourcompany.com にあるものとしましょう。このサーバーをあなたの作業中のプロ ジェクトから参照できるようにするには、第2章 で紹介した git remote add コマンドを使 ⽤します。このリモートに teamone という名前をつけ、URL ではなく短い名前で参照でき るようにします (図 3-25 を参照ください)。 git fetch teamone を実⾏すれば、まだ⼿元にないデータをリモートの teamone サーバーか らすべて取得できるようになりました。今回、このサーバーが保持してるデータは origin サーバーが保持するデータの⼀部なので、Gitは何のデータも取得しません。代わりに、 teamone/master というリモートブランチが指すコミットを、teamone サーバーの master ブラ ンチが指すコミットと同じにします。 (図 3-26 を参照ください)。 73 第3章 Git のブランチ機能 Scott Chacon Pro Git 図 3.24: git fetch コマンドによるリモートへの参照の更新 図 3.25: 別のサーバーをリモートとして追加 3.5.1 プッシュ ブランチの内容をみんなと共有したくなったら、書き込み権限を持つどこかのリモート にそれをプッシュしなければなりません。ローカルブランチの内容が⾃動的にリモートと 同期されることはありません。共有したいブランチは、明⽰的にプッシュする必要があり ます。たとえば、共有したくない内容はプライベートなブランチで作業を進め、共有した い内容だけのトピックブランチを作成してそれをプッシュするということもできます。 ⼿元にある serverfix というブランチを他⼈と共有したい場合は、最初のブランチを プッシュしたときと同様の⽅法でそれをプッシュします。つまり git push (remote) (branch) を実⾏します。 74 Scott Chacon Pro Git 3.5節 リモートブランチ 図 3.26: teamone の master ブランチの位置をローカルに取得する $ git push origin serverfix Counting objects: 20, done. Compressing objects: 100% (14/14), done. Writing objects: 100% (15/15), 1.74 KiB, done. Total 15 (delta 5), reused 0 (delta 0) To [email protected]:schacon/simplegit.git * [new branch] serverfix -> serverfix これは、ちょっとしたショートカットです。Git はまずブランチ名 serverfix を refs/ heads/ serverfix:refs/ heads/ serverfix に 展 開 し ま す。 こ れ は 「⼿ 元 の ロー カ ル ブ ラ ン チ serverfix をプッシュして、リモートの serverfix ブランチを更新しろ」という意味で す。refs/heads/ の部分の意味については第9章 で詳しく説明しますが、これは⼀般的に 省略可能です。git push origin serverfix:serverfix とすることもできます。これも同じこ とで、「こっちの serverfix で、リモートの serverfix を更新しろ」という意味になりま す。この⽅式を使えば、ローカルブランチの内容をリモートにある別の名前のブランチに プッシュすることができます。リモートのブランチ名を serverfix という名前にしたくな い場合は、git push origin serverfix:awesomebranch とすればローカルの serverfix ブランチを リモートの awesomebranch という名前のブランチ名でプッシュすることができます。 次 に 誰 か が サー バー か ら フェッ チ し た と き に は、 そ の ⼈ が 取 得 す る サー バー 上 の serverfix はリモートブランチ origin/serverfix となります。 $ git fetch origin remote: Counting objects: 20, done. remote: Compressing objects: 100% (14/14), done. remote: Total 15 (delta 5), reused 0 (delta 0) Unpacking objects: 100% (15/15), done. From [email protected]:schacon/simplegit 75 第3章 Git のブランチ機能 * [new branch] serverfix Scott Chacon Pro Git -> origin/serverfix 注意すべき点は、新しいリモートブランチを取得したとしても、それが⾃動的にローカ ルで編集可能になるわけではないというところです。⾔い換えると、この場合に新たに serverfix ブランチができるわけではないということです。できあがるのは origin/serverfix ポインタだけであり、これは変更することができません。 この作業を現在の作業ブランチにマージするには、git merge origin/serverfix を実⾏し ます。ローカル環境に serverfix ブランチを作ってそこで作業を進めたい場合は、リモー トブランチからそれを作成します。 $ git checkout -b serverfix origin/serverfix Branch serverfix set up to track remote branch serverfix from origin. Switched to a new branch 'serverfix' これで、origin/serverfix が指す先から作業を開始するためのローカルブランチができあ がりました。 3.5.2 追跡ブランチ リモートブランチからローカルブランチにチェックアウトすると、追跡ブランチ (tracking branch) というブランチが⾃動的に作成されます。追跡ブランチとは、リモートブラ ンチと直接のつながりを持つローカルブランチのことです。追跡ブランチ上で git push を 実⾏すると、Git は⾃動的にプッシュ先のサーバーとブランチを判断します。また、追跡 ブランチ上で git pull を実⾏すると、リモートの参照先からすべてのデータを取得し、対 応するリモートブランチの内容を⾃動的にマージします。 あるリポジトリをクローンしたら、⾃動的に master ブランチを作成し、origin/master を 追跡するようになります。これが、git push や git pull が引数なしでもうまく動作する 理由です。しかし、必要に応じてそれ以外の追跡ブランチを作成し、origin 以外にある ブランチや master 以外のブランチを追跡させることも可能です。シンプルな⽅法として は、git checkout -b [branch] [remotename]/[branch] を実⾏します。Git バージョン 1.6.2 以 降では、より簡単に --track を使うことができます。 $ git checkout --track origin/serverfix Branch serverfix set up to track remote branch serverfix from origin. Switched to a new branch 'serverfix' ローカルブランチをリモートブランチと違う名前にしたい場合は、最初に紹介した⽅法 でローカルブランチに別の名前を指定します。 $ git checkout -b sf origin/serverfix 76 Scott Chacon Pro Git 3.6節 リベース Branch sf set up to track remote branch serverfix from origin. Switched to a new branch 'sf' これで、ローカルブランチ sf が⾃動的に origin/serverfix を追跡するようになりまし た。 3.5.3 リモートブランチの削除 リモートブランチでの作業が終わったとしましょう。つまり、あなたや他のメンバーが ⼀通りの作業を終え、それをリモートの master ブランチ (あるいは安定版のコードライン となるその他のブランチ) にマージし終えたということです。リモートブランチを削除す るコマンドは、少しわかりにくい構⽂ですが git push [remotename] :[branch] となります。 サーバーの serverfix ブランチを削除したい場合は次のようになります。 $ git push origin :serverfix To [email protected]:schacon/simplegit.git - [deleted] serverfix ドッカーン。これでブランチはサーバーから消えてしまいました。このページの端を 折っておいたほうがいいかもしれませんね。実際にこのコマンドが必要になったときに は、おそらくこの構⽂を忘れてしまっているでしょうから。このコマンドを覚えるコツ は、少し前に説明した構⽂ git push [remotename] [localbranch]:[remotebranch] を思い出すこ とです。[localbranch] の部分をそのまま残して考えると、これは基本的に「こっちの (何 もなし) で、向こうの [remotebranch] を更新しろ」と⾔っていることになります。 3.6 リベース Git には、あるブランチの変更を別のブランチに統合するための⽅法が⼤きく分けて⼆ つあります。merge と rebase です。このセクションでは、リベースについて「どういう意 味か」「どのように⾏うのか」「なぜそんなにもすばらしいのか」「どんなときに使うの か」を説明します。 3.6.1 リベースの基本 マージについての説明で使⽤した例を振り返ってみましょう (図 3-27 を参照くださ い)。作業が⼆つに分岐しており、それぞれのブランチに対してコミットされていること がわかります。 このブランチを統合する最も簡単な⽅法は、先に説明したように merge コマンドを使う ことです。これは、⼆つのブランチの最新のスナップショット (C3 と C4) とそれらの共 通の祖先 (C2) による三⽅向のマージを⾏い、新しいスナップショットを作成 (そしてコ ミット) します。その結果は図 3-28 のようになります。 しかし、別の⽅法もあります。C3 で⾏った変更のパッチを取得し、それを C4 の先 端に適⽤するのです。Git では、この作業のことを リベース (rebasing) と呼んでいま 77 第3章 Git のブランチ機能 Scott Chacon Pro Git 図 3.27: 分岐したコミットの歴史 図 3.28: 分岐した作業履歴をひとつに統合する す。rebase コマンドを使⽤すると、⼀⽅のブランチにコミットされたすべての変更をもう ⼀⽅のブランチで再現することができます。 今回の例では、次のように実⾏します。 $ git checkout experiment $ git rebase master First, rewinding head to replay your work on top of it... Applying: added staged command これは、まずふたつのブランチ (現在いるブランチとリベース先のブランチ) の共通の 先祖に移動し、現在のブランチ上の各コミットの diff を取得して⼀時ファイルに保存し、 現在のブランチの指す先をリベース先のブランチと同じコミットに移動させ、そして先ほ どの変更を順に適⽤していきます。図 3-29 にこの⼿順をまとめました。 図 3.29: C3 での変更の C4 へのリベース この時点で、master ブランチに戻って fast-forward マージができるようになりました (図 3-30 を参照ください)。 78 Scott Chacon Pro Git 3.6節 リベース 図 3.30: master ブランチの Fast-forward これで、C3ʼ が指しているスナップショットの内容は、先ほどのマージの例で C5 が 指すスナップショットと全く同じものになりました。最終的な統合結果には差がありませ んが、リベースのほうがよりすっきりした歴史になります。リベース後のブランチのログ を⾒ると、まるで⼀直線の歴史のように⾒えます。元々平⾏稼働していたにもかかわら ず、それが⼀連の作業として⾒えるようになるのです。 リモートブランチ上での⾃分のコミットをすっきりさせるために、よくこの作業を⾏い ます。たとえば、⾃分がメンテナンスしているのではないプロジェクトに対して貢献した いと考えている場合などです。この場合、あるブランチ上で⾃分の作業を⾏い、プロジェ クトに対してパッチを送る準備ができたらそれを origin/master にリベースすることになり ます。そうすれば、メンテナは特に統合作業をしなくても単に fast-forward するだけで 済ませられるのです。 あなたが最後に⾏ったコミットが指すスナップショットは、リベースした結果の最後の コミットであってもマージ後の最終のコミットであっても同じものとなることに注意しま しょう。違ってくるのは、そこに⾄る歴史だけです。リベースは、⼀⽅のラインの作業内 容をもう⼀⽅のラインに順に適⽤しますが、マージの場合はそれぞれの最終地点を統合し ます。 3.6.2 さらに興味深いリベース リベース先のブランチ以外でもそのリベースを再現することができます。たとえば図 3-31 のような歴史を考えてみましょう。トピックブランチ (server) を作成してサーバー 側の機能をプロジェクトに追加し、それをコミットしました。その後、そこからさらに クライアント側の変更⽤のブランチ (client) を切って数回コミットしました。最後に、 server ブランチに戻ってさらに何度かコミットを⾏いました。 クライアント側の変更を本流にマージしてリリースしたいけれど、サーバー側の変更は まだそのままテストを続けたいという状況になったとします。クライアント側の変更のう ちサーバー側にはないもの (C8 と C9) を master ブランチで再現するには、git rebase の --onto オプションを使⽤します。 $ git rebase --onto master server client これは「client ブランチに移動して client ブランチと server ブランチの共通の先祖か らのパッチを取得し、master 上でそれを適⽤しろ」という意味になります。ちょっと複雑 ですが、その結果は図 3-32 に⽰すように⾮常にクールです。 これで、master ブランチを fast-forward することができるようになりました (図 3-33 を参照ください)。 79 第3章 Git のブランチ機能 Scott Chacon Pro Git 図 3.31: トピックブランチからさらにトピックブランチを作成した歴史 図 3.32: 別のトピックブランチから派生したトピックブランチのリベース $ git checkout master $ git merge client 図 3.33: master ブランチを fast-forward し、client ブランチの変更を含める さて、いよいよ server ブランチのほうも取り込む準備ができました。server ブランチ 80 Scott Chacon Pro Git 3.6節 リベース の内容を master ブランチにリベースする際には、事前にチェックアウトする必要はなく git rebase [basebranch] [topicbranch] を実⾏するだけでだいじょうぶです。このコマンド は、トピックブランチ (ここでは server) をチェックアウトしてその変更をベースブランチ (master) 上に再現します。 $ git rebase master server これは、server での作業を master の作業に続け、結果は図 3-34 のようになります。 図 3.34: server ブランチを master ブランチ上にリベースする これで、ベースブランチ (master) を fast-forward することができます。 $ git checkout master $ git merge server ここで client ブランチと server ブランチを削除します。すべての作業が取り込まれた ので、これらのブランチはもはや不要だからです。これらの処理を済ませた結果、最終的 な歴史は図 3-35 のようになりました。 $ git branch -d client $ git branch -d server 図 3.35: 最終的なコミット履歴 3.6.3 ほんとうは怖いリベース あぁ、このすばらしいリベース機能。しかし、残念ながら⽋点もあります。その⽋点は ほんの⼀⾏でまとめることができます。 公開リポジトリにプッシュしたコミットをリベースしてはいけない この指針に従っている限り、すべてはうまく進みます。もしこれを守らなければ、あな たは嫌われ者となり、友⼈や家族からも軽蔑されることになるでしょう。 81 第3章 Git のブランチ機能 Scott Chacon Pro Git リベースをすると、既存のコミットを破棄して新たなコミットを作成することになりま す。新たに作成したコミットは破棄したものと似てはいますが別物です。あなたがどこか にプッシュしたコミットを誰かが取得してその上で作業を始めたとしましょう。あなたが git rebase でそのコミットを書き換えて再度プッシュすると、相⼿は再びマージすること になります。そして相⼿側の作業を⾃分の環境にプルしようとするとおかしなことになっ てしまします。 いったん公開した作業をリベースするとどんな問題が発⽣するのか、例を⾒てみましょ う。中央サーバーからクローンした環境上で何らかの作業を進めたものとします。現在の コミット履歴は図 3-36 のようになっています。 図 3.36: リポジトリをクローンし、なんらかの作業をすませた状態 さて、誰か他の⼈が、マージを含む作業をしてそれを中央サーバーにプッシュしまし た。それを取得し、リモートブランチの内容を作業環境にマージすると、図 3-37 のよう な状態になります。 図 3.37: さらなるコミットを取得し、作業環境にマージした状態 次に、さきほどマージした作業をプッシュした⼈が、気が変わったらしく新たにリベー スし直したようです。なんと git push --force を使ってサーバー上の歴史を上書きしてし まいました。あなたはもう⼀度サーバーにアクセスし、新しいコミットを⼿元に取得しま す。 82 Scott Chacon Pro Git 3.6節 リベース 図 3.38: 誰かがリベースしたコミットをプッシュし、あなたの作業環境の元になってい るコミットが破棄された ここであなたは、新しく取得した内容をまたマージしなければなりません。すでにマー ジ済みのはずであるにもかかわらず。リベースを⾏うとコミットの SHA-1 ハッシュが変 わってしまうので、Git はそれを新しいコミットと判断します。実際のところ C4 の作業 は既に取り込み済みなのですが (図 3-39 を参照ください)。 図 3.39: 同じ作業を再びマージして新たなマージコミットを作成する 今後の他の開発者の作業を追いかけていくために、今回のコミットもマージする必要 があります。そうすると、あなたのコミット履歴には C4 と C4ʼ の両⽅のコミットが含 まれることになります。これらは SHA-1 ハッシュが異なるだけで、作業内容やコミット メッセージは同じものです。このような状態の歴史の上で git log を実⾏すると、同じ⼈ による同じ⽇付で同じメッセージのコミットがふたつ登場することになり、混乱します。 さらに、この歴史をサーバーにプッシュすると、リベースしたコミットを再び中央サー バーに戻すことになってしまい、混乱する⼈がさらに増えます。 リベースはあくまでもプッシュする前のコミットをきれいにするための⽅法であるとと らえ、リベースするのはまだ公開していないコミットのみに限定するようにしている限り はすべてがうまく進みます。もしいったんプッシュした後のコミットをリベースしてしま い、どこか他のところでそのコミットを元に作業を進めている⼈がいたとすると、やっか いなトラブルに巻き込まれることになるでしょう。 83 第3章 Git のブランチ機能 Scott Chacon Pro Git 3.7 まとめ 本章では、Git におけるブランチとマージの基本について取り上げました。新たなブラ ンチの作成、ブランチの切り替え、ローカルブランチのマージなどの作業が気軽にできる ようになったことでしょう。また、ブランチを共有サーバーにプッシュして公開したり他 の共有ブランチ上で作業をしたり、公開する前にブランチをリベースしたりする⽅法を⾝ につけました。 84 第4章 Git サーバー ここまで読んだみなさんは、ふだん Git を使う上で必要になるタスクのほとんどを⾝に つけたことでしょう。しかし、Git で何らかの共同作業をしようと思えばリモートの Git リポジトリを持つ必要があります。個⼈リポジトリとの間でのプッシュやプルも技術的に は可能ですが、お勧めしません。よっぽど気をつけておかないと、ほかの⼈がどんな作業 をしているのかをすぐに⾒失ってしまうからです。さらに、⾃分のコンピューターがオフ ラインのときにもほかの⼈が⾃分のリポジトリにアクセスできるようにしたいとなると、 共有リポジトリを持つほうがずっと便利です。というわけで、他のメンバーとの共同作業 をするときには、中間リポジトリをどこかに⽤意してみんながそこにアクセスできるよう にし、プッシュやプルを⾏うようにすることをお勧めします。本書ではこの⼿のリポジト リのことを 『Git サーバー』 と呼ぶことにします。しかし、⼀般的に Git リポジトリを ホストするのに必要なリソースはほんの少しだけです。それ専⽤のサーバーをわざわざ⽤ 意する必要はまずありません。 Git サーバーを⽴ち上げるのは簡単です。まず、サーバーとの通信にどのプロトコルを 使うのかを選択します。この章の最初のセクションで、どんなプロトコルが使えるのか とそれぞれのプロトコルの利点・⽋点を説明します。その次のセクションでは、それぞれ のプロトコルを使⽤したサーバーの設定⽅法とその動かし⽅を説明します。最後に、ホス ティングサービスについて紹介します。他⼈のサーバー上にコードを置くのが気にならな い、そしてサーバーの設定だの保守だのといった⾯倒なことはやりたくないという⼈のた めのものです。 ⾃前でサーバーを⽴てることには興味がないという⼈は、この章は最後のセクションま で読み⾶ばし、ホスティングサービスに関する情報だけを読めばよいでしょう。そして次 の章に進み、分散ソース管理環境での作業について学びます。 リモートリポジトリは、⼀般的に ベアリポジトリ となります。これは、作業ディレク トリをもたない Git リポジトリのことです。このリポジトリは共同作業の中継地点として のみ⽤いられるので、ディスク上にスナップショットをチェックアウトする必要はありま せん。単に Git のデータがあればそれでよいのです。端的に⾔うと、ベアリポジトリとは そのプロジェクトの .git ディレクトリだけで構成されるもののことです。 4.1 プロトコル Git では、データ転送⽤のネットワークプロトコルとして Local、Secure Shell (SSH)、 Git そして HTTP の四つを使⽤できます。ここでは、それぞれがどんなものなのかとどん 85 第4章 Git サーバー Scott Chacon Pro Git な場⾯で使うべきか (使うべきでないか) を説明します。 注意すべき点として、HTTP 以外のすべてのプロトコルは、サーバー上に Git がインス トールされている必要があります。 4.1.1 Local プロトコル ⼀番基本的なプロトコルが Local プロトコル です。これは、リモートリポジトリを ディスク上の別のディレクトリに置くものです。これがよく使われるのは、たとえばチー ム全員がアクセスできる共有ファイルシステム (NFS など) がある場合です。あるいは、 あまりないでしょうが全員が同じコンピューターにログインしている場合にも使えます。 後者のパターンはあまりお勧めできません。すべてのコードリポジトリが同じコンピュー ター上に存在することになるので、何か事故が起こったときに何もかも失ってしまう可能 性があります。 共有ファイルシステムをマウントしているのなら、それをローカルのファイルベースの リポジトリにクローンしたりお互いの間でプッシュやプルをしたりすることができます。 この⼿のリポジトリをクローンしたり既存のプロジェクトのリモートとして追加したりす るには、リポジトリへのパスを URL に指定します。たとえば、ローカルリポジトリにク ローンするにはこのようなコマンドを実⾏します。 $ git clone /opt/git/project.git あるいは次のようにすることもできます。 $ git clone file:///opt/git/project.git URL の先頭に file:// を明⽰するかどうかで、Git の動きは微妙に異なります。file:// を明⽰せずパスだけを指定し、かつコピー元とコピー先が同⼀のファイルシステム上にあ る場合は、Git は必要なオブジェクトにハードリンクを張ろうとします。もし異なるファ イルシステム上にある場合は、Git はシステムデフォルトのファイルコピー機能を使って 必要なオブジェクトをコピーします。⼀⽅ file:// を指定した場合は、Git がプロセスを⽴ ち上げ、そのプロセスが (通常は) ネットワーク越しにデータを転送します。⼀般的に、 直接のコピーに⽐べてこれは⾮常に⾮効率的です。file:// プレフィックスをつける最も ⼤きな理由は、(他のバージョン管理システムからインポートしたときなどにあらわれる) 関係のない参照やオブジェクトを除いたクリーンなコピーがほしいということです。本書 では通常のパス表記を使⽤します。そのほうがたいていの場合に⾼速となるからです。 ローカルのリポジトリを既存の Git プロジェクトに追加するには、このようなコマンド を実⾏します。 $ git remote add local_proj /opt/git/project.git そうすれば、このリモートとの間のプッシュやプルを、まるでネットワーク越しにある のと同じようにすることができます。 86 Scott Chacon Pro Git 4.1節 プロトコル 利点 ファイルベースのリポジトリの利点は、シンプルであることと既存のファイルアクセス 権やネットワークアクセスを流⽤できることです。チーム全員がアクセスできる共有ファ イルシステムがすでに存在するのなら、リポジトリを⽤意するのは⾮常に簡単です。ベア リポジトリのコピーをみんながアクセスできるどこかの場所に置き、読み書き可能な権限 を与えるという、ごく普通の共有ディレクトリ上での作業です。この作業のために必要 なベアリポジトリをエクスポートする⽅法については次のセクション「Git サーバーの取 得」で説明します。 もうひとつ、ほかの誰かの作業ディレクトリの内容をすばやく取り込めるのも便利なと ころです。同僚と作業しているプロジェクトで相⼿があなたに作業内容を確認してほしい ⾔ってきたときなど、わざわざリモートのサーバーにプッシュしてもらってそれをプルす るよりは単に git pull /home/john/project のようなコマンドを実⾏するほうがずっと簡単で す。 欠点 この⽅式の⽋点は、メンバーが別の場所にいるときに共有アクセスを設定するのは⼀般 的に難しいということです。⾃宅にいるときに⾃分のラップトップからプッシュしようと したら、リモートディスクをマウントする必要があります。これはネットワーク越しのア クセスに⽐べて困難で遅くなるでしょう。 また、何らかの共有マウントを使⽤している場合は、必ずしもこの⽅式が最⾼速となる わけではありません。ローカルリポジトリが⾼速だというのは、単にデータに⾼速にアク セスできるからというだけの理由です。NFS 上に置いたリポジトリは、同じサーバーで 稼動しているリポジトリに SSH でアクセスしたときよりも遅くなりがちです。SSH でア クセスしたときは、各システムのローカルディスクにアクセスすることになるからです。 4.1.2 SSH プロトコル Git の転送プロトコルのうちもっとも⼀般的なのが SSH でしょう。SSH によるサー バーへのアクセスは、ほとんどの場⾯で既に⽤意されているからです。仮にまだ⽤意され ていなかったとしても、導⼊するのは容易なことです。また SSH は、ネットワークベー スの Git 転送プロトコルの中で、容易に読み書き可能な唯⼀のものです。その他のネット ワークプロトコル (HTTP および Git) は⼀般的に読み込み専⽤で⽤いるものです。不特定 多数向けにこれらのプロトコルを開放したとしても、書き込みコマンドを実⾏するために は SSH が必要となります。SSH は認証付きのネットワークプロトコルでもあります。あ らゆるところで⽤いられているので、環境を準備するのも容易です。 Git リポジトリを SSH 越しにクローンするには、次のように ssh:// URL を指定しま す。 $ git clone ssh://user@server/project.git あるいは、SCPコマンドのような省略形を使うこともできます。 87 第4章 Git サーバー Scott Chacon Pro Git $ git clone user@server:project.git ユーザー名も省略することもできます。その場合、Git は現在ログインしているユー ザーでの接続を試みます。 利点 SSH を使う利点は多数あります。まず、ネットワーク越しでのリポジトリへの書き込 みアクセスで認証が必要となる場⾯では、基本的にこのプロトコルを使わなければなりま せん。次に、⼀般的に SSH 環境の準備は容易です。SSH デーモンはごくありふれたツー ルなので、ネットワーク管理者の多くはその使⽤経験があります。また、多くの OS に標 準で組み込まれており、管理⽤ツールが付属しているものもあります。さらに、SSH 越 しのアクセスは安全です。すべての転送データは暗号化され、信頼できるものとなりま す。最後に、Git プロトコルや Local プロトコルと同程度に効率的です。転送するデータ を可能な限りコンパクトにすることができます。 欠点 SSH の⽋点は、リポジトリへの匿名アクセスを許可できないということです。たとえ 読み込み専⽤であっても、リポジトリにアクセスするには SSH 越しでのマシンへのアク セス権限が必要となります。つまり、オープンソースのプロジェクトにとっては SSH は あまりうれしくありません。特定の企業内でのみ使⽤するのなら、SSH はおそらく唯⼀ の選択肢となるでしょう。あなたのプロジェクトに読み込み専⽤の匿名アクセスを許可し たい場合は、リポジトリへのプッシュ⽤に SSH を⽤意するのとは別にプル⽤の環境とし て別のプロトコルを提供する必要があります。 4.1.3 Git プロトコル 次は Git プロトコルです。これは Git に標準で付属する特別なデーモンです。専⽤の ポート (9418) をリスンし、SSH プロトコルと同様のサービスを提供しますが、認証は⾏ いません。Git プロトコルを提供するリポジトリを準備するには、git-daemon-export-ok と いうファイルを作らなければなりません (このファイルがなければデーモンはサービスを 提供しません)。ただ、このままでは⼀切セキュリティはありません。Git リポジトリを すべての⼈に開放し、クローンさせることができます。しかし、⼀般に、このプロトコル でプッシュさせることはありません。プッシュアクセスを認めることは可能です。しかし 認証がないということは、その URL を知ってさえいればインターネット上の誰もがプロ ジェクトにプッシュできるということになります。これはありえない話だと⾔っても差し ⽀えないでしょう。 利点 Git プロトコルは、もっとも⾼速な転送プロトコルです。公開プロジェクトで⼤量のト ラフィックをさばいている場合、あるいは巨⼤なプロジェクトで読み込みアクセス時の ユーザー認証が不要な場合は、Git デーモンを⽤いてリポジトリを公開するとよいでしょ う。このプロトコルは SSH プロトコルと同様のデータ転送メカニズムを使いますが、暗 号化と認証のオーバーヘッドがないのでより⾼速です。 88 Scott Chacon Pro Git 4.1節 プロトコル 欠点 Git プロトコルの弱点は、認証の仕組みがないことです。Git プロトコルだけでしかプ ロジェクトにアクセスできないという状況は、⼀般的に望ましくありません。SSH と組 み合わせ、プッシュ (書き込み) 権限を持つ⼀部の開発者には SSH を使わせてそれ以外の ⼈には git:// での読み込み専⽤アクセスを⽤意することになるでしょう。また、Git プロ トコルは準備するのがもっとも難しいプロトコルでもあります。まず、独⾃のデーモンを 起動しなければなりません (この章の『Gitosis』のところで詳しく説明します)。そのた めには xinetd やそれに類するものの設定も必要になりますが、これはそんなにお⼿軽にで きるものではありません。また、ファイアウォールでポート 9418 のアクセスを許可する 必要もあります。これは標準のポートではないので、企業のファイアウォールでは許可さ れなていないかもしれません。⼤企業のファイアウォールでは、こういったよくわからな いポートは普通ブロックされています。 4.1.4 HTTP/S プロトコル 最後は HTTP プロトコルです。HTTP あるいは HTTPS のうれしいところは、準備す るのが簡単だという点です。基本的に、必要な作業といえば Git リポジトリを HTTP の ドキュメントルート以下に置いて post-update フックを⽤意することだけです (Git のフッ クについては第7章 で詳しく説明します)。これで、ウェブサーバー上のその場所にアク セスできる⼈ならだれでもリポジトリをクローンできるようになります。リポジトリへの HTTP での読み込みアクセスを許可するには、こんなふうにします。 $ cd /var/www/htdocs/ $ git clone --bare /path/to/git_project gitproject.git $ cd gitproject.git $ mv hooks/post-update.sample hooks/post-update $ chmod a+x hooks/post-update これだけです。Git に標準でついてくる post-update フックは、適切なコマンド (git update-server-info) を実⾏して HTTP でのフェッチとクローンをうまく動くようにしま す。このコマンドが実⾏されるのは、このリポジトリに対して SSH 越しでのプッシュが あったときです。その他の⼈たちがクローンする際には次のようにします。 $ git clone http://example.com/gitproject.git 今回の例ではたまたま /var/www/htdocs (⼀般的な Apache の標準設定) を使⽤しました が、別にそれに限らず任意のウェブサーバーを使うことができます。単にベアリポジトリ をそのパスに置けばよいだけです。Git のデータは、普通の静的ファイルとして扱われま す (実際のところどのようになっているかの詳細は第9章 を参照ください)。 HTTP 越しの Git のプッシュを⾏うことも可能ですが、あまり使われていません。 ま た、 こ れ に は 複 雑 な WebDAV の 設 定 が 必 要 で す。 めっ た に 使 わ れ る こ と が な い ので、本書では取り扱いません。HTTP でのプッシュに興味があるかたのために、そ 89 第4章 Git サーバー Scott Chacon Pro Git れ⽤のリポジトリを準備する⽅法が http://www.kernel.org/pub/software/scm/git/docs/howto/ setup-git-server-over-http.txt で公開されています。HTTP 越しでの Git のプッシュに関し て、よいお知らせがひとつあります。どんな WebDAV サーバーでも使うことが可能で、 特に Git ならではの機能は必要ありません。つまり、もしプロバイダが WebDAV による ウェブサイトの更新に対応しているのなら、それを使⽤することができます。 利点 HTTP を使⽤する利点は、簡単にセットアップできるということです。便利なコマンド で、Git リポジトリへの読み取りアクセスを全世界に公開できます。ものの数分で⽤意で きることでしょう。また、HTTP はサーバー上のリソースを激しく使⽤することはありま せん。すべてのデータは HTTP サーバー上の静的なファイルとして扱われます。普通の Apache サーバーは毎秒数千ファイルぐらいは余裕でさばくので、⼩規模サーバーであっ たとしても (Git リポジトリへのへのアクセスで) サーバーが過負荷になることはないで しょう。 HTTPS で読み込み専⽤のリポジトリを公開することもできます。これで、転送される コンテンツを暗号化したりクライアント側で特定の署名つき SSL 証明書を使わせたりす ることができるようになります。そこまでやるぐらいなら SSH の公開鍵を使うほうが簡 単ではありますが、場合によっては署名⼊り SSL 証明書やその他の HTTP ベースの認証 ⽅式を使った HTTPS での読み込み専⽤アクセスを使うこともあるでしょう。 もうひとつの利点としてあげられるのは、HTTP が⾮常に⼀般的なプロトコルであると いうことです。たいていの企業のファイアウォールはこのポートを通すように設定されて います。 欠点 HTTP によるリポジトリの提供の問題点は、クライアント側から⾒て⾮効率的だとい うことです。リポジトリのフェッチやクローンには⾮常に時間がかかります。また、他の ネットワークプロトコルにくらべてネットワークのオーバーヘッドや転送量が⾮常に増加 します。必要なデータだけをやりとりするといった賢い機能はない (サーバー側で転送時 になんらかの作業をすることができない) ので、HTTP はよく ダム (dumb) プロトコルな どと呼ばれています。HTTP とその他のプロトコルの間の効率の違いに関する詳細な情報 は、第9章 を参照ください。 4.2 サーバー用の Git の取得 Git サーバーを⽴ち上げるには、既存のリポジトリをエクスポートして新たなベアリポ ジトリ (作業ディレクトリを持たないリポジトリ) を作らなければなりません。これは簡 単にできます。リポジトリをクローンして新たにベアリポジトリを作成するには、clone コマンドでオプション --bare を指定します。慣例により、ベアリポジトリのディレクトリ 名の最後は .git とすることになっています。 $ git clone --bare my_project my_project.git Cloning into bare repository 'my_project.git'... done. 90 Scott Chacon Pro Git 4.2節 サーバー⽤の Git の取得 そうすると、Git ディレクトリのデータを my_project.git ディレクトリにコピーできま す。 これは、おおざっぱに⾔うと次の操作と同じようなことです。 $ cp -Rf my_project/.git my_project.git 設定ファイルにはちょっとした違いもありますが、ほぼこんなものです。作業ディレク トリなしで Git リポジトリを受け取り、それ単体のディレクトリを作成しました。 4.2.1 ベアリポジトリのサーバー上への設置 ベアリポジトリを取得できたので、あとはそれをサーバー上においてプロトコルを準備 するだけです。ここでは、git.example.com というサーバーがあってそこに SSH でアクセ スできるものと仮定しましょう。Git リポジトリはサーバー上の /opt/git ディレクトリに 置く予定です。新しいリポジトリを作成するには、ベアリポジトリを次のようにコピーし ます。 $ scp -r my_project.git [email protected]:/opt/git この時点で、同じサーバーに SSH でアクセスできてかつ /opt/git ディレクトリへの読 み込みアクセス権限がある⼈なら、次のようにしてこのリポジトリをクローンできるよう になりました。 $ git clone [email protected]:/opt/git/my_project.git ユーザーが SSH でアクセスでき、かつ /opt/git/my_project.git ディレクトリへの書き込 みアクセス権限があれば、すでにプッシュもできる状態になっています。git init コマン ドで --shared オプションを指定すると、リポジトリに対するグループ書き込みパーミッ ションを⾃動的に追加することができます。 $ ssh [email protected] $ cd /opt/git/my_project.git $ git init --bare --shared 既存の Git リポジトリからベアリポジトリを作成し、メンバーが SSH でアクセスでき るサーバーにそれを配置するだけ。簡単ですね。これで、そのプロジェクトでの共同作業 ができるようになりました。 複数名が使⽤する Git サーバーをたったこれだけの作業で⽤意できるというのは特筆 すべきことです。サーバー SSH アクセス可能なアカウントを作成し、ベアリポジトリ 91 第4章 Git サーバー Scott Chacon Pro Git をサーバーのどこかに置き、そこに読み書き可能なアクセス権を設定する。これで準備 OK。他には何もいりません。 次のいくつかのセクションでは、より洗練された環境を作るための⽅法を説明します。 いちいちユーザーごとにアカウントを作らなくて済む⽅法、⼀般向けにリポジトリへの 読み込みアクセスを開放する⽅法、ウェブ UI の設定、Gitosis の使い⽅などです。しか し、数名のメンバーで閉じたプロジェクトでの作業なら、SSH サーバーとベアリポジト リ さえ あれば⼗分なことは覚えておきましょう。 4.2.2 ちょっとしたセットアップ ⼩規模なグループ、あるいは数名の開発者しかいない組織で Git を使うなら、すべては シンプルに進められます。Git サーバーを準備する上でもっとも複雑なことのひとつは、 ユーザー管理です。同⼀リポジトリに対して「このユーザーは読み込みのみが可能、あの ユーザーは読み書きともに可能」などと設定したければ、アクセス権とパーミッションの 設定は多少難しくなります。 SSH アクセス 開発者全員が SSH でアクセスできるサーバーがすでにあるのなら、リポジトリを⽤意 するのは簡単です。先ほど説明したように、ほとんど何もする必要はないでしょう。より 複雑なアクセス制御をリポジトリ上で⾏いたい場合は、そのサーバーの OS 上でファイル システムのパーミッションを設定するとよいでしょう。 リポジトリに対する書き込みアクセスをさせたいメンバーの中にサーバーのアカウント を持っていない⼈がいる場合は、新たに SSH アカウントを作成しなければなりません。 あなたがサーバーにアクセスできているということは、すでに SSH サーバーはインス トールされているということです。 その状態で、チームの全員にアクセス権限を与えるにはいくつかの⽅法があります。ひ とつは全員分のアカウントを作成すること。直感的ですがすこし⾯倒です。ひとりひとり に対して adduser を実⾏して初期パスワードを設定するという作業をしなければなりませ ん。 もうひとつの⽅法は、ʻgitʼ ユーザーをサーバー上に作成し、書き込みアクセスが 必要なユーザーには SSH 公開鍵を⽤意してもらってそれを ʻgitʼ ユーザーの ~/.ssh/ authorized_keys に追加します。これで、全員が ʻgitʼ ユーザーでそのマシンにアクセス できるようになりました。これがコミットデータに影響を及ぼすことはありません。SSH で接続したときのユーザーとコミットするときに記録されるユーザーとは別のものだから です。 あるいは、SSH サーバーの認証を LDAP サーバーやその他の中央管理形式の仕組みな ど既に⽤意されているものにするとこもできます。各ユーザーがサーバー上でシェルへの アクセスができさえすれば、どんな仕組みの SSH 認証であっても動作します。 4.3 SSH 公開鍵の作成 多くの Git サーバーでは、SSH の公開鍵認証を使⽤しています。この⽅式を使⽤する には、各ユーザーが⾃分の公開鍵を作成しなければなりません。公開鍵のつくりかたは、 OS が何であってもほぼ同じです。まず、⾃分がすでに公開鍵を持っていないかどうか確 認します。デフォルトでは、各ユーザーの SSH 鍵はそのユーザーの ~/.ssh ディレクトリ 92 Scott Chacon Pro Git 4.3節 SSH 公開鍵の作成 に置かれています。⾃分が鍵を持っているかどうかを確認するには、このディレクトリに ⾏ってその中⾝を調べます。 $ cd ~/.ssh $ ls authorized_keys2 id_dsa config known_hosts id_dsa.pub そして「○○」「○○.pub」というファイル名の組み合わせを探します。「○○」の部 分は、通常は id_dsa あるいは id_rsa となります。もし⾒つかったら、.pub がついている ほうのファイルがあなたの公開鍵で、もう⼀⽅があなたの秘密鍵です。そのようなファイ ルがない (あるいはそもそも .ssh ディレクトリがない) 場合は、ssh-keygen というプログラ ムを実⾏してそれを作成します。このプログラムは Linux/Mac なら SSH パッケージに 含まれており、Windows では MSysGit パッケージに含まれています。 $ ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/Users/schacon/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /Users/schacon/.ssh/id_rsa. Your public key has been saved in /Users/schacon/.ssh/id_rsa.pub. The key fingerprint is: 43:c5:5b:5f:b1:f1:50:43:ad:20:a6:92:6a:1f:9a:3a [email protected] まず、鍵の保存先 (.ssh/id_rsa) を指定し、それからパスフレーズを⼆回⼊⼒するよう求 められます。鍵を使うときにパスフレーズを⼊⼒したくない場合は、パスフレーズを空の ままにしておきます。 さて、次に各ユーザーは⾃分の公開鍵をあなた (あるいは Git サーバーの管理者である 誰か) に送らなければなりません (ここでは、すでに公開鍵認証を使⽤するように SSH サーバーが設定済みであると仮定します)。公開鍵を送るには、.pub ファイルの中⾝をコ ピーしてメールで送ります。公開鍵は、このようなファイルになります。 $ cat ~/.ssh/id_rsa.pub ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3 Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA t3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/En mZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx NrRFi9wrf+M7Q== [email protected] 93 第4章 Git サーバー Scott Chacon Pro Git 各 種 OS 上 で の SSH 鍵 の 作 り ⽅ に つ い て は、 GitHub の http://github.com/guides/ providing-your-ssh-key に詳しく説明されています。 4.4 サーバーのセットアップ それでは、サーバー側での SSH アクセスの設定について順を追って⾒ていきましょ う。この例では authorized_keys ⽅式でユーザーの認証を⾏います。また、Ubuntu のよ うな標準的な Linux ディストリビューションを動かしているものと仮定します。まずは ʻgitʼ ユーザーを作成し、そのユーザーの .ssh ディレクトリを作りましょう。 $ sudo adduser git $ su git $ cd $ mkdir .ssh 次に、開発者たちの SSH 公開鍵をそのユーザーの authorized_keys に追加していきま しょう。受け取った鍵が⼀時ファイルとして保存されているものとします。先ほどもごら んいただいたとおり、公開鍵の中⾝はこのような感じになっています。 $ cat /tmp/id_rsa.john.pub ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCB007n/ww+ouN4gSLKssMxXnBOvf9LGt4L ojG6rs6hPB09j9R/T17/x4lhJA0F3FR1rP6kYBRsWj2aThGw6HXLm9/5zytK6Ztg3RPKK+4k Yjh6541NYsnEAZuXz0jTTyAUfrtU3Z5E003C4oxOj6H0rfIF1kKI9MAQLMdpGW1GYEIgS9Ez Sdfd8AcCIicTDWbqLAcU4UpkaX8KyGlLwsNuuGztobF8m72ALC/nLF6JLtPofwFBlgc+myiv O7TCUSBdLQlgMVOFq1I2uPWQOkOWQAHukEOmfjy2jctxSDBQ220ymjaNsHT4kgtZg2AYYgPq dAv8JggJICUvax2T9va5 gsg-keypair これを authorized_keys に追加していきましょう。 $ cat /tmp/id_rsa.john.pub >> ~/.ssh/authorized_keys $ cat /tmp/id_rsa.josie.pub >> ~/.ssh/authorized_keys $ cat /tmp/id_rsa.jessica.pub >> ~/.ssh/authorized_keys さて、彼らが使うための空のリポジトリを作成しましょう。git init に --bare オプショ ンを指定して実⾏すると、作業ディレクトリのない空のリポジトリを初期化します。 $ cd /opt/git $ mkdir project.git $ cd project.git $ git --bare init 94 Scott Chacon Pro Git 4.4節 サーバーのセットアップ これで、John と Josie そして Jessica はプロジェクトの最初のバージョンをプッシュ できるようになりました。このリポジトリをリモートとして追加し、ブランチをプッシュ すればいいのです。何か新しいプロジェクトを追加しようと思ったら、そのたびに誰か がサーバーにログインし、ベアリポジトリを作らなければならないことに注意しましょ う。ʻgitʼ ユーザーとリポジトリを作ったサーバーのホスト名を gitserver としておきま しょう。gitserver がそのサーバーを指すように DNS を設定しておけば、このようなコマ ンドを使えます。 # John のコンピューターで $ cd myproject $ git init $ git add . $ git commit -m 'initial commit' $ git remote add origin git@gitserver:/opt/git/project.git $ git push origin master これで、他のメンバーがリポジトリをクローンして変更内容を書き戻せるようになりま した。 $ git clone git@gitserver:/opt/git/project.git $ cd project $ vim README $ git commit -am 'fix for the README file' $ git push origin master この⽅法を使えば、⼩規模なチーム⽤の読み書き可能な Git サーバーをすばやく⽴ち上 げることができます。 万⼀の場合に備えて ʻgitʼ ユーザーができることを制限するのも簡単で、Git に関す る作業しかできない制限付きシェル git-shell が Git に付属しています。これを ʻgitʼ ユーザーのログインシェルにしておけば、ʻgitʼ ユーザーはサーバーへの通常のシェル アクセスができなくなります。これを使⽤するには、ユーザーのログインシェルとして bash や csh ではなく git-shell を指定します。そのためには /etc/passwd ファイルを編集 しなければなりません。 $ sudo vim /etc/passwd いちばん最後に、このような⾏があるはずです。 git:x:1000:1000::/home/git:/bin/sh 95 第4章 Git サーバー Scott Chacon Pro Git ここで /bin/sh を /usr/bin/git-shell (which git-shell を実⾏してインストール先を探し、 それを指定します) に変更します。変更後はこのようになるでしょう。 git:x:1000:1000::/home/git:/usr/bin/git-shell これで、ʻgitʼ ユーザーは Git リポジトリへのプッシュやプル以外のシェル操作がで きなくなりました。それ以外の操作をしようとすると、このように拒否されます。 $ ssh git@gitserver fatal: What do you think I am? A shell? Connection to gitserver closed. 4.5 一般公開 匿名での読み込み専⽤アクセス機能を追加するにはどうしたらいいでしょうか? ⾝内に 閉じたプロジェクトではなくオープンソースのプロジェクトを扱うときに、これを考える ことになります。あるいは、⾃動ビルドや継続的インテグレーション⽤に⼤量のサーバー が⽤意されており、それがしばしば⼊れ替わるという場合など。単なる読み込み専⽤の匿 名アクセスのためだけに毎回 SSH 鍵を作成しなければならないのは⼤変です。 おそらく⼀番シンプルな⽅法は、静的なウェブサーバーを⽴ち上げてそのドキュメント ルートに Git リポジトリを置き、本章の最初のセクションで説明した post-update フック を有効にすることです。先ほどの例を続けてみましょう。リポジトリが /opt/git ディレク トリにあり、マシン上で Apache が稼働中であるものとします。念のために⾔いますが、 別に Apache に限らずどんなウェブサーバーでも使えます。しかしここでは、Apache の 設定を例にして何が必要なのかを説明していきます。 まずはフックを有効にします。 $ cd project.git $ mv hooks/post-update.sample hooks/post-update $ chmod a+x hooks/post-update この post-update は、いったい何をするのでしょうか? その中⾝はこのようになります。 $ cat .git/hooks/post-update #!/bin/sh # # An example hook script to prepare a packed repository for use over # dumb transports. # 96 Scott Chacon Pro Git 4.6節 GitWeb # To enable this hook, rename this file to "post-update". # exec git-update-server-info SSH 経由でサーバーへのプッシュが⾏われると、Git はこのコマンドを実⾏し、HTTP 経由での取得に必要なファイルを更新します。 次に、Apache の設定に VirtualHost エントリを追加して Git プロジェクトのディレク トリをドキュメントルートに設定します。ここでは、ワイルドカード DNS を設定して *.gitserver へのアクセスがすべてこのマシンに来るようになっているものとします。 <VirtualHost *:80> ServerName git.gitserver DocumentRoot /opt/git <Directory /opt/git/> Order allow, deny allow from all </Directory> </VirtualHost> また、/opt/git ディレクトリの所有グループを www-data にし、ウェブサーバーがこの リポジトリにアクセスできるようにしなければなりません。CGI スクリプトを実⾏する Apache インスタンスのユーザー権限で動作することになるからです。 $ chgrp -R www-data /opt/git Apache を再起動すれば、プロジェクトの URL を指定してリポジトリのクローンがで きるようになります。 $ git clone http://git.gitserver/project.git この⽅法を使えば、多数のユーザーに HTTP での読み込みアクセス権を与える設定が たった数分でできあがります。多数のユーザーに認証なしでのアクセスを許可するための もうひとつの⽅法として、Git デーモンを⽴ち上げることもできます。しかし、その場合 はプロセスをデーモン化させなければなりません。その⽅法については次のセクションで 説明します。 4.6 GitWeb これで、読み書き可能なアクセス⽅法と読み込み専⽤のアクセス⽅法を⽤意できるよ うになりました。次にほしくなるのは、ウェブベースでの閲覧⽅法でしょうか。Git に 97 第4章 Git サーバー Scott Chacon Pro Git は標準で GitWeb という CGI スクリプトが付属しており、これを使うことができます。 GitWeb の使⽤例は、たとえば http://git.kernel.org で確認できます (図 4-1 を参照くださ い)。 図 4.1: GitWeb のユーザーインターフェイス ⾃分のプロジェクトでためしに GitWeb を使ってみようという⼈のために、⼀時的な インスタンスを⽴ち上げるためのコマンドが Git に付属しています。これを実⾏するに は lighttpd や webrick といった軽量なサーバーが必要です。Linux マシンなら、たいてい lighttpd がインストールされています。これを実⾏するには、プロジェクトのディレクト リで git instaweb と打ち込みます。Mac の場合なら、Leopard には Ruby がプレインス トールされています。したがって webrick が⼀番よい選択肢でしょう。instaweb を lighttpd 以外で実⾏するには、--httpd オプションを指定します。 $ git instaweb --httpd=webrick [2009-02-21 10:02:21] INFO WEBrick 1.3.1 [2009-02-21 10:02:21] INFO ruby 1.8.6 (2008-03-03) [universal-darwin9.0] これは、HTTPD サーバーをポート 1234 で起動させ、⾃動的にウェブブラウザーを⽴ ち上げてそのページを表⽰させます。⾮常にお⼿軽です。ひととおり⾒終えてサーバーを 終了させたくなったら、同じコマンドに --stop オプションをつけて実⾏します。 $ git instaweb --httpd=webrick --stop ウェブインターフェイスをチーム内で常時⽴ち上げたりオープンソースプロジェクト⽤ に公開したりする場合は、CGI スクリプトを設定して通常のウェブサーバーに配置しな ければなりません。Linux のディストリビューションの中には、apt や yum などで gitweb パッケージが⽤意されているものもあります。まずはそれを探してみるとよいでしょう。 ⼿動での GitWeb のインストールについて、さっと流れを説明します。まずは Git のソー スコードを取得しましょう。その中に GitWeb が含まれており、CGI スクリプトを作る ことができます。 98 Scott Chacon Pro Git 4.7節 Gitosis $ git clone git://git.kernel.org/pub/scm/git/git.git $ cd git/ $ make GITWEB_PROJECTROOT="/opt/git" \ prefix=/usr gitweb $ sudo cp -Rf gitweb /var/www/ コマンドを実⾏する際に、Git リポジトリの場所 GITWEB_PROJECTROOT 変数で指定しなけれ ばならないことに注意しましょう。さて、次は Apache にこのスクリプトを処理させる ようにしなければなりません。VirtualHost に次のように追加しましょう。 <VirtualHost *:80> ServerName gitserver DocumentRoot /var/www/gitweb <Directory /var/www/gitweb> Options ExecCGI +FollowSymLinks +SymLinksIfOwnerMatch AllowOverride All order allow,deny Allow from all AddHandler cgi-script cgi DirectoryIndex gitweb.cgi </Directory> </VirtualHost> GitWeb は、CGI に対応したウェブサーバーならどんなものを使っても動かすことがで きます。何か別のサーバーのほうがよいというのなら、そのサーバーで動かすのもたやす いことでしょう。これで、http://gitserver/ にアクセスすればリポジトリをオンラインで ⾒られるようになりました。また http://git.gitserver で、HTTP 越しのクローンやフェッ チもできます。 4.7 Gitosis ユーザーの公開鍵を authorized_keys にまとめてアクセス管理する⽅法は、しばらくの間 はうまくいくでしょう。しかし、何百⼈ものユーザーを管理する段階になると、この⽅式 はとても⾯倒になります。サーバーのシェルでの操作が毎回発⽣するわけですし、またア クセス制御が皆無な状態、つまり公開鍵を登録した⼈はすべてのプロジェクトのすべての ファイルを読み書きできる状態になってしまいます。 ここで、よく使われている Gitosis というソフトウェアについて紹介しましょう。 Gitosis は、authorized_keys ファイルを管理したりちょっとしたアクセス制御を⾏ったり するためのスクリプト群です。ユーザーを追加したりアクセス権を定義したりするため の UI に、ウェブではなく独⾃の Git リポジトリを採⽤しているというのが興味深い点で す。プロジェクトに関する情報を準備してそれをプッシュすると、その情報に基づいて Gitosis がサーバーを設定するというクールな仕組みになっています。 99 第4章 Git サーバー Scott Chacon Pro Git Gitosis のインストールは簡単だとはいえませんが、それほど難しくもありません。 Linux サーバー上で運⽤するのがいちばん簡単でしょう。今回の例では、ごく平凡な Ubuntu 8.10 サーバーを使います。 Gitosis は Python のツールを使います。まずは Python の setuptools パッケージをイ ンストールしなければなりません。Ubuntu なら python-setuptools というパッケージが あります。 $ apt-get install python-setuptools 次に、プロジェクトのメインサイトから Gitosis をクローンしてインストールします。 $ git clone https://github.com/tv42/gitosis.git $ cd gitosis $ sudo python setup.py install これで、Gitosis が使う実⾏ファイル群がインストールされました。Gitosis は、リポジ トリが /home/git にあることが前提となっています。しかしここではすでに /opt/git にリ ポジトリが存在するので、いろいろ設定しなおすのではなくシンボリックリンクを作って しまいましょう。 $ ln -s /opt/git /home/git/repositories Gitosis は鍵の管理も⾏うので、まず現在の鍵ファイルを削除してあとでもう⼀度鍵 を追加し、Gitosis に authorized_keys を⾃動管理させなければなりません。ここではまず authorized_keys を別の場所に移動します。 $ mv /home/git/.ssh/authorized_keys /home/git/.ssh/ak.bak 次は ʻgitʼ ユーザーのシェルをもし git-shell コマンドに変更していたのなら、元に戻 さなければなりません。⼈にログインさせるのではなく、かわりに Gitosis に管理しても らうのです。/etc/passwd ファイルにある git:x:1000:1000::/home/git:/usr/bin/git-shell の⾏を、次のように戻しましょう。 100 Scott Chacon Pro Git 4.7節 Gitosis git:x:1000:1000::/home/git:/bin/sh いよいよ Gitosis の初期設定です。⾃分の公開鍵を使って gitosis-init コマンドを実⾏ します。サーバー上に⾃分の公開鍵をおいていない場合は、まず公開鍵をコピーしましょ う。 $ sudo -H -u git gitosis-init < /tmp/id_dsa.pub Initialized empty Git repository in /opt/git/gitosis-admin.git/ Reinitialized existing Git repository in /opt/git/gitosis-admin.git/ これで、指定した鍵を持つユーザーが Gitosis ⽤の Git リポジトリを変更できるように なりました。次に、新しいリポジトリの post-update スクリプトに実⾏ビットを設定しま す。 $ sudo chmod 755 /opt/git/gitosis-admin.git/hooks/post-update これで準備完了です。きちんと設定できていれば、Gitosis の初期設定時に登録した公 開鍵を使って SSH でサーバーにログインできるはずです。結果はこのようになります。 $ ssh git@gitserver PTY allocation request failed on channel 0 ERROR:gitosis.serve.main:Need SSH_ORIGINAL_COMMAND in environment. Connection to gitserver closed. これは「何も Git のコマンドを実⾏していないので、接続を拒否した」というメッセー ジです。では、実際に何か Git のコマンドを実⾏してみましょう。Gitosis 管理リポジト リをクローンします。 # on your local computer $ git clone git@gitserver:gitosis-admin.git gitosis-admin というディレクトリができました。次のような内容になっています。 $ cd gitosis-admin $ find . ./gitosis.conf 101 第4章 Git サーバー Scott Chacon Pro Git ./keydir ./keydir/scott.pub gitosis.conf が、ユーザーやリポジトリそしてパーミッションを指定するためのファイ ルです。keydir ディレクトリには、リポジトリへの何らかのアクセス権を持つ全ユーザー の公開鍵ファイルを格納します。ユーザーごとにひとつのファイルとなります。keydir ディレクトリ内のファイル名 (この例では scott.pub) は⼈によって異なるでしょう。こ れは、gitosis-init スクリプトでインポートした公開鍵の最後にある説明をもとにして Gitosis がつけた名前です。 gitosis.conf ファイルを⾒ると、今のところは先ほどクローンした gitosis-admin プロ ジェクトについての情報しか書かれていません。 $ cat gitosis.conf [gitosis] [group gitosis-admin] members = scott writable = gitosis-admin これは、ʻscottʼ ユーザー(Gitosis の初期化時に公開鍵を指定したユーザー)だけが gitosis-admin プロジェクトにアクセスできるという意味です。 では、新しいプロジェクトを追加してみましょう。mobile という新しいセクションを作 成し、モバイルチームのメンバーとモバイルチームがアクセスするプロジェクトを書き ⼊れます。今のところ存在するユーザーは ʻscottʼ だけなので、とりあえずは彼をメ ンバーとして追加します。そして、新しいプロジェクト iphone_project を作ることにしま しょう。 [group mobile] members = scott writable = iphone_project gitosis-admin プロジェクトに⼿を⼊れたら、それをコミットしてサーバーにプッシュし ないと変更が反映されません。 $ git commit -am 'add iphone_project and mobile group' [master 8962da8] add iphone_project and mobile group 1 file changed, 4 insertions(+) $ git push origin master Counting objects: 5, done. Compressing objects: 100% (3/3), done. 102 Scott Chacon Pro Git 4.7節 Gitosis Writing objects: 100% (3/3), 272 bytes | 0 bytes/s, done. Total 3 (delta 0), reused 0 (delta 0) To git@gitserver:gitosis-admin.git fb27aec..8962da8 master -> master 新しい iphone_project プロジェクトにプッシュするには、ローカル側のプロジェクト に、このサーバーをリモートとして追加します。サーバー側でわざわざベアリポジトリを 作る必要はありません。先ほどプッシュした地点で、Gitosis が⾃動的にベアリポジトリ の作成を済ませています。 $ git remote add origin git@gitserver:iphone_project.git $ git push origin master Initialized empty Git repository in /opt/git/iphone_project.git/ Counting objects: 3, done. Writing objects: 100% (3/3), 230 bytes | 0 bytes/s, done. Total 3 (delta 0), reused 0 (delta 0) To git@gitserver:iphone_project.git * [new branch] master -> master パスを指定する必要がないことに注⽬しましょう (実際、パスを指定しても動作しませ ん)。コロンの後にプロジェクト名を指定するだけで、Gitosis がプロジェクトを⾒つけて くれます。 このプロジェクトに新たなメンバーを迎え⼊れることになりました、公開鍵を追加し なければなりません。しかし、今までのようにサーバー上の ~/.ssh/authorized_keys に追記 する必要はありません。ユーザー単位の鍵ファイルを keydir ディレクトリ内に置くだけ です。鍵ファイルにつけた名前が、gitosis.conf でその⼈を指定するときの名前となりま す。では、John と Josie そして Jessica の公開鍵を追加しましょう。 $ cp /tmp/id_rsa.john.pub keydir/john.pub $ cp /tmp/id_rsa.josie.pub keydir/josie.pub $ cp /tmp/id_rsa.jessica.pub keydir/jessica.pub そして彼らを ʻmobileʼ チームに追加し、iphone_project を読み書きできるようにしま す。 [group mobile] members = scott john josie jessica writable = iphone_project この変更をコミットしてプッシュすると、この四⼈のユーザーがプロジェクトへの読み 書きをできるようになります。 103 第4章 Git サーバー Scott Chacon Pro Git Gitosis にはシンプルなアクセス制御機能もあります。John には読み込み専⽤のアクセ ス権を設定したいという場合は、このようにします。 [group mobile] members = scott josie jessica writable = iphone_project [group mobile_ro] members = john readonly = iphone_project John はプロジェクトをクローンして変更内容を受け取れます。しかし、⼿元での変更 をプッシュしようとすると Gitosis に拒否されます。このようにして好きなだけのグルー プを作成し、それぞれに個別のユーザーとプロジェクトを含めることができます。また、 グループのメンバーとして別のグループを指定し (その場合は先頭に @ をつけます)、メン バーを⾃動的に継承することもできます。 [group mobile_committers] members = scott josie jessica [group mobile] members = @mobile_committers writable = iphone_project [group mobile_2] members = @mobile_committers john writable = another_iphone_project 何か問題が発⽣した場合には、[gitosis] セクションの下に loglevel=DEBUG を書いておく と便利です。設定ミスでプッシュ権限を奪われてしまった場合は、サーバー上の /home/ git/.gitosis.conf を直接編集して元に戻します。Gitosis は、このファイルから情報を読み 取っています。gitosis.conf ファイルへの変更がプッシュされてきたときに、その内容を このファイルに書き出します。このファイルを⼿動で変更しても、次に gitosis-admin プロ ジェクトへのプッシュが成功した時点でその内容が書き換えられることになります。 4.8 Gitolite このセクションでは、Gitoliteの紹介およびインストールと初期設定の⽅法を解説しま す。しかし、完全な状態ではなく、Gitolite に付属する⼤量のドキュメントに取って代わ るものというわけでもありません。また、このセクションは時々更新される可能性がある ので、最新の情報も確認しておくとよいでしょう。 104 Scott Chacon Pro Git 4.8節 Gitolite GitoliteはGitに認可機能を与えるもので、認証にはsshdかhttpdを使⽤します(復習: 認証 とはユーザーが誰かを確認することで、認可とはユーザーがアクセスを許可されているか どうかを確認することです)。 Gitolite は、単なるリポジトリ単位の権限付与だけではなくリポジトリ内のブランチや タグ単位で権限を付与することができます。つまり、特定の⼈ (あるいはグループ) にだ け特定の 『refs』 (ブランチあるいはタグ) に対するプッシュ権限を与えて他の⼈には許 可しないといったことができるのです。 4.8.1 インストール Gitolite のインストールは⾮常に簡単で、豊富な付属ドキュメントを読まなくてもイン ストールできます。必要なものは、何らかの Unix 系サーバのアカウントです。root アク セス権は不要です。Git や Perl、そして OpenSSH 互換の SSH サーバは既にインストー ルされているものとします。以下の例では、gitserver というホストにあるアカウント git を使います。 Gitolite は、いわゆる 『サーバー』 ソフトウェアとしては少し変わっています。アク セスは SSH 経由で⾏うので、サーバー上のすべての userid が 『gitolite host』 となり 得ます。もっともシンプルなインストール⽅法をこれから解説していきますので、その他 の⽅法についてはドキュメントを確認してください。 ま ず は、 ユー ザーgitを イ ン ス トー ル 先 の サー バー 上 に 作 成 し、 そ の ユー ザー で ロ グ イ ン し ま す。 ssh の 公 開 鍵 (デ フォ ル ト 設 定 でssh- keygenを 実 ⾏ し て い る 場 合 は、~/.ssh/ id_rsa.pubがそれに当たります)をあなたのワークステーションからコピーし、ファイル 名を<yourname>.pub (以下の例ではscott.pubを使⽤します)に変更しておきます。次に、以下の コマンドを実⾏します: git clone git://github.com/sitaramc/gitolite gitolite/install -ln # $HOME/bin が存在し、かつPATHが通っているものとします gitolite setup -pk $HOME/scott.pub gitolite-adminという名前のGitリポジトリが、最後のコマンドにより作成されます。 最後に、あなたのワークステーションに戻って、git clone git@gitserver:gitolite-adminを実 ⾏します。これで完了です! Gitolite はサーバにインストールされ、gitolite-admin という 新しいリポジトリがあなたのワークステーションにできあがりました。Gitolite の設定を 管理するには、このリポジトリに変更を加えてプッシュします。 4.8.2 インストールのカスタマイズ デフォルトのインストールは素早く済ませられ、たいていの⼈にとってはこれで⼗分で しょう。しかし、必要に応じてインストール⽅法をカスタマイズすることもできます。rc ファイルを変更してカスタマイズすることも可能ですし、もしそれ以上を望むのであれば、 gitoliteのカスタマイズについてのドキュメントを確認してください。 105 第4章 Git サーバー Scott Chacon Pro Git 4.8.3 設定ファイルおよびアクセス制御ルール イ ン ス トー ル が 終 わっ た ら、 あ な た の ワー ク ス テー ショ ン に ク ロー ン し た ば か り のgitolite-admin リポジトリに移動して、中をのぞいてみましょう。 $ cd ~/gitolite-admin/ $ ls conf/ keydir/ $ find conf keydir -type f conf/gitolite.conf keydir/scott.pub $ cat conf/gitolite.conf repo gitolite-admin RW+ = scott repo testing RW+ = @all 『scott』 (先ほどの gitolite setup コマンドで、指定した公開鍵の名前です) が、gitoliteadmin リポジトリおよび同名の公開鍵ファイルへの読み書き権限を持っていることに注⽬ しましょう。 ユーザーを追加するのは簡単です。『alice』というユーザーを追加するなら、彼⼥の公 開鍵を取得し、alice.pubに名前を変更、そしてそれをあなたのワークステーションにクロー ンされたgitolite-adminレポジトリ内のkeydirディレクトリにコピーします。変更を追加し、 コミットし、プッシュすれば、ユーザーの追加は完了です。 Gitolite の設定ファイルの構⽂はドキュメントに詳しく書かれているので、ここでは⼤ 事なところに絞って説明します。 ユーザやリポジトリをグループにまとめることもできます。グループ名は単なるマクロ のようなものです。グループを定義する際には、それがユーザであるかプロジェクトであ るかは無関係です。実際にその「マクロ」を 使う 段階になって初めてそれらを区別する ことになります。 @oss_repos = linux perl rakudo git gitolite @secret_repos = fenestra pear @admins = scott @interns = ashok @engineers = sitaram dilbert wally alice @staff = @admins @engineers @interns パーミッションは、『ref』 レベルで設定することができます。次の例では、インター ン (@interns) は 『int』 ブランチにしかプッシュできないように設定しています。エン 106 Scott Chacon Pro Git 4.8節 Gitolite ジニア (@engineers) は名前が 『eng-』 で始まるすべてのブランチにプッシュでき、ま た 『rc』 のあとに⼀桁の数字が続く名前のタグにもプッシュできます。また、管理者 (@admins) はすべての ref に対してあらゆることができます。 repo @oss_repos RW int$ = @interns RW eng- = @engineers RW refs/tags/rc[0-9] = @engineers RW+ = @admins RW や RW+ の後に書かれている式は正規表現 (regex) で、これにマッチする refname (ref) が対象となります。なので、これに 『refex』 と名付けました! もちろん、refex はこの 例で⽰したよりずっと強⼒なものです。Perl の正規表現になじめない⼈は、あまりやり すぎないようにしましょう。 また、すでにお気づきかもしれませんが、refs/ で始まらない refex を指定すると、 Gitolite はその先頭に refs/heads/ がついているものとみなします。これは構⽂上の利便性 を意識したものです。 設定ファイルの構⽂の中でも重要なのは、ひとつのリポジトリに対するルールをすべて ひとまとめにしなくてもよいということです。共通の設定をひとまとめにして上の例のよ うに oss_repos に対してルールを設定し、その後で個々の場合について個別のルールを追 加していくこともできます。 repo gitolite RW+ = sitaram このルールは、gitolite リポジトリのルール群に追加されます。 で、実際のアクセス制御ルールはどのように書けばいいの? と思われたことでしょう。 簡単に説明します。 Gitolite のアクセス制御には⼆段階のレベルがあります。まず最初はリポジトリレベル のアクセス制御です。あるリポジトリへの読み込み (書き込み) アクセス権を持っている ということは、そのリポジトリの すべての ref に対する読み込み (書き込み) 権限を持っ ていることを意味します。Gitosisにはこのレベルのアクセス制御しかありませんでした。 もうひとつのレベルは 『書き込み』 アクセス権だけを制御するものですが、リポジト リ内のブランチやタグ単位で設定できます。ユーザ名、試みられるアクセス (W あるいは +)、そして更新される refname が既知となります。アクセスルールのチェックは、設定 ファイルに書かれている順に⾏われ、この組み合わせにマッチ (単なる⽂字列マッチでは なく正規表現によるマッチであることに注意しましょう) するものを探していきます。 マッチするものが⾒つかったら、プッシュが成功します。マッチしなかった場合は、アク セスが拒否されます。 4.8.4 『拒否』 ルールによる高度なアクセス制御 これまでに⾒てきた権限は R、RW あるいは RW+ だけでした。しかし、Gitolite にはそれ 以外の権限もあります。それが - で、『禁⽌』 をあらわすものです。これを使えばより 107 第4章 Git サーバー Scott Chacon Pro Git 強⼒なアクセス制御ができるようになりますが、少し設定は複雑になります。マッチしな ければアクセスを拒否するというだけでなく、ルールを書く順番もからんでくることにな るからです。 上の例で、エンジニアは master と integ 以外 のすべてのブランチを巻き戻せるように 設定しようとすると、次のようになります。 RW master integ = @engineers - = @engineers master integ RW+ = @engineers この場合も上から順にルールを適⽤し、最初にマッチしたアクセス権をあてはめます。 何もマッチしなければアクセスは拒否されます。master や integ に対する巻き戻し以外 のプッシュは、最初のルールにマッチするので許可されます。これらのブランチに対する 巻き戻しのプッシュは最初のルールにマッチしません。そこで⼆番⽬のルールに移動し、 この時点で拒否されます。master と integ 以外への (巻き戻しを含む) 任意のプッシュは 最初の⼆つのルールのいずれにもマッチしないので、三番⽬のルールが適⽤されます。 4.8.5 ファイル単位でのプッシュの制限 変更をプッシュすることのできるブランチを制限するだけでなく、変更できるファイル を制限することも可能です。たとえば、Makefile (あるいはその他のプログラム) などは 誰もが変更できるというものではないでしょう。このファイルはさまざまなものに依存し ており、変更によっては壊れてしまうことがあるかもしれないからです。そんな場合は次 のように設定します。 repo foo RW - VREF/NAME/Makefile = @junior_devs @senior_devs = @junior_devs この機能には⼤幅な変更が加えられているので、Gitoliteの旧バージョンから移⾏してき たユーザーは気をつけてください。移⾏の⼿引きを確認してください。 4.8.6 個人ブランチ Gitolite には 『個⼈ブランチ』 (『個⼈的なブランチ⽤の名前空間』 と⾔ったほうがい いでしょうか) という機能があります。これは、法⼈の環境では⾮常に便利なものです。 Git の世界では、いわゆる「プルリクエスト」によるコードのやりとりが頻繁に発⽣し ます。しかし法⼈の環境では、権限のない⼈によるアクセスは厳禁です。開発者のワーク ステーションにはそんな権限はありません。そこで、まず⼀度中央サーバにプッシュし て、そこからプルしてもらうよう誰かにお願いすることになります。 これを中央管理型の VCS でやろうとすると、同じ名前のブランチが乱造されることに なってしまいます。また、これらのアクセス権限を設定するのは管理者にとって⾯倒な作 業です。 108 Scott Chacon Pro Git 4.9節 Git デーモン Gitolite では、開発者ごとに 『personal』 あるいは 『scratch』 といった名前空間プ レフィックス (たとえば refs/personal/<devname>/*) を定義できます。詳細は、ドキュメント を確認してください。 4.8.7 『ワイルドカード』 リポジトリ Gitolite では、ワイルドカード (実際のところは Perl の正規表現です) を使ってリポジ トリを指定することができます。たとえば assignments/s[0-9][0-9]/a[0-9][0-9] のようにし ます。この機能を使うと、新たな権限モード (C) が⽤意されます。これは、ワイルドカー ドにマッチするリポジトリの作成を許可するモードです。新たに作成したリポジトリの所 有者は⾃動的にそのユーザに設定され、他のユーザに R あるいは RW の権限を付与できる ようになります。この機能についても、詳細はドキュメントを確認してください。 4.8.8 その他の機能 最後にその他の機能の例を紹介しましょう。これらについての詳しい説明は、ドキュメ ントにあります。 ログ記録: Gitolite は、成功したアクセスをすべてログに記録します。巻き戻し権限 (RW +) を与えているときに、誰かが master を吹っ⾶ばしてしまったとしましょう。そんなと きにはログファイルが救世主となります。ログを⾒れば、問題を起こした SHA をすぐに 発⾒できるでしょう。 アクセス権の報告: もうひとつの便利な機能は、サーバに ssh で接続したときに起こり ます。gitolite はあなたがアクセスするリポジトリとどのようなアクセスができるかを表 ⽰します。たとえばこんな感じです。 hello scott, this is git@git running gitolite3 v3.01-18-g9609868 on git 1.7.4.4 R anu-wsd R entrans R W git-notes R W gitolite R W gitolite-admin R indic_web_input R shreelipi_converter 委譲: ⼤規模な環境では、特定のリポジトリのグループに対する責任を委譲して個別に 管理させることもできます。こうすれば主管理者の負荷が軽減され、主管理者がボトル ネックとなることも少なくなります。 ミラーリング: Gitolite は、複数のミラーを保守したり、プライマリサーバーが落ちた ときに簡単にミラーに切り替えたりすることができます。 4.9 Git デーモン 認証の不要な読み取り専⽤アクセスを⼀般に公開する場合は、HTTP を捨てて Git プロ トコルを使うことを考えることになるでしょう。主な理由は速度です。Git プロトコルの 109 第4章 Git サーバー Scott Chacon Pro Git ほうが HTTP に⽐べてずっと効率的で⾼速です。Git プロトコルを使えば、ユーザーの時 間を節約することになります。 Git プロトコルは、認証なしで読み取り専⽤アクセスを⾏うためのものです。ファイア ウォールの外にサーバーがあるのなら、⼀般に公開しているプロジェクトにのみ使うよう にしましょう。ファイアウォール内で使うのなら、たとえば⼤量のメンバーやコンピュー ター (継続的インテグレーションのビルドサーバーなど) に対して SSH の鍵なしで読み取 り専⽤アクセスを許可するという使い⽅もあるでしょう。 いずれにせよ、Git プロトコルは⽐較的容易にセットアップすることができます。デー モン化するためには、このようなコマンドを実⾏します。 git daemon --reuseaddr --base-path=/opt/git/ /opt/git/ --reuseaddr は、前の接続がタイムアウトするのを待たずにサーバーを再起動させるオプ ションです。--base-path オプションを指定すると、フルパスをしていしなくてもプロジェ クトをクローンできるようになります。そして最後に指定したパスは、Git デーモンに公 開させるリポジトリの場所です。ファイアウォールを使っているのなら、ポート 9418 に ⽳を開けなければなりません。 プロセスをデーモンにする⽅法は、OS によってさまざまです。Ubuntu の場合は Upstart スクリプトを使います。 /etc/event.d/local-git-daemon のようなファイルを⽤意して、このようなスクリプトを書きます。 start on startup stop on shutdown exec /usr/bin/git daemon \ --user=git --group=git \ --reuseaddr \ --base-path=/opt/git/ \ /opt/git/ respawn セキュリティを考慮して、リポジトリに対する読み込み権限しかないユーザーでこ のデーモンを実⾏させるようにしましょう。新しいユーザー ʻgit-roʼ を作り、この ユーザーでデーモンを実⾏させるとよいでしょう。ここでは、説明を簡単にするために Gitosis と同じユーザー ʻgitʼ で実⾏させることにします。 マシンを再起動すれば Git デーモンが⾃動的に⽴ち上がり、終了させても再び起動する ようになります。再起動せずに実⾏させるには、次のコマンドを実⾏します。 110 Scott Chacon Pro Git 4.9節 Git デーモン initctl start local-git-daemon その他のシステムでは、xinetd や sysvinit システムのスクリプトなど、コマンドをデー モン化して監視できる仕組みを使います。 次に、どのプロジェクトに対して Git プロトコルでの認証なしアクセスを許可するのか を Gitosis に指定します。各リポジトリ⽤のセクションを追加すれば、Git デーモンから の読み込みアクセスを許可するように指定することができます。Git プロトコルでのアク セスを iphone_projectに許可したい場合は、gitosis.conf の最後に次のように追加します。 [repo iphone_project] daemon = yes この変更をコミットしてプッシュすると、デーモンがこのプロジェクトへのアクセスを 受け付けるようになります。 Gitosis を使わずに Git デーモンを設定したい場合は、Git デーモンで公開したいプロ ジェクトに対してこのコマンドを実⾏しなければなりません。 $ cd /path/to/project.git $ touch git-daemon-export-ok このファイルが存在するプロジェクトについては、Git は認証なしで公開してもよいも のとみなします。 Gitosis を使うと、どのプロジェクトを GitWeb で⾒せるのかを指定することもできま す。まずは次のような⾏を /etc/gitweb.conf に追加しましょう。 $projects_list = "/home/git/gitosis/projects.list"; $projectroot = "/home/git/repositories"; $export_ok = "git-daemon-export-ok"; @git_base_url_list = ('git://gitserver'); GitWeb でどのプロジェクトを⾒せるのかを設定するには、Gitosis の設定ファイルで gitweb を指定します。たとえば、iphone_projectを GitWeb で⾒せたい場合は、repo の設定 は次のようになります。 [repo iphone_project] daemon = yes gitweb = yes 111 第4章 Git サーバー Scott Chacon Pro Git これをコミットしてプッシュすると、GitWeb で iphone_projectが⾃動的に表⽰されるよ うになります。 4.10 Git のホスティング Git サーバーを⽴ち上げる作業が⾯倒なら、外部の専⽤ホスティングサイトに Git プロ ジェクトを置くという選択肢があります。この⽅法には多くの利点があります。ホスティ ングサイトでプロジェクトを⽴ち上げるのは簡単ですし、サーバーのメンテナンスや⽇々 の監視も不要です。⾃前のサーバーを持っているとしても、オープンソースのコードなど はホスティングサイトで公開したいこともあるかもしれません。そのほうがオープンソー スコミュニティのメンバーに⾒つけてもらいやすく、そして⽀援を受けやすくなります。 今ではホスティングの選択肢が数多くあり、それぞれ利点もあれば⽋点もあります。最 新の情報を知るには、次のページを調べましょう。 https://git.wiki.kernel.org/index.php/GitHosting これらすべてについて網羅することは不可能ですし、たまたま私⾃⾝がこの中のひとつ で働いていることもあるので、ここでは GitHub を使ってアカウントの作成からプロジェ クトの⽴ち上げまでの⼿順を説明します。どのような流れになるのかを⾒ていきましょ う。 GitHub は最⼤のオープンソース Git ホスティングサイトで、公開リポジトリだけでな く⾮公開のリポジトリもホスティングできる数少ないサイトのひとつです。つまり、オー プンソースのコードと⾮公開のコードを同じ場所で管理できるのです。実際、本書に関す る⾮公開の共同作業についても GitHub を使っています。 4.10.1 GitHub GitHub がその他多くのコードホスティングサイトと異なるのは、プロジェクトの位置 づけです。プロジェクトを主体に考えるのではなく、GitHub ではユーザー主体の構成に なっています。私が GitHub に grit プロジェクトを公開したとして、それは github.com/ grit ではなく github.com/schacon/grit となります。どのプロジェクトにも「正式な」バー ジョンというものはありません。たとえ最初の作者がプロジェクトを放棄したとしても、 それをユーザーからユーザーへと⾃由に移動することができます。 GitHub は営利企業なので、⾮公開のリポジトリについては料⾦をとって管理していま す。しかし、フリーのアカウントを取得すればオープンソースのプロジェクトを好きなだ け公開することができます。その⽅法についてこれから説明します。 4.10.2 ユーザーアカウントの作成 まずはフリー版のユーザーアカウントを作成しましょう。Pricing and Signup のペー ジ http://github.com/plans で、フリーアカウントの 『Sign Up』 ボタンを押すと (図 4-2 を参照ください)、新規登録ページに移動します。 ユーザー名を選び、メールアドレスを⼊⼒します。アカウントとパスワードがこのメー ルアドレスに関連づけられます (図 4-3 を参照ください)。 112 Scott Chacon Pro Git 4.10節 Git のホスティング 図 4.2: GitHub のプラン説明ページ 図 4.3: GitHub のユーザー登録フォーム それが終われば、次に SSH の公開鍵を追加しましょう。新しい鍵を作成する⽅法に ついては、さきほど「ちょっとしたセットアップ」のところで説明しました。公開鍵の 内容をコピーし、SSH Public Key のテキストボックスに貼り付けます。『explain ssh keys』 のリンクをクリックすると、主要 OS 上での公開鍵の作成⼿順を詳しく説明して くれます。『I agree, sign me up』 ボタンをクリックすると、あなたのダッシュボード に移動します (図 4-4 を参照ください)。 図 4.4: GitHub のユーザーダッシュボード では次に、新しいリポジトリの作成に進みましょう。 4.10.3 新しいリポジトリの作成 ダッシュボードで、Your Repositories の横にあるリンク 『create a new one』 をク リックしましょう。新規リポジトリの作成フォームに進みます (図 4-5 を参照ください)。 ここで必要なのはプロジェクト名を決めることだけです。ただ、それ以外に説明⽂を追 加することもできます。ここで 『Create Repository』 ボタンを押せば、GitHub 上での 113 第4章 Git サーバー Scott Chacon Pro Git 図 4.5: GitHub での新しいリポジトリの作成 新しいリポジトリのできあがりです (図 4-6 を参照ください)。 図 4.6: GitHub でのプロジェクトのヘッダ情報 まだ何もコードが追加されていないので、ここでは「新しいプロジェクトを作る⽅法」 「既存の Git プロジェクトをプッシュする⽅法」「Subversion の公開リポジトリからイ ンポートする⽅法」が説明されています (図 4-7 を参照ください)。 図 4.7: 新しいリポジトリに関する説明 この説明は、本書でこれまでに説明してきたものとほぼ同じです。まだ Git プロジェク トでないプロジェクトを初期化するには、次のようにします。 $ git init $ git add . $ git commit -m 'initial commit' ローカルにある Git リポジトリを使⽤する場合は、GitHub をリモートに登録して 114 Scott Chacon Pro Git 4.10節 Git のホスティング master ブランチをプッシュします。 $ git remote add origin [email protected]:testinguser/iphone_project.git $ git push origin master これで GitHub 上でリポジトリが公開され、だれもがプロジェクトにアクセスでき るような URL ができあがりました。この例の場合は http://github.com/testinguser/iphone_ project です。各プロジェクトのページのヘッダには、ふたつの Git URL が表⽰されてい ます (図 4-8 を参照ください)。 図 4.8: 公開 URL とプライベート URL が表示されたヘッダ Public Clone URL は、読み込み専⽤の公開 URL で、これを使えば誰でもプロジェク トをクローンできます。この URL は、あなたのウェブサイトをはじめとしたお好みの場 所で紹介することができます。 Your Clone URL は、読み書き可能な SSH の URL で、先ほどアップロードした公開鍵 に対応する SSH 秘密鍵を使った場合にのみアクセスできます。他のユーザーがこのプロ ジェクトのページを⾒てもこの URL は表⽰されず、公開 URL のみが⾒えるようになっ ています。 4.10.4 Subversion からのインポート GitHub では、Subversion で公開しているプロジェクトを Git にインポートすること もできます。先ほどの説明ページの最後のリンクをクリックすると、Subversion からの インポート⽤ページに進みます。このページにはインポート処理についての情報が表⽰さ れており、公開 Subversion リポジトリの URL を⼊⼒するテキストボックスが⽤意され ています。 図 4.9: Subversion からのインポート もしそのプロジェクトが⾮常に⼤規模なものであったり標準とは異なるものであった り、あるいは公開されていないものであったりした場合は、この⼿順ではうまくいかな 115 第4章 Git サーバー Scott Chacon Pro Git いでしょう。第7章 で、⼿動でのプロジェクトのインポート⼿順について詳しく説明しま す。 4.10.5 共同作業者の追加 では、チームの他のメンバーを追加しましょう。John、Josie そして Jessica は全員す でに GitHub のアカウントを持っており、彼らもこのリポジトリにプッシュできるように したければ、プロジェクトの共同作業者として登録します。そうすれば、彼らの公開鍵を つかったプッシュも可能となります。 プロジェクトのヘッダにある 『edit』 ボタンをクリックするかプロジェクトの上の Admin タブを選択すると、GitHub プロジェクトの管理者⽤ページに移動します (図 4-10 を参照ください)。 図 4.10: GitHub の管理者用ページ 別のユーザーにプロジェクトへの書き込み権限を付与するには、『Add another collaborator』リンクをクリックします。新しいテキストボックスがあらわれるので、そこ にユーザー名を記⼊します。何か⼊⼒すると、マッチするユーザー名の候補がポップアッ プ表⽰されます。ユーザーが⾒つかれば、Add ボタンをクリックすればそのユーザーを 共同作業者に追加できます (図 4-11 を参照ください)。 図 4.11: プロジェクトへの共同作業者の追加 対象者を全員追加し終えたら、Repository Collaborators のところにその⼀覧が⾒える はずです (図 4-12 を参照ください)。 誰かのアクセス権を剥奪したい場合は、『revoke』 リンクをクリックすればそのユー ザーはプッシュできなくなります。また、今後新たにプロジェクトを作ったときに、この 共同作業者⼀覧をコピーして使うこともできます。 4.10.6 あなたのプロジェクト プロジェクトをプッシュするか、あるいは Subversion からのインポートを済ませる と、プロジェクトのメインページは図 4-13 のようになります。 116 Scott Chacon Pro Git 4.10節 Git のホスティング 図 4.12: プロジェクトの共同作業者一覧 図 4.13: GitHub プロジェクトのメインページ 他の⼈がこのプロジェクトにアクセスしたときに⾒えるのがこのページとなります。 このページには、さまざまな情報を⾒るためのタブが⽤意されています。Commits タブ に表⽰されるのはコミットの⼀覧で、git log コマンドの出⼒と同様にコミット時刻が新 しい順に表⽰されます。Network タブには、このプロジェクトをフォークして何か貢献 してくれた⼈の⼀覧が表⽰されます。Downloads タブには、プロジェクト内でタグが打 たれている任意の点について tar や zip でまとめたものをアップロードすることができま す。Wiki タブには、プロジェクトに関するドキュメントやその他の情報を書き込むため の wiki が⽤意されています。Graphs タブは、プロジェクトに対する貢献やその他の統 計情報を視覚化して表⽰します。そして、Source タブにはプロジェクトのメインディレ クトリの⼀覧が表⽰され、もし README ファイルがあればその内容が下に表⽰されま す。このタブでは、最新のコミットについての情報も表⽰されます。 4.10.7 プロジェクトのフォーク プッシュアクセス権のない別のプロジェクトに協⼒したくなったときは、GitHub では プロジェクトをフォークすることを推奨しています。興味を持ったとあるプロジェクトの ページに⾏って、それをちょっとばかりハックしたくなったときは、プロジェクトのヘッ ダにある 『fork』 ボタンをクリックしましょう。GitHub が⾃分のところにそのプロジェ クトをコピーしてくれるので、そこへのプッシュができるようになります。 この⽅式なら、プッシュアクセス権を与えるために共同作業者としてユーザーを追加す ることを気にせずにすみます。プロジェクトをフォークした各ユーザーが⾃分のところに プッシュし、主メンテナーは必要に応じてかれらの作業をマージすればいいのです。 プロジェクトをフォークするには、そのプロジェクトのページ (この場合は mojombo/ 117 第4章 Git サーバー Scott Chacon Pro Git chronic) に移動してヘッダの 『fork』 ボタンをクリックします (図 4-14 を参照くださ い)。 図 4.14: 任意のプロジェクトの書き込み可能なコピーを取得する 『fork』 ボタン 数秒後に新しいプロジェクトのページに移動します。そこには、このプロジェクトがど のプロジェクトのフォークであるかが表⽰されています (図 4-15 を参照ください)。 図 4.15: フォークしたプロジェクト 4.10.8 GitHub のまとめ これで GitHub についての説明を終えますが、特筆すべき点はこれらの作業を本当に⼿ 早く済ませられることです。アカウントを作ってプロジェクトを追加してそこにプッシュ する、ここまでがほんの数分でできてしまいます。オープンソースのプロジェクトを公開 したのなら、数多くの開発者のコミュニティがあなたのプロジェクトにアクセスできるよ うになりました。きっと中にはあなたに協⼒してくれる⼈もあらわれることでしょう。少 なくとも、Git を動かして試してみる⼟台としては使えるはずです。 4.11 まとめ リモート Git リポジトリを⽤意するためのいくつかの⽅法を紹介し、他のメンバーとの 共同作業ができるようになりました。 ⾃前でサーバーを構築すれば、多くのことを制御できるようになり、ファイアウォール の内側でもサーバーを実⾏することができます。しかし、サーバーを構築して運⽤するに はそれなりの⼿間がかかります。ホスティングサービスを使えば、サーバーの準備や保守 は簡単になります。しかし、他⼈のサーバー上に⾃分のコードを置き続けなければなりま せん。組織によってはそんなことを許可していないかもしれません。 どの⽅法 (あるいは複数の⽅法の組み合わせ) を使えばいいのか、⾃分や所属先の事情 に合わせて考えましょう。 118 第5章 Git での分散作業 リモート Git リポジトリを⽤意し、すべての開発者がコードを共有できるようになりま した。また、ローカル環境で作業をする際に使う基本的な Git コマンドについても⾝につ いたことでしょう。次に、Git を使った分散作業の流れを⾒ていきましょう。 本章では、Git を使った分散環境での作業の流れを説明します。⾃分のコードをプロ ジェクトに提供する⽅法、そしてプロジェクトのメンテナーと⾃分の両⽅が作業を進めや すくする⽅法、そして多数の開発者からの貢献を受け⼊れるプロジェクトを運営する⽅法 などを扱います。 5.1 分散作業の流れ 中央管理型のバージョン管理システム (Centralized Version Control System: CVCS) とは違い、Git は分散型だという特徴があります。この特徴を⽣かすと、プロジェクトの 開発者間での共同作業をより柔軟に⾏えるようになります。中央管理型のシステムでは、 個々の開発者は中央のハブに対するノードという位置づけとなります。しかし Git では、 各開発者はノードであると同時にハブにもなり得ます。つまり、誰もが他のリポジトリに 対してコードを提供することができ、誰もが公開リポジトリを管理して他の開発者の作業 を受け⼊れることもできるということです。これは、みなさんのプロジェクトや開発チー ムでの作業の流れにさまざまな可能性をもたらします。本章では、この柔軟性を⽣かすい くつかの実例を⽰します。それぞれについて、利点だけでなく想定される弱点についても 扱うので、適宜取捨選択してご利⽤ください。 5.1.1 中央集権型のワークフロー 中央管理型のシステムでは共同作業の⽅式は⼀つだけです。それが中央集権型のワーク フローです。これは、中央にある⼀つのハブ (リポジトリ) がコードを受け⼊れ、他のメ ンバー全員がそこに作業内容を同期させるという流れです。多数の開発者がハブにつなが るノードとなり、作業を⼀か所に集約します (図 5-1 を参照ください)。 ⼆⼈の開発者がハブからのクローンを作成して個々に変更をした場合、最初の開発者が それをプッシュするのは特に問題なくできます。もう⼀⼈の開発者は、まず最初の開発者 の変更をマージしてからサーバーへのプッシュを⾏い、最初の開発者の変更を消してしま わないようにします。この考え⽅は、Git 上でも Subversion (あるいはその他の CVCS) と同様に⽣かせます。そしてこの⽅式は Git でも完全に機能します。 119 第5章 Git での分散作業 Scott Chacon Pro Git 図 5.1: 中央集権型のワークフロー ⼩規模なチームに所属していたり、組織内で既に中央集権型のワークフローになじんで いたりなどの場合は、Git でその⽅式を続けることも簡単です。リポジトリをひとつ⽴ち 上げて、チームのメンバー全員がそこにプッシュできるようにすればいいのです。Git は 他のユーザーの変更を上書きしてしまうことはありません。誰かがクローンして⼿元で変 更を加えた内容をプッシュしようとしたときに、もし既に他の誰かの変更がプッシュされ ていれば、サーバー側でそのプッシュは拒否されます。そして、直接プッシュすることは できないのでまずは変更内容をマージしなさいと教えてくれます。この⽅式は多くの⼈に とって魅⼒的なものでしょう。これまでにもなじみのある⽅式だし、今までそれでうまく やってきたからです。 5.1.2 統合マネージャー型のワークフロー Git では複数のリモートリポジトリを持つことができるので、書き込み権限を持つ公開 リポジトリを各⾃が持ち、他のメンバーからは読み込みのみのアクセスを許可するとい う⽅式をとることもできます。この⽅式には、「公式」プロジェクトを表す公式なリポジ トリも含みます。このプロジェクトの開発に参加するには、まずプロジェクトのクローン を⾃分⽤に作成し、変更はそこにプッシュします。次に、メインプロジェクトのメンテ ナーに「変更を取り込んでほしい」とお願いします。メンテナーはあなたのリポジトリを リモートに追加し、変更を取り込んでマージします。そしてその結果をリポジトリにプッ シュするのです。この作業の流れは次のようになります (図 5-2 を参照ください)。 1. プロジェクトのメンテナーが公開リポジトリにプッシュする 2. 開発者がそのリポジトリをクローンし、変更を加える 3. 開発者が各⾃の公開リポジトリにプッシュする 4. 開発者がメンテナーに「変更を取り込んでほしい」というメールを送る 5. メンテナーが開発者のリポジトリをリモートに追加し、それをマージする 6. マージした結果をメンテナーがメインリポジトリにプッシュする これは GitHub のようなサイトでよく使われている流れです。プロジェクトを容易に フォークでき、そこにプッシュした内容をみんなに簡単に⾒てもらえます。この⽅式の主 な利点の⼀つは、あなたはそのまま開発を続⾏し、メインリポジトリのメンテナーはいつ でも好きなタイミングで変更を取り込めるということです。変更を取り込んでもらえるま で作業を⽌めて待つ必要はありません。⾃分のペースで作業を進められるのです。 120 Scott Chacon Pro Git 5.1節 分散作業の流れ 図 5.2: 統合マネージャー型のワークフロー 5.1.3 独裁者と若頭型のワークフロー これは、複数リポジトリ型のワークフローのひとつです。何百⼈もの開発者が参加する ような巨⼤なプロジェクトで採⽤されています。有名どころでは Linux カーネルがこの ⽅式です。統合マネージャーを何⼈も⽤意し、それぞれにリポジトリの特定の部分を担当 させます。彼らは若頭 (lieutenant) と呼ばれます。そしてすべての若頭をまとめる統合マ ネージャーが「慈悲深い独裁者 (benevalent dictator)」です。独裁者のリポジトリが基 準リポジトリとなり、すべてのメンバーはこれをプルします。この作業の流れは次のよう になります (図 5-3 を参照ください)。 1. ⼀般の開発者はトピックブランチ上で作業を進め、master の先頭にリベースする。 独裁者の master ブランチがマスターとなる 2. 若頭が各開発者のトピックブランチを⾃分の master ブランチにマージする 3. 独裁者が各若頭の master ブランチを⾃分の master ブランチにマージする 4. 独裁者が⾃分の master をリポジトリにプッシュし、他のメンバーがリベースでき るようにする 図 5.3: 慈悲深い独裁者型のワークフロー この⼿のワークフローはあまり⼀般的ではありませんが、⼤規模なプロジェクトや⾼度 に階層化された環境では便利です。プロジェクトリーダー (独裁者) が⼤半の作業を委譲 し、サブセット単位である程度まとまってからコードを統合することができるからです。 Git のような分散システムでよく使われるワークフローの多くは、実社会での何らかの ワークフローにあてはめて考えることができます。これで、どのワークフローがあなた に合うかがわかったことでしょう (ですよね?)。次は、より特化した例をあげて個々のフ ローを実現する⽅法を⾒ていきましょう。 121 第5章 Git での分散作業 Scott Chacon Pro Git 5.2 プロジェクトへの貢献 さまざまなワークフローの概要について説明しました。また、すでにみなさんは Git の 基本的な使い⽅を⾝につけています。このセクションでは、何らかのプロジェクトに貢献 する際のよくあるパターンについて学びましょう。 これは⾮常に説明しづらい内容です。というのも、ほんとうにいろいろなパターンがあ るからです。Git は柔軟なシステムなので、いろいろな⽅法で共同作業をすることができ ます。そのせいもあり、どのプロジェクトをとってみても微妙に他とは異なる⽅式を使っ ているのです。違いが出てくる原因としては、アクティブな貢献者の数やプロジェクトで 使⽤しているワークフロー、あなたのコミット権、そして外部からの貢献を受け⼊れる際 の⽅式などがあります。 最初の要素はアクティブな貢献者の数です。そのプロジェクトに対してアクティブに コードを提供している開発者はどれくらいいるのか、そして彼らはどれくらいの頻度で提 供しているのか。よくあるのは、数名の開発者が⼀⽇数回のコミットを⾏うというもので す。休眠状態のプロジェクトなら、もう少し頻度が低くなるでしょう。⼤企業や⼤規模な プロジェクトでは、開発者の数が数千⼈になることもあります。数⼗から下⼿したら百 を超えるようなパッチが毎⽇やってきます。開発者の数が増えれば増えるほど、あなたの コードをきちんと適⽤したり他のコードをマージしたりするのが難しくなります。あなた が⼿元で作業をしている間に他の変更が⼊って、⼿元で変更した内容が無意味になってし まったりあるいは他の変更を壊してしまう⽻⽬になったり。そのせいで、⼿元の変更を適 ⽤してもらうための待ち時間が発⽣したり。⼿元のコードを常に最新の状態にし、正しい パッチを作るにはどうしたらいいのでしょうか。 次に考えるのは、プロジェクトが採⽤しているワークフローです。中央管理型で、すべ ての開発者がコードに対して同等の書き込みアクセス権を持っている状態? 特定のメン テナーや統合マネージャーがすべてのパッチをチェックしている? パッチを適⽤する前に ピアレビューをしている? あなたはパッチをチェックしたりピアレビューに参加したりし ている⼈? 若頭型のワークフローを使っており、まず彼らにコードを渡さなければならな い? 次の問題は、あなたのコミット権です。あなたがプロジェクトへの書き込みアクセス権 限を持っている場合は、プロジェクトに貢献するための作業の流れが変わってきます。書 き込み権限がない場合、そのプロジェクトではどのような形式での貢献を推奨しています か? 何かポリシーのようなものはありますか? ⼀度にどれくらいの作業を貢献することに なりますか? また、どれくらいの頻度で貢献することになりますか? これらの点を考慮して、あなたがどんな流れでどのようにプロジェクトに貢献していく のかが決まります。単純なものから複雑なものまで、実際の例を⾒ながら考えていきま しょう。これらの例を参考に、あなたなりのワークフローを⾒つけてください。 5.2.1 コミットの指針 個々の例を⾒る前に、コミットメッセージについてのちょっとした注意点をお話してお きましょう。コミットに関する指針をきちんと定めてそれを守るようにすると、Git での 共同作業がよりうまく進むようになります。Git プロジェクトでは、パッチの投稿⽤のコ ミットを作成するときのヒントをまとめたドキュメントを⽤意しています。Git のソース の中にある Documentation/SubmittingPatches をごらんください。 まず、余計な空⽩⽂字を含めてしまわないように注意が必要です。Git には、余計な空 ⽩⽂字をチェックするための簡単な仕組みがあります。コミットする前に git diff --check 122 Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 を実⾏してみましょう。おそらく意図したものではないと思われる空⽩⽂字を探し、それ を教えてくれます。例を⽰しましょう。端末上では⾚で表⽰される箇所を X で置き換えて います。 $ git diff --check lib/simplegit.rb:5: trailing whitespace. + @git_dir = File.expand_path(git_dir)XX lib/simplegit.rb:7: trailing whitespace. + XXXXXXXXXXX lib/simplegit.rb:26: trailing whitespace. + def command(git_cmd)XXXX コミットの前にこのコマンドを実⾏すれば、余計な空⽩⽂字をコミットしてしまって他 の開発者に嫌がられることもなくなるでしょう。 次に、コミットの単位が論理的に独⽴した変更となるようにしましょう。つまり、個々 の変更内容を把握しやすくするということです。週末に五つの問題点を修正した⼤規模な 変更を、⽉曜⽇にまとめてコミットするなどということは避けましょう。仮に週末の間に コミットできなかったとしても、ステージングエリアを活⽤して⽉曜⽇にコミット内容を 調整することができます。修正した問題ごとにコミットを分割し、それぞれに適切なコ メントをつければいいのです。もし別々の問題の修正で同じファイルを変更しているの なら、git add --patch を使ってその⼀部だけをステージすることもできます (詳しくは第6 章 で説明します)。すべての変更を同時に追加しさえすれば、⼀度にコミットしようが五 つのコミットに分割しようがブランチの先端は同じ状態になります。あとから変更内容を レビューする他のメンバーのことも考えて、できるだけレビューしやすい状態でコミット するようにしましょう。こうしておけば、あとからその変更の⼀部だけを取り消したりす るのにも便利です。第6章 では、Git を使って歴史を書き換えたり対話的にファイルをス テージしたりする⽅法を説明します。第6章 で説明する⽅法を使えば、きれいでわかりや すい歴史を作り上げることができます。 最後に注意しておきたいのが、コミットメッセージです。よりよいコミットメッセージ を書く習慣を⾝に着けておくと、Git を使った共同作業をより簡単に⾏えるようになりま す。⼀般的な規則として、メッセージの最初には変更の概要を⼀⾏ (50 ⽂字以内) にまと めた説明をつけるようにします。その後に空⾏をひとつ置いてからより詳しい説明を続 けます。Git プロジェクトでは、その変更の動機やこれまでの実装との違いなどのできる だけ詳しい説明をつけることを推奨しています。参考にするとよいでしょう。また、メッ セージでは命令形、現在形を使うようにしています。つまり 『私は○○のテストを追加 しました (I added tests for)』 とか 『○○のテストを追加します (Adding tests for,)』 で はなく 『○○のテストを追加 (Add tests for.)』 形式にするということです。Tim Pope が tpope.net で書いたテンプレート (の⽇本語訳) を以下に⽰します。 短い (50 文字以下での) 変更内容のまとめ 必要に応じた、より詳細な説明。72文字程度で折り返します。最初の 行がメールの件名、残りの部分がメールの本文だと考えてもよいでしょ 123 第5章 Git での分散作業 Scott Chacon Pro Git う。最初の行と詳細な説明の間には、必ず空行を入れなければなりま せん (詳細説明がまったくない場合は空行は不要です)。空行がないと、 rebase などがうまく動作しません。 空行を置いて、さらに段落を続けることもできます。 - 箇条書きも可能 - 箇条書きの記号としては、主にハイフンやアスタリスクを使います。 箇条書き記号の前にはひとつ空白を入れ、各項目の間には空行を入 れます。しかし、これ以外の流儀もいろいろあります。 すべてのコミットメッセージがこのようになっていれば、他の開発者との作業が⾮常に 進めやすくなるでしょう。Git プロジェクトでは、このようにきれいに整形されたコミッ トメッセージを使っています。git log --no-merges を実⾏すれば、きれいに整形されたプ ロジェクトの歴史がどのように⾒えるかがわかります。 これ以降の例を含めて本書では、説明を簡潔にするためにこのような整形を省略しま す。そのかわりに git commit の -m オプションを使います。本書での私のやり⽅をまねす るのではなく、ここで説明した⽅式を使いましょう。 5.2.2 非公開な小規模のチーム 実際に遭遇するであろう環境のうち最も⼩規模なのは、⾮公開のプロジェクトで開発者 が数名といったものです。ここでいう「⾮公開」とは、クローズドソースであるというこ と。つまり、チームのメンバー以外は⾒られないということです。チーム内のメンバーは 全員、リポジトリへのプッシュ権限を持っています。 こういった環境では、今まで Subversion やその他の中央管理型システムを使っていた ときとほぼ同じワークフローで作業を進めることができます。オフラインでコミットで きたりブランチやマージが楽だったりといった Git ならではの利点はいかせますが、作 業の流れ⾃体は今までとほぼ同じです。最⼤の違いは、マージが (コミット時にサーバー 側で⾏われるのではなく) クライアント側で⾏われるということです。⼆⼈の開発者が共 有リポジトリで開発を始めるときにどうなるかを⾒ていきましょう。最初の開発者 John が、リポジトリをクローンして変更を加え、それをローカルでコミットします (これ以降 のメッセージでは、プロトコル関連のメッセージを ... で省略しています)。 # John のマシン $ git clone john@githost:simplegit.git Initialized empty Git repository in /home/john/simplegit/.git/ ... $ cd simplegit/ $ vim lib/simplegit.rb $ git commit -am 'removed invalid default value' [master 738ee87] removed invalid default value 1 files changed, 1 insertions(+), 1 deletions(-) 124 Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 もう⼀⼈の開発者 Jessica も同様に、リポジトリをクローンして変更をコミットしまし た。 # Jessica のマシン $ git clone jessica@githost:simplegit.git Initialized empty Git repository in /home/jessica/simplegit/.git/ ... $ cd simplegit/ $ vim TODO $ git commit -am 'add reset task' [master fbff5bc] add reset task 1 files changed, 1 insertions(+), 0 deletions(-) Jessica が作業内容をサーバーにプッシュします。 # Jessica のマシン $ git push origin master ... To jessica@githost:simplegit.git 1edee6b..fbff5bc master -> master John も同様にプッシュしようとしました。 # John のマシン $ git push origin master To john@githost:simplegit.git ! [rejected] master -> master (non-fast forward) error: failed to push some refs to 'john@githost:simplegit.git' John はプッシュできませんでした。Jessica が先にプッシュを済ませていたからです。 Subversion になじみのある⼈には特に注⽬してほしいのですが、ここで John と Jessica が編集していたのは別々のファイルです。Subversion ならこのような場合はサーバー側 で⾃動的にマージを⾏いますが、Git の場合はローカルでマージしなければなりません。 John は、まず Jessica の変更内容を取得してマージしてからでないと、⾃分の変更を プッシュできないのです。 $ git fetch origin ... 125 第5章 Git での分散作業 Scott Chacon Pro Git From john@githost:simplegit + 049d078...fbff5bc master -> origin/master この時点で、John のローカルリポジトリは図 5-4 のようになっています。 図 5.4: John のリポジトリ John の⼿元に Jessica がプッシュした内容が届きましたが、さらにそれを彼⾃⾝の作 業にマージしてからでないとプッシュできません。 $ git merge origin/master Merge made by recursive. TODO | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) マージがうまくいきました。John のコミット履歴は図 5-5 のようになります。 図 5.5: origin/master をマージした後の John のリポジトリ ⾃分のコードが正しく動作することを確認した John は、変更内容をサーバーにプッ シュします。 126 Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 $ git push origin master ... To john@githost:simplegit.git fbff5bc..72bbc59 master -> master 最終的に、John のコミット履歴は図 5-6 のようになりました。 図 5.6: origin サーバーにプッシュした後の John の履歴 ⼀⽅そのころ、Jessica はトピックブランチで作業を進めていました。issue54 というト ピックブランチを作成した彼⼥は、そこで 3 回コミットをしました。彼⼥はまだ John の 変更を取得していません。したがって、彼⼥のコミット履歴は図 5-7 のような状態です。 図 5.7: Jessica のコミット履歴 Jessica は John の作業を取り込もうとしました。 # Jessica のマシン $ git fetch origin ... From jessica@githost:simplegit fbff5bc..72bbc59 master -> origin/master これで、さきほど John がプッシュした内容が取り込まれました。Jessica の履歴は図 5-8 のようになります。 Jessica のトピックブランチ上での作業が完了しました。プッシュする前にどんな作業 をマージしなければならないのかを知るため、彼⼥は git log コマンドを実⾏しました。 127 第5章 Git での分散作業 Scott Chacon Pro Git 図 5.8: John の変更を取り込んだ後の Jessica の履歴 $ git log --no-merges origin/master ^issue54 commit 738ee872852dfaa9d6634e0dea7a324040193016 Author: John Smith <[email protected]> Date: Fri May 29 16:01:27 2009 -0700 removed invalid default value Jessica はトピックブランチの内容を⾃分の master ブランチにマージし、同じく John の作業 (origin/master) も⾃分の master ブランチにマージして再び変更をサーバーにプッ シュすることになります。まずは master ブランチに戻り、これまでの作業を統合できるよ うにします。 $ git checkout master Switched to branch "master" Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded. origin/master と issue54 のどちらからマージしてもかまいません。どちらも上流にある ので、マージする順序が変わっても結果は同じなのです。どちらの順でマージしても、最 終的なスナップショットはまったく同じものになります。ただそこにいたる歴史が微妙に 変わってくるだけです。彼⼥はまず issue54 からマージすることにしました。 $ git merge issue54 Updating fbff5bc..4af4298 Fast forward README | lib/simplegit.rb | 1 + 6 +++++- 2 files changed, 6 insertions(+), 1 deletions(-) 何も問題は発⽣しません。ご覧の通り、単なる fast-forward です。次に Jessica は John の作業 (origin/master) をマージします。 128 Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 $ git merge origin/master Auto-merging lib/simplegit.rb Merge made by recursive. lib/simplegit.rb | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) こちらもうまく完了しました。Jessica の履歴は図 5-9 のようになります。 図 5.9: John の変更をマージした後の Jessica の履歴 これで、Jessica の master ブランチから origin/master に到達可能となります。これで⾃ 分の変更をプッシュできるようになりました (この作業の間に John は何もプッシュして いなかったものとします)。 $ git push origin master ... To jessica@githost:simplegit.git 72bbc59..8059c15 master -> master 各開発者が何度かコミットし、お互いの作業のマージも無事できました。図 5-10 をご らんください。 図 5.10: すべての変更をサーバーに書き戻した後の Jessica の履歴 これがもっとも単純なワークフローです。トピックブランチでしばらく作業を進め、統 合できる状態になれば⾃分の master ブランチにマージする。他の開発者の作業を取り込 む場合は、origin/master を取得してもし変更があればマージする。そして最終的にそれを サーバーの master ブランチにプッシュする。全体的な流れは図 5-11 のようになります。 129 第5章 Git での分散作業 Scott Chacon Pro Git 図 5.11: 複数開発者での Git を使ったシンプルな開発作業のイベントシーケンス 5.2.3 非公開で管理されているチーム 次に扱うシナリオは、⼤規模な⾮公開のグループに貢献するものです。機能単位の⼩規 模なグループで共同作業した結果を別のグループと統合するような環境での作業の進め⽅ を学びましょう。 John と Jessica が共同でとある機能を実装しており、Jessica はそれとは別の件で Josie とも作業をしているものとします。彼らの勤務先は統合マネージャー型のワークフ ローを採⽤しており、各グループの作業を統合する担当者が決まっています。メインリポ ジトリの master ブランチを更新できるのは統合担当者だけです。この場合、すべての作業 はチームごとのブランチで⾏われ、後で統合担当者がまとめることになります。 では、Jessica の作業の流れを追っていきましょう。彼⼥は⼆つの機能を同時に実装し ており、それぞれ別の開発者と共同作業をしています。すでに⾃分⽤のリポジトリをク ローンしている彼⼥は、まず featureA の作業を始めることにしました。この機能⽤に新し いブランチを作成し、そこで作業を進めます。 # Jessica のマシン $ git checkout -b featureA Switched to a new branch "featureA" $ vim lib/simplegit.rb $ git commit -am 'add limit to log function' [featureA 3300904] add limit to log function 1 files changed, 1 insertions(+), 1 deletions(-) ⾃分の作業内容を John に渡すため、彼⼥は featureA ブランチへのコミットをサーバー 130 Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 にプッシュしました。Jessica には master ブランチへのプッシュをする権限はありませ ん。そこにプッシュできるのは統合担当者だけなのです。そこで、John との共同作業⽤ の別のブランチにプッシュします。 $ git push origin featureA ... To jessica@githost:simplegit.git * [new branch] featureA -> featureA Jessica は John に「私の作業を featureA というブランチにプッシュしておいたので、 ⾒てね」というメールを送りました。John からの返事を待つ間、Jessica はもう⼀⽅の featureB の作業を Josie とはじめます。まず最初に、この機能⽤の新しいブランチをサー バーの master ブランチから作ります。 # Jessica のマシン $ git fetch origin $ git checkout -b featureB origin/master Switched to a new branch "featureB" そして Jessica は、featureB ブランチに何度かコミットしました。 $ vim lib/simplegit.rb $ git commit -am 'made the ls-tree function recursive' [featureB e5b0fdc] made the ls-tree function recursive 1 files changed, 1 insertions(+), 1 deletions(-) $ vim lib/simplegit.rb $ git commit -am 'add ls-files' [featureB 8512791] add ls-files 1 files changed, 5 insertions(+), 0 deletions(-) Jessica のリポジトリは図 5-12 のようになっています。 この変更をプッシュしようと思ったそのときに、Josie から「私の作業を featureBee というブランチにプッシュしておいたので、⾒てね」というメールがやってきました。 Jessica はまずこの変更をマージしてからでないとサーバーにプッシュすることはできま せん。そこで、まず Josie の変更を git fetch で取得しました。 $ git fetch origin ... From jessica@githost:simplegit * [new branch] featureBee -> origin/featureBee 131 第5章 Git での分散作業 Scott Chacon Pro Git 図 5.12: Jessica のコミット履歴 次に、git merge でこの内容を⾃分の作業にマージします。 $ git merge origin/featureBee Auto-merging lib/simplegit.rb Merge made by recursive. lib/simplegit.rb | 4 ++++ 1 files changed, 4 insertions(+), 0 deletions(-) ここでちょっとした問題が発⽣しました。彼⼥は、⼿元の featureB ブランチの内容を サーバーの featureBee ブランチにプッシュしなければなりません。このような場合は、git push コマンドでローカルブランチ名に続けてコロン (:) を書き、その後にリモートブラン チ名を指定します。 $ git push origin featureB:featureBee ... To jessica@githost:simplegit.git fba9af8..cd685d1 featureB -> featureBee これは refspec と呼ばれます。第9章 で、Git の refspec の詳細とそれで何ができるの かを説明します。 さて、John からメールが返ってきました。「私の変更も featureA ブランチにプッシュ しておいたので、確認よろしく」とのことです。彼⼥は git fetch でその変更を取り込み ます。 $ git fetch origin ... From jessica@githost:simplegit 3300904..aad881d 132 featureA -> origin/featureA Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 そして、git log で何が変わったのかを確認します。 $ git log origin/featureA ^featureA commit aad881d154acdaeb2b6b18ea0e827ed8a6d671e6 Author: John Smith <[email protected]> Date: Fri May 29 19:57:33 2009 -0700 changed log output to 30 from 25 確認を終えた彼⼥は、John の作業を⾃分の featureA ブランチにマージしました。 $ git checkout featureA Switched to branch "featureA" $ git merge origin/featureA Updating 3300904..aad881d Fast forward lib/simplegit.rb | 10 +++++++++- 1 files changed, 9 insertions(+), 1 deletions(-) Jessica はもう少し⼿を⼊れたいところがあったので、再びコミットしてそれをサー バーにプッシュします。 $ git commit -am 'small tweak' [featureA 774b3ed] small tweak 1 files changed, 1 insertions(+), 1 deletions(-) $ git push origin featureA ... To jessica@githost:simplegit.git 3300904..774b3ed featureA -> featureA Jessica のコミット履歴は、この時点で図 5-13 のようになります。 Jessica、Josie そして John は、統合担当者に「featureA ブランチと featureBee ブラン チは本流に統合できる状態になりました」と報告しました。これらのブランチが本流に統 合された後で本流を取得すると、マージコミットが新たに追加されて図 5-14 のような状 態になります。 Git へ移⾏するグループが続出しているのも、この「複数チームの作業を並⾏して進 め、後で統合できる」という機能のおかげです。⼩さなグループ単位でリモートブランチ を使った共同作業ができ、しかもそれがチーム全体の作業を妨げることがない。これは Git の⼤きな利点です。ここで⾒たワークフローをまとめると、図 5-15 のようになりま す。 133 第5章 Git での分散作業 Scott Chacon Pro Git 図 5.13: Jessica がブランチにコミットした後のコミット履歴 図 5.14: Jessica が両方のトピックブランチをマージしたあとのコミット履歴 図 5.15: 管理されたチームでのワークフローの基本的な流れ 134 Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 5.2.4 小規模な公開プロジェクト 公開プロジェクトに貢献するとなると、また少し話が変わってきます。そのプロジェク トのブランチを直接更新できる権限はないでしょうから、何か別の⽅法でメンテナに接 触する必要があります。最初の例では、フォークをサポートしている Git ホスティング サービスでフォークを使って貢献する⽅法を説明します。repo.or.cz と GitHub はどちら もフォークに対応しており、多くのメンテナはこの⽅式での協⼒を期待しています。そし てこの次のセクションでは、メールでパッチを送る形式での貢献について説明します。 まずはメインリポジトリをクローンしましょう。そしてパッチ⽤のトピックブランチを 作り、そこで作業を進めます。このような流れになります。 $ git clone (url) $ cd project $ git checkout -b featureA $ (作業) $ git commit $ (作業) $ git commit rebase -i を使ってすべての作業をひとつのコミットにまとめたり、メンテナがレビュー しやすいようにコミット内容を整理したりといったことも⾏うかもしれません。対話的な リベースの⽅法については第6章 で詳しく説明します。 ブランチでの作業を終えてメンテナに渡せる状態になったら、プロジェクトのページに ⾏って 『Fork』 ボタンを押し、⾃分⽤に書き込み可能なフォークを作成します。このリ ポジトリの URL をリモートとして追加しなければなりません。ここでは myfork という名 前にしました。 $ git remote add myfork (url) ⾃分の作業内容は、ここにプッシュすることになります。変更を master ブランチに マージしてからそれをプッシュするよりも、今作業中の内容をそのままリモートブランチ にプッシュするほうが簡単でしょう。もしその変更が受け⼊れられなかったり⼀部だけが 取り込まれたりした場合に、master ブランチを巻き戻す必要がなくなるからです。メン テナがあなたの作業をマージするかリベースするかあるいは⼀部だけ取り込むか、いずれ にせよあなたはその結果をリポジトリから再度取り込むことになります。 $ git push myfork featureA ⾃分⽤のフォークに作業内容をプッシュし終えたら、それをメンテナに伝えましょ う。これは、よく「プルリクエスト」と呼ばれるもので、ウェブサイトから実⾏する (GutHub には 『pull request』 ボタンがあり、メンテナに⾃動的にメッセージを送って 135 第5章 Git での分散作業 Scott Chacon Pro Git くれます) こともできれば git request-pull コマンドの出⼒をプロジェクトのメンテナに メールで送ることもできます。 request-pull コマンドには、トピックブランチをプルしてもらいたい先のブランチとそ の Git リポジトリの URL を指定します。すると、プルしてもらいたい変更の概要が出⼒ されます。たとえば Jessica が John にプルリクエストを送ろうとしたとしましょう。彼 ⼥はすでにトピックブランチ上で 2 回のコミットを済ませています。 $ git request-pull origin/master myfork The following changes since commit 1edee6b1d61823a2de3b09c160d7080b8d1b3a40: John Smith (1): added a new function are available in the git repository at: git://githost/simplegit.git featureA Jessica Smith (2): add limit to log function change log output to 30 from 25 lib/simplegit.rb | 10 +++++++++- 1 files changed, 9 insertions(+), 1 deletions(-) この出⼒をメンテナに送れば「どのブランチからフォークしたのか、どういったコミッ トをしたのか、そしてそれをどこにプルしてほしいのか」を伝えることができます。 ⾃分がメンテナになっていないプロジェクトで作業をする場合は、master ブランチでは 常に origin/master を追いかけるようにし、⾃分の作業はトピックブランチで進めていく ほうが楽です。そうすれば、パッチが拒否されたときも簡単にそれを捨てることができま す。また、作業内容ごとにトピックブランチを分離しておけば、本流のリポジトリが更新 されてパッチがうまく適⽤できなくなったとしても簡単にリベースできるようになりま す。たとえば、さきほどのプロジェクトに対して別の作業をすることになったとしましょ う。その場合は、先ほどプッシュしたトピックブランチを使うのではなく、メインリポジ トリの master ブランチから新たなトピックブランチを作成します。 $ git checkout -b featureB origin/master $ (作業) $ git commit $ git push myfork featureB $ (メンテナにメールを送る) $ git fetch origin これで、それぞれのトピックがサイロに⼊った状態になりました。お互いのトピックが 邪魔しあったり依存しあったりすることなく、それぞれ個別に書き換えやリベースが可能 136 Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 となります。図 5-16 を参照ください。 図 5.16: featureB に関する作業のコミット履歴 プロジェクトのメンテナが、他の⼤量のパッチを適⽤したあとであなたの最初のパッチ を適⽤しようとしました。しかしその時点でパッチはすでにそのままでは適⽤できなく なっています。こんな場合は、そのブランチを origin/master の先端にリベースして衝突を 解決させ、あらためて変更内容をメンテナに送ります。 $ git checkout featureA $ git rebase origin/master $ git push -f myfork featureA これで、あなたの歴史は図 5-17 のように書き換えられました。 図 5.17: featureA の作業を終えた後のコミット履歴 ブランチをリベースしたので、プッシュする際には -f を指定しなければなりません。 これは、サーバー上の featureA ブランチをその直系の⼦孫以外のコミットで上書きするた めです。別のやり⽅として、今回の作業を別のブランチ (featureAv2 など) にプッシュする こともできます。 もうひとつ別のシナリオを考えてみましょう。あなたの⼆番⽬のブランチを⾒たメン テナが、その考え⽅は気に⼊ったものの細かい実装をちょっと変更してほしいと連絡し てきました。この場合も、プロジェクトの master ブランチから作業を進めます。現在の origin/master から新たにブランチを作成し、そこに featureB ブランチの変更を押し込み、 もし衝突があればそれを解決し、実装をちょっと変更してからそれを新しいブランチとし てプッシュします。 137 第5章 Git での分散作業 Scott Chacon Pro Git $ git checkout -b featureBv2 origin/master $ git merge --no-commit --squash featureB $ (実装をちょっと変更する) $ git commit $ git push myfork featureBv2 --squash オプションは、マージしたいブランチでのすべての作業をひとつのコミットに まとめ、それを現在のブランチの先頭にマージします。--no-commit オプションは、⾃動的 にコミットを記録しないよう Git に指⽰しています。こうすれば、別のブランチのすべて の変更を取り込んでさらに⼿元で変更を加えたものを新しいコミットとして記録できるの です。 そして、メンテナに「⾔われたとおりのちょっとした変更をしたものが featureBv2 ブラ ンチにあるよ」と連絡します (図 5-18 を参照ください)。 図 5.18: featureBv2 の作業を終えた後のコミット履歴 5.2.5 大規模な公開プロジェクト 多くの⼤規模プロジェクトでは、パッチを受け付ける⼿続きが確⽴されています。プロ ジェクトによっていろいろ異なるので、まずはそのプロジェクト固有のルールがないかど うか確認しましょう。しかし、⼤規模なプロジェクトの多くは開発者⽤メーリングリスト へのパッチの投稿を受け付けています。そこで、ここではそれを例にとって話を進めま す。 実際の作業の流れは先ほどとほぼ同じで、作業する内容ごとにトピックブランチを作成 することになります。違うのは、パッチをプロジェクトに提供する⽅法です。プロジェク トをフォークし、⾃分⽤のリポジトリにプッシュするのではなく、個々のコミットについ てメールを作成し、それを開発者⽤メーリングリストに投稿します。 $ git checkout -b topicA $ (作業) $ git commit $ (作業) $ git commit 138 Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 これで⼆つのコミットができあがりました。これらをメーリングリストに投稿しま す。git format-patch を使うと mbox 形式のファイルが作成されるので、これをメーリン グリストに送ることができます。このコマンドは、コミットメッセージの⼀⾏⽬を件名、 残りのコミットメッセージとコミット内容のパッチを本⽂に書いたメールを作成します。 これのよいところは、format-patch で作成したメールからパッチを適⽤すると、すべての コミット情報が適切に維持されるというところです。次のセクションで実際にパッチを適 ⽤するところになれば、よりはっきりと実感するでしょう。 $ git format-patch -M origin/master 0001-add-limit-to-log-function.patch 0002-changed-log-output-to-30-from-25.patch format-patch コマンドは、できあがったパッチファイルの名前を出⼒します。-M スイッ チは、名前が変わったことを検出するためのものです。できあがったファイルは次のよう になります。 $ cat 0001-add-limit-to-log-function.patch From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001 From: Jessica Smith <[email protected]> Date: Sun, 6 Apr 2008 10:17:23 -0700 Subject: [PATCH 1/2] add limit to log function Limit log functionality to the first 20 --lib/simplegit.rb | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diff --git a/lib/simplegit.rb b/lib/simplegit.rb index 76f47bc..f9815f1 100644 --- a/lib/simplegit.rb +++ b/lib/simplegit.rb @@ -14,7 +14,7 @@ class SimpleGit end def log(treeish = 'master') - command("git log #{treeish}") + command("git log -n 20 #{treeish}") end def ls_tree(treeish = 'master') -1.6.2.rc1.20.g8c5b.dirty 139 第5章 Git での分散作業 Scott Chacon Pro Git このファイルを編集して、コミットメッセージには書けなかったような情報をメーリ ングリスト⽤に追加することもできます。--- の⾏とパッチの開始位置 (lib/simplegit.rb の⾏) の間にメッセージを書くと、メールを受信した⼈はそれを読むことができますが、 パッチからは除外されます。 これをメーリングリストに投稿するには、メールソフトにファイルの内容を貼り付ける か、あるいはコマンドラインのプログラムを使います。ファイルの内容をコピーして貼り 付けると「かしこい」メールソフトが勝⼿に改⾏の位置を変えてしまうなどの問題が起 こりがちです。ありがたいことに Git には、きちんとしたフォーマットのパッチを IMAP で送ることを⽀援するツールが⽤意されています。これを使うと便利です。ここでは、 パッチを Gmail で送る⽅法を説明しましょう。というのも、たまたま私が使ってるメー ルソフトが Gmail だからです。さまざまなメールソフトでの詳細なメール送信⽅法が、 Git ソースコードにある Documentation/SubmittingPatches の最後に載っています。 まず。~/.gitconfig ファイルの imap セクションを設定します。それぞれの値を git config コマンドで順に設定してもかまいませんし、このファイルに⼿で書き加えてもかま いません。最終的に、設定ファイルは次のようになります。 [imap] folder = "[Gmail]/Drafts" host = imaps://imap.gmail.com user = [email protected] pass = p4ssw0rd port = 993 sslverify = false IMAP サーバーで SSL を使っていない場合は、最後の⼆⾏はおそらく不要でしょう。 そして host のところが imaps:// ではなく imap:// となります。ここまでの設定が終われ ば、git imap-send を実⾏して IMAP サーバーの Drafts フォルダにパッチを置くことがで きるようになります。 $ cat *.patch |git imap-send Resolving imap.gmail.com... ok Connecting to [74.125.142.109]:993... ok Logging in... sending 2 messages 100% (2/2) done あとは、Drafts フォルダに移動して To フィールドをメーリングリストのアドレスに変 更し (おそらく CC には担当メンテなのアドレスを⼊れ)、送信できるようになりました。 SMTP サーバーを使ってパッチを送ることもできます。IMAP サーバー同様、設定は git config コマンドで順に設定してもいいですし、~/.gitconfig ファイルの sendmail セク ションに直接⼊⼒してもかまいません。 140 Scott Chacon Pro Git 5.2節 プロジェクトへの貢献 [sendemail] smtpencryption = tls smtpserver = smtp.gmail.com smtpuser = [email protected] smtpserverport = 587 設定が追加できたら、git send-email を実⾏してパッチを送信します。 $ git send-email *.patch 0001-added-limit-to-log-function.patch 0002-changed-log-output-to-30-from-25.patch Who should the emails appear to be from? [Jessica Smith <[email protected]>] Emails will be sent from: Jessica Smith <[email protected]> Who should the emails be sent to? [email protected] Message-ID to be used as In-Reply-To for the first email? y Git はその後、各パッチについてこのようなログ情報をはき出すはずです。 (mbox) Adding cc: Jessica Smith <[email protected]> from \line 'From: Jessica Smith <[email protected]>' OK. Log says: Sendmail: /usr/sbin/sendmail -i [email protected] From: Jessica Smith <[email protected]> To: [email protected] Subject: [PATCH 1/2] added limit to log function Date: Sat, 30 May 2009 13:29:15 -0700 Message-Id: <[email protected]> X-Mailer: git-send-email 1.6.2.rc1.20.g8c5b.dirty In-Reply-To: <y> References: <y> Result: OK 5.2.6 まとめ このセクションでは、今後みなさんが遭遇するであろうさまざまな形式の Git プロジェ クトについて、関わっていくための作業⼿順を説明しました。そして、その際に使える新 兵器もいくつか紹介しました。次はもう⼀⽅の側、つまり Git プロジェクトを運営する側 について⾒ていきましょう。慈悲深い独裁者、あるいは統合マネージャーとしての作業⼿ 順を説明します。 141 第5章 Git での分散作業 Scott Chacon Pro Git 5.3 プロジェクトの運営 プロジェクトに貢献する⽅法だけでなく、プロジェクトを運営する⽅法についても知っ ておくといいでしょう。たとえば format-patch を使ってメールで送られてきたパッチを処 理する⽅法や、別のリポジトリのリモートブランチでの変更を統合する⽅法などです。本 流のリポジトリを保守するにせよパッチの検証や適⽤を⼿伝うにせよ、どうすれば貢献者 たちにとってわかりやすくなるかを知っておくべきでしょう。 5.3.1 トピックブランチでの作業 新しい機能を組み込もうと考えている場合は、トピックブランチを作ることをおすすめ します。トピックブランチとは、新しく作業を始めるときに⼀時的に作るブランチのこと です。そうすれば、そのパッチだけを個別にいじることができ、もしうまくいかなかった としてもすぐに元の状態に戻すことができます。ブランチの名前は、今からやろうとして いる作業の内容にあわせたシンプルな名前にしておきます。たとえば ruby_client などと いったものです。そうすれば、しばらく時間をおいた後でそれを廃棄することになったと きに、内容を思い出しやすくなります。Git プロジェクトのメンテナは、ブランチ名に名 前空間を使うことが多いようです。たとえば sc/ruby_client のようになり、ここでの sc は その作業をしてくれた⼈の名前を短縮したものとなります。⾃分の master ブランチをも とにしたブランチを作成する⽅法は、このようになります。 $ git branch sc/ruby_client master 作成してすぐそのブランチに切り替えたい場合は、checkout -b コマンドを使います。 $ git checkout -b sc/ruby_client master 受け取った作業はこのトピックブランチですすめ、⻑期ブランチに統合するかどうかを 判断することになります。 5.3.2 メールで受け取ったパッチの適用 あなたのプロジェクトへのパッチをメールで受け取った場合は、まずそれをトピックブ ランチに適⽤して中⾝を検証します。メールで届いたパッチを適⽤するには git apply と git am の⼆通りの⽅法があります。 apply でのパッチの適用 git diff あるいは Unix の diff コマンドで作ったパッチを受け取ったときは、git apply コマンドを使ってパッチを適⽤します。パッチが /tmp/patch-ruby-client.patch にあるとす ると、このようにすればパッチを適⽤できます。 142 Scott Chacon Pro Git 5.3節 プロジェクトの運営 $ git apply /tmp/patch-ruby-client.patch これは、作業ディレクトリ内のファイルを変更します。patch -p1 コマンドでパッチをあ てるのとほぼ同じなのですが、それ以上に「これでもか」というほどのこだわりを持って パッチを適⽤するので fuzzy マッチになる可能性が少なくなります。また、git diff 形式 ではファイルの追加・削除やファイル名の変更も扱うことができますが、patch コマンド にはそれはできません。そして最後に、git apply は「全部適⽤するか、あるいは⼀切適⽤ しないか」というモデルを採⽤しています。⼀⽅ patch コマンドの場合は、途中までパッ チがあたった中途半端な状態になって困ることがあります。git apply のほうが、 patch よ りもこだわりを持った処理を⾏うのです。git apply コマンドはコミットを作成するわけで はありません。実⾏した後で、その変更をステージしてコミットする必要があります。 git apply を使って、そのパッチをきちんと適⽤できるかどうかを事前に確かめることが できます。パッチをチェックするには git apply --check を実⾏します。 $ git apply --check 0001-seeing-if-this-helps-the-gem.patch error: patch failed: ticgit.gemspec:1 error: ticgit.gemspec: patch does not apply 何も出⼒されなければ、そのパッチはうまく適⽤できるということです。このコマンド は、チェックに失敗した場合にゼロ以外の値を返して終了します。スクリプト内でチェッ クしたい場合などにはこの返り値を使⽤します。 am でのパッチの適用 コードを提供してくれた⼈が Git のユーザーで、format-patch コマンドを使ってパッチ を送ってくれたとしましょう。この場合、あなたの作業はより簡単になります。パッチの 中に、作者の情報やコミットメッセージも含まれているからです。「パッチを作るときに は、できるだけ diff ではなく format-patch を使ってね」とお願いしてみるのもいいでしょ う。昔ながらの形式のパッチが届いたときだけは git apply を使わなければならなくなり ます。 format-patch で作ったパッチを適⽤するには git am を使います。技術的なお話をする と、git am は mbox ファイルを読み込む仕組みになっています。mbox はシンプルなプ レーンテキスト形式で、⼀通あるいは複数のメールのメッセージをひとつのテキストファ イルにまとめるためのものです。中⾝はこのようになります。 From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001 From: Jessica Smith <[email protected]> Date: Sun, 6 Apr 2008 10:17:23 -0700 Subject: [PATCH 1/2] add limit to log function Limit log functionality to the first 20 143 第5章 Git での分散作業 Scott Chacon Pro Git 先ほどのセクションでごらんいただいたように、format-patch コマンドの出⼒結果も これと同じ形式で始まっていますね。これは、mbox 形式のメールフォーマットとしても 正しいものです。git send-email を正しく使ったパッチが送られてきた場合、受け取った メールを mbox 形式で保存して git am コマンドでそのファイルを指定すると、すべての パッチの適⽤が始まります。複数のメールをまとめてひとつの mbox に保存できるメー ルソフトを使っていれば、送られてきたパッチをひとつのファイルにまとめて git am で⼀ 度に適⽤することもできます。 しかし、format-patch で作ったパッチがチケットシステム (あるいはそれに類する何か) にアップロードされたような場合は、まずそのファイルをローカルに保存して、それを git am に渡すことになります。 $ git am 0001-limit-log-function.patch Applying: add limit to log function どんなパッチを適⽤したのかが表⽰され、コミットも⾃動的に作られます。作者の情報 はメールの From ヘッダと Date ヘッダから取得し、コミットメッセージは Subject とメール の本⽂ (パッチより前の部分) から取得します。たとえば、先ほどごらんいただいた mbox の例にあるパッチを適⽤した場合は次のようなコミットとなります。 $ git log --pretty=fuller -1 commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0 Author: Jessica Smith <[email protected]> AuthorDate: Sun Apr 6 10:17:23 2008 -0700 Commit: Scott Chacon <[email protected]> CommitDate: Thu Apr 9 09:19:06 2009 -0700 add limit to log function Limit log functionality to the first 20 Commit には、そのパッチを適⽤した⼈と適⽤した⽇時が表⽰されます。Author には、そ のパッチを実際に作成した⼈と作成した⽇時が表⽰されます。 しかし、パッチが常にうまく適⽤できるとは限りません。パッチを作成したときの状態 と現在のメインブランチとが⼤きくかけ離れてしまっていたり、そのパッチが別の (まだ 適⽤していない) パッチに依存していたりなどといったことがあり得るでしょう。そんな 場合は git am は失敗し、次にどうするかを聞かれます。 $ git am 0001-seeing-if-this-helps-the-gem.patch Applying: seeing if this helps the gem error: patch failed: ticgit.gemspec:1 error: ticgit.gemspec: patch does not apply 144 Scott Chacon Pro Git 5.3節 プロジェクトの運営 Patch failed at 0001. When you have resolved this problem run "git am --resolved". If you would prefer to skip this patch, instead run "git am --skip". To restore the original branch and stop patching run "git am --abort". このコマンドは、何か問題が発⽣したファイルについて衝突マークを書き込みます。こ れは、マージやリベースに失敗したときに書き込まれるのとよく似たものです。問題を解 決する⽅法も同じです。まずはファイルを編集して衝突を解決し、新しいファイルをス テージし、git am --resolved を実⾏して次のパッチに進みます。 $ (ファイルを編集する) $ git add ticgit.gemspec $ git am --resolved Applying: seeing if this helps the gem Git にもうちょっと賢く働いてもらって衝突を回避したい場合は、-3 オプションを使⽤ します。これは、Git で三⽅向のマージを⾏うオプションです。このオプションはデフォ ルトでは有効になっていません。適⽤するパッチの元になっているコミットがあなたのリ ポジトリ上のものでない場合に正しく動作しないからです。パッチの元になっているコ ミットが⼿元にある場合は、-3 オプションを使うと、衝突しているパッチをうまく適⽤ できます。 $ git am -3 0001-seeing-if-this-helps-the-gem.patch Applying: seeing if this helps the gem error: patch failed: ticgit.gemspec:1 error: ticgit.gemspec: patch does not apply Using index info to reconstruct a base tree... Falling back to patching base and 3-way merge... No changes -- Patch already applied. ここでは、既に適⽤済みのパッチを適⽤してみました。-3 オプションがなければ、衝 突が発⽣していたことでしょう。 たくさんのパッチが含まれる mbox からパッチを適⽤するときには、am コマンドを対 話モードで実⾏することもできます。パッチが⾒つかるたびに処理を⽌め、それを適⽤す るかどうかの確認を求められます。 $ git am -3 -i mbox Commit Body is: -------------------------seeing if this helps the gem 145 第5章 Git での分散作業 Scott Chacon Pro Git -------------------------Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all これは、「⼤量にあるパッチについて、内容をまず⼀通り確認したい」「既に適⽤済み のパッチは適⽤しないようにしたい」などの場合に便利です。 トピックブランチ上でそのトピックに関するすべてのパッチの適⽤を済ませてコミット すれば、次はそれを⻑期ブランチに統合するかどうか (そしてどのように統合するか) を 考えることになります。 5.3.3 リモートブランチのチェックアウト ⾃前のリポジトリを持つ Git ユーザーが⾃分のリポジトリに変更をプッシュし、そのリ ポジトリの URL とリモートブランチ名だけをあなたにメールで連絡してきた場合のこと を考えてみましょう。そのリポジトリをリモートとして登録し、それをローカルにマージ することになります。 Jessica から「すばらしい新機能を作ったので、私のリポジトリの ruby-client ブランチ を⾒てください」といったメールが来たとします。これを⼿元でテストするには、リモー トとしてこのリポジトリを追加し、ローカルにブランチをチェックアウトします。 $ git remote add jessica git://github.com/jessica/myproject.git $ git fetch jessica $ git checkout -b rubyclient jessica/ruby-client 「この前のとは違う、別のすばらしい機能を作ったの!」と別のブランチを伝えられた 場合は、すでにリモートの設定が済んでいるので単にそのブランチを取得してチェックア ウトするだけで確認できます。 この⽅法は、誰かと継続的に共同作業を進めていく際に便利です。ちょっとしたパッチ をたまに提供してくれるだけの⼈の場合は、パッチをメールで受け取るようにしたほう が時間の節約になるでしょう。全員に⾃前のサーバーを⽤意させて、たまに送られてくる パッチを取得するためだけに定期的にリモートの追加と削除を⾏うなどというのは時間の 無駄です。ほんの数件のパッチを提供してくれる⼈たちを含めて数百ものリモートを管理 することなど、きっとあなたはお望みではないでしょう。しかし、スクリプトやホスティ ングサービスを使えばこの⼿の作業は楽になります。つまり、どのような⽅式をとるか は、あなたや他のメンバーがどのような⽅式で開発を進めるかによって決まります。 この⽅式のもうひとつの利点は、コミットの履歴も同時に取得できるということです。 マージの際に問題が起こることもあるでしょうが、そんな場合にも相⼿の作業が⾃分側の どの地点に基づくものなのかを知ることができます。適切に三⽅向のマージが⾏われる ので、-3 を指定したときに「このパッチの基点となるコミットにアクセスできればいい なぁ」と祈る必要はありません。 継続的に共同作業を続けるわけではないけれど、それでもこの⽅式でパッチを取得した いという場合は、リモートリポジトリの URL を git pull コマンドで指定することもでき ます。これは⼀度きりのプルに使うものであり、リモートを参照する URL は保存されま せん。 146 Scott Chacon Pro Git 5.3節 プロジェクトの運営 $ git pull git://github.com/onetimeguy/project.git From git://github.com/onetimeguy/project * branch HEAD -> FETCH_HEAD Merge made by recursive. 5.3.4 何が変わるのかの把握 トピックブランチの中に、提供してもらった作業が含まれた状態になりました。次に何 をすればいいのか考えてみましょう。このセクションでは、これまでに扱ったいくつかの コマンドを復習します。それらを使って、もしこの変更をメインブランチにマージしたら いったい何が起こるのかを調べていきましょう。 トピックブランチのコミットのうち、master ブランチに存在しないコミットの内容を ひとつひとつレビューできれば便利でしょう。master ブランチに含まれるコミットを除 外するには、ブランチ名の前に --not オプションを指定します。たとえば、誰かから受け 取った⼆つのパッチを適⽤するために contrib というブランチを作成したとすると、 $ git log contrib --not master commit 5b6235bd297351589efc4d73316f0a68d484f118 Author: Scott Chacon <[email protected]> Date: Fri Oct 24 09:53:59 2008 -0700 seeing if this helps the gem commit 7482e0d16d04bea79d0dba8988cc78df655f16a0 Author: Scott Chacon <[email protected]> Date: Mon Oct 22 19:38:36 2008 -0700 updated the gemspec to hopefully work better このようなコマンドを実⾏すればそれぞれのコミットの内容を確認できます。git log に -p オプションを渡せば、コミットの後に diff を表⽰させることもできます。これも以 前に説明しましたね。 このトピックブランチを別のブランチにマージしたときに何が起こるのかを完全な diff で知りたい場合は、ちょっとした裏技を使わないと正しい結果が得られません。おそらく 「こんなコマンドを実⾏するだけじゃないの?」と考えておられることでしょう。 $ git diff master このコマンドで表⽰される diff は、誤解を招きかねないものです。トピックブランチを 切った時点からさらに master ブランチが先に進んでいたとすると、これは少し奇妙に⾒ 147 第5章 Git での分散作業 Scott Chacon Pro Git える結果を返します。というのも、Git は現在のトピックブランチの最新のコミットのス ナップショットと master ブランチの最新のコミットのスナップショットを直接⽐較するか らです。トピックブランチを切った後に master ブランチ上であるファイルに⾏を追加した とすると、スナップショットを⽐較した結果は「トピックブランチでその⾏を削除しよう としている」状態になります。 master がトピックブランチの直系の先祖である場合は、これは特に問題とはなりませ ん。しかし⼆つの歴史が分岐している場合には、diff の結果は「トピックブランチで新し く追加したすべての内容を追加し、master ブランチにしかないものはすべて削除する」と いうものになります。 本当に知りたいのはトピックブランチで変更された内容、つまりこのブランチを master にマージしたときに master に加わる変更です。これを知るには、Git に「トピッ クブランチの最新のコミット」と「トピックブランチと master ブランチの直近の共通の 先祖」とを⽐較させます。 共通の先祖を⾒つけだしてそこからの diff を取得するには、このようにします。 $ git merge-base contrib master 36c7dba2c95e6bbb78dfa822519ecfec6e1ca649 $ git diff 36c7db しかし、これでは不便です。そこで Git には、同じことをより⼿短にやるための⼿段と してトリプルドット構⽂が⽤意されています。diff コマンドを実⾏するときにピリオドを 三つ打った後に別のブランチを指定すると、「現在いるブランチの最新のコミット」と 「指定した⼆つのブランチの共通の先祖」とを⽐較するようになります。 $ git diff master...contrib このコマンドは、master との共通の先祖から分岐した現在のトピックブランチで変更 された内容のみを表⽰します。この構⽂は、覚えやすいので⾮常に便利です。 5.3.5 提供された作業の取り込み トピックブランチでの作業をメインブランチに取り込む準備ができたら、どのように取 り込むかを考えることになります。さらに、プロジェクトを運営していくにあたっての全 体的な作業の流れはどのようにしたらいいでしょうか? さまざまな⽅法がありますが、こ こではそのうちのいくつかを紹介します。 マージのワークフロー シンプルなワークフローのひとつとして、作業を⾃分の master ブランチに取り込むこと を考えます。ここでは、master ブランチで安定版のコードを管理しているものとします。 トピックブランチでの作業が⼀段落したら (あるいは誰かから受け取ったパッチをトピッ クブランチ上で検証し終えたら)、それを master ブランチにマージしてからトピックブ ランチを削除し、作業を進めることになります。ruby_client および php_client の⼆つのブ 148 Scott Chacon Pro Git 5.3節 プロジェクトの運営 ランチを持つ図 5-19 のようなリポジトリでまず ruby_client をマージしてから php_client もマージすると、歴史は図 5-20 のようになります。 図 5.19: いくつかのトピックブランチを含む履歴 図 5.20: トピックブランチをマージした後の状態 これがおそらく⼀番シンプルなワークフローでしょうが、⼤規模なリポジトリやプロ ジェクトで作業をしていると問題が発⽣することもあります。 多⼈数で開発していたり⼤規模なプロジェクトに参加していたりする場合は、⼆段階 以上のマージサイクルを使うこともあるでしょう。ここでは、⻑期間運⽤するブランチが master と develop のふたつあるものとします。master が更新されるのは安定版がリリースさ れるときだけで、新しいコードはずべて develop ブランチに統合されるという流れです。 これらのブランチは、両⽅とも定期的に公開リポジトリにプッシュすることになります。 新しいトピックブランチをマージする準備ができたら (図 5-21)、それを develop にマージ します (図 5-22)。そしてリリースタグを打つときに、master を現在の develop ブランチが 149 第5章 Git での分散作業 Scott Chacon Pro Git 指す位置に進めます (図 5-23)。 図 5.21: トピックブランチのマージ前 図 5.22: トピックブランチのマージ後 図 5.23: トピックブランチのリリース後 他の⼈があなたのプロジェクトをクローンするときには、master をチェックアウトす れば最新の安定版をビルドすることができ、その後の更新を追いかけるのも容易にできる ようになります。⼀⽅ develop をチェックアウトすれば、さらに最先端の状態を取得す ることができます。この考え⽅を推し進めると、統合⽤のブランチを⽤意してすべての作 業をいったんそこにマージするようにもできます。統合ブランチ上のコードが安定してテ 150 Scott Chacon Pro Git 5.3節 プロジェクトの運営 ストを通過すれば、それを develop ブランチにマージします。そしてそれが安定してい ることが確認できたら master ブランチを先に進めるということになります。 大規模マージのワークフロー Git 開発プロジェクトには、常時稼働するブランチが四つあります。master、next、そし て新しい作業⽤の pu (proposed updates) とメンテナンスバックポート⽤の maint です。 新しいコードを受け取ったメンテナは、まず⾃分のリポジトリのトピックブランチにそれ を格納します。先ほど説明したのと同じ⽅式です (図 5-24 を参照ください)。そしてその 内容を検証し、安全に取り込める状態かさらなる作業が必要かを⾒極めます。だいじょう ぶだと判断したらそれを next にマージします。このブランチをプッシュすれば、すべて のメンバーがそれを試せるようになります。 図 5.24: 複数のトピックブランチの並行管理 さらに作業が必要なトピックについては、pu にマージします。完全に安定していると 判断されたトピックについては改めて master にマージされ、next にあるトピックのうちま だ master に⼊っていないものを再構築します。つまり、master はほぼ常に前に進み、next は時々リベースされ、pu はそれ以上の頻度でリベースされることになります (図 5-25 を 参照ください)。 図 5.25: 常時稼働する統合用ブランチへのトピックブランチのマージ 最終的に master にマージされたトピックブランチは、リポジトリから削除します。 Git 開発プロジェクトでは maint ブランチも管理しています。これは最新のリリースから フォークしたもので、メンテナンスリリースに必要なバックポート⽤のパッチを管理しま す。つまり、Git のリポジトリをクローンするとあなたは四つのブランチをチェックアウ 151 第5章 Git での分散作業 Scott Chacon Pro Git トすることができるということです。これらのブランチはどれも異なる開発段階を表し、 「どこまで最先端を追いかけたいか」「どのように Git プロジェクトに貢献したいか」に よって使い分けることになります。メンテナ側では、新たな貢献を受け⼊れるためのワー クフローが整っています。 リベースとチェリーピックのワークフロー 受け取った作業を master ブランチにマージするのではなく、リベースやチェリーピッ クを使って master ブランチの先端につなげていく⽅法を好むメンテナもいます。そのほ うがほぼ直線的な歴史を保てるからです。トピックブランチでの作業を終えて統合できる 状態になったと判断したら、そのブランチで rebase コマンドを実⾏し、その変更を現在 の master (あるいは develop などの) ブランチの先端につなげます。うまくいけば、master ブランチをそのまま前に進めてることでプロジェクトの歴史を直線的に進めることができ ます。 あるブランチの作業を別のブランチに移すための⼿段として、他にチェリーピック (つ まみぐい) という⽅法があります。Git におけるチェリーピックとは、コミット単位での リベースのようなものです。あるコミットによって変更された内容をパッチとして受け取 り、それを現在のブランチに再適⽤します。トピックブランチでいくつかコミットしたう ちのひとつだけを統合したい場合、あるいはトピックブランチで⼀回だけコミットしたけ れどそれをリベースではなくチェリーピックで取り込みたい場合などにこの⽅法を使⽤し ます。図 5-26 のようなプロジェクトを例にとって考えましょう。 図 5.26: チェリーピック前の歴史 コミット e43a6 を master ブランチに取り込むには、次のようにします。 $ git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf Finished one cherry-pick. [master]: created a0a41a9: "More friendly message when locking the index fails." 3 files changed, 17 insertions(+), 3 deletions(-) これは e43a6 と同じ内容の変更を施しますが、コミットの SHA-1 値は新しくなりま す。適⽤した⽇時が異なるからです。これで、歴史は図 5-27 のように変わりました。 あとは、このトピックブランチを削除すれば取り込みたくない変更を消してしまうこと ができます。 152 Scott Chacon Pro Git 5.3節 プロジェクトの運営 図 5.27: トピックブランチのコミットをチェリーピックした後の歴史 5.3.6 リリース用のタグ付け いよいよリリースする時がきました。おそらく、後からいつでもこのリリースを取得で きるようにタグを打っておくことになるでしょう。新しいタグを打つ⽅法は第2章 で説明 しました。タグにメンテナの署名を⼊れておきたい場合は、このようにします。 $ git tag -s v1.5 -m 'my signed 1.5 tag' You need a passphrase to unlock the secret key for user: "Scott Chacon <[email protected]>" 1024-bit DSA key, ID F721C45A, created 2009-02-09 タグに署名した場合、署名に使⽤した PGP 鍵ペアの公開鍵をどのようにして配布する かが問題になるかもしれません。Git 開発プロジェクトのメンテナ達がこの問題をどのよ うに解決したかというと、⾃分たちの公開鍵を blob としてリポジトリに含め、それを直 接指すタグを追加することにしました。この⽅法を使うには、まずどの鍵を使うかを決め るために gpg --list-keys を実⾏します。 $ gpg --list-keys /Users/schacon/.gnupg/pubring.gpg --------------------------------pub 1024D/F721C45A 2009-02-09 [expires: 2010-02-09] uid Scott Chacon <[email protected]> sub 2048g/45D02282 2009-02-09 [expires: 2010-02-09] 鍵を直接 Git データベースにインポートするには、鍵をエクスポートしてそれをパイプ で git hash-object に渡します。これは、鍵の中⾝を新しい blob として Git に書き込み、 その blob の SHA-1 を返します。 $ gpg -a --export F721C45A | git hash-object -w --stdin 659ef797d181633c87ec71ac3f9ba29fe5775b92 153 第5章 Git での分散作業 Scott Chacon Pro Git 鍵の中⾝を Git に取り込めたので、この鍵を直接指定するタグを作成できるようになり ました。hash-object コマンドで知った SHA-1 値を指定すればいいのです。 $ git tag -a maintainer-pgp-pub 659ef797d181633c87ec71ac3f9ba29fe5775b92 git push --tags を実⾏すると、maintainer-pgp-pub タグをみんなと共有できるようになり ます。誰かがタグを検証したい場合は、あなたの PGP 鍵が⼊った blob をデータベース から直接プルで取得し、それを PGP にインポートすればいいのです。 $ git show maintainer-pgp-pub | gpg --import この鍵をインポートした⼈は、あなたが署名したすべてのタグを検証できるようになり ます。タグのメッセージに検証⼿順の説明を含めておけば、git show <tag> でエンドユー ザー向けに詳しい検証⼿順を⽰すことができます。 5.3.7 ビルド番号の生成 Git では、コミットごとに ʻv123ʼ のような単調な番号を振っていくことはありませ ん。もし特定のコミットに対して⼈間がわかりやすい名前がほしければ、そのコミット に対して git describe を実⾏します。Git は、そのコミットに最も近いタグの名前とその タグからのコミット数、そしてそのコミットの SHA-1 値の⼀部を使った名前を作成しま す。 $ git describe master v1.6.2-rc1-20-g8c5b85c これで、スナップショットやビルドを公開するときにわかりやすい名前をつけられるよ うになります。実際、Git そのもののソースコードを Git リポジトリからクローンしてビ ルドすると、git --version が返す結果はこの形式になります。タグが打たれているコミッ トを直接指定した場合は、タグの名前が返されます。 git describe コマンドは注釈付きのタグ (-a あるいは -s フラグをつけて作成したタグ) を 使います。したがって、git describe を使うならリリースタグは注釈付きのタグとしなけ ればなりません。そうすれば、describe したときにコミットの名前を適切につけること ができます。この⽂字列を checkout コマンドや show コマンドでの対象の指定に使うこ ともできますが、これは末尾にある SHA-1 値の省略形に依存しているので将来にわたっ てずっと使えるとは限りません。たとえば Linux カーネルは、最近 SHA-1 オブジェクト の⼀意性を確認するための⽂字数を 8 ⽂字から 10 ⽂字に変更しました。そのため、古い git describe の出⼒での名前はもはや使えません。 5.3.8 リリースの準備 実際にリリースするにあたって⾏うであろうことのひとつに、最新のスナップショット のアーカイブを作るという作業があります。Git を使っていないというかわいそうな⼈た 154 Scott Chacon Pro Git 5.3節 プロジェクトの運営 ちにもコードを提供するために。その際に使⽤するコマンドは git archive です。 $ git archive master --prefix='project/' | gzip > `git describe master`.tar.gz $ ls *.tar.gz v1.6.2-rc1-20-g8c5b85c.tar.gz tarball を開けば、プロジェクトのディレクトリの下に最新のスナップショットが得ら れます。まったく同じ⽅法で zip アーカイブを作成することもできます。この場合は git archive で --format=zip オプションを指定します。 $ git archive master --prefix='project/' --format=zip > `git describe master`.zip これで、あなたのプロジェクトのリリース⽤にすてきな tarball と zip アーカイブがで きあがりました。これをウェブサイトにアップロードするなりメールで送ってあげるなり しましょう。 5.3.9 短いログ そろそろメーリングリストにメールを送り、プロジェクトに何が起こったのかをみんな に知らせてあげましょう。前回のリリースから何が変わったのかの変更履歴を⼿軽に取得 するには git shortlog コマンドを使います。これは、指定した範囲のすべてのコミットの まとめを出⼒します。たとえば、直近のリリースの名前が v1.0.1 だった場合は、次のよ うにすると前回のリリース以降のすべてのコミットの概要が得られます。 $ git shortlog --no-merges master --not v1.0.1 Chris Wanstrath (8): Add support for annotated tags to Grit::Tag Add packed-refs annotated tag support. Add Grit::Commit#to_patch Update version and History.txt Remove stray `puts` Make ls_tree ignore nils Tom Preston-Werner (4): fix dates in history dynamic version method Version bump to 1.0.2 Regenerated gemspec for version 1.0.2 v1.0.1 以降のすべてのコミットの概要が、作者別にまとめて得られました。これをメー リングリストに投稿するといいでしょう。 155 第5章 Git での分散作業 Scott Chacon Pro Git 5.4 まとめ Git を使っているプロジェクトにコードを提供したり、⾃分のプロジェクトに他のユー ザーからのコードを取り込んだりといった作業を安⼼してこなせるようになりましたね。 おめでとうございます。Git を使いこなせる開発者の仲間⼊りです! 次の章では、複雑な 状況に対応するためのより強⼒なツールやヒントを学びます。これであなたは真の Git マ スターとなることでしょう。 156 第6章 Git のさまざまなツール Git を使ったソースコード管理のためのリポジトリの管理や保守について、⽇々使⽤す るコマンドやワークフローの⼤半を⾝につけました。ファイルの追跡やコミットといった 基本的なタスクをこなせるようになっただけではなくステージングエリアの威⼒もいか せるようになりました。また気軽にトピックブランチを切ってマージする⽅法も知りまし た。 では、Git の⾮常に強⼒な機能の数々をさらに探っていきましょう。⽇々の作業でこれ らを使うことはあまりありませんが、いつかは必要になるかもしれません。 6.1 リビジョンの選択 Git で特定のコミットやコミットの範囲を指定するにはいくつかの⽅法があります。明 ⽩なものばかりではありませんが、知っておくと役⽴つでしょう。 6.1.1 単一のリビジョン SHA-1 ハッシュを指定すれば、コミットを明確に参照することができます。しかしそ れ以外にも、より⼈間にやさしい⽅式でコミットを参照することもできます。このセク ションでは単⼀のコミットを参照するためのさまざまな⽅法の概要を説明します。 6.1.2 SHA の短縮形 Git は、最初の数⽂字をタイプしただけであなたがどのコミットを指定したいのかを汲 み取ってくれます。条件は、SHA-1 の最初の 4 ⽂字以上を⼊⼒していることと、それで ひとつのコミットが特定できる (現在のリポジトリに、⼊⼒した⽂字ではじまる SHA-1 のコミットがひとつしかない) ことです。 あるコミットを指定するために git log コマンドを実⾏し、とある機能を追加したコ ミットを⾒つけました。 $ git log commit 734713bc047d87bf7eac9674765ae793478c50d3 Author: Scott Chacon <[email protected]> Date: Fri Jan 2 18:32:33 2009 -0800 157 第6章 Git のさまざまなツール Scott Chacon Pro Git fixed refs handling, added gc auto, updated tests commit d921970aadf03b3cf0e71becdaab3147ba71cdef Merge: 1c002dd... 35cfb2b... Author: Scott Chacon <[email protected]> Date: Thu Dec 11 15:08:43 2008 -0800 Merge commit 'phedders/rdocs' commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b Author: Scott Chacon <[email protected]> Date: Thu Dec 11 14:58:32 2008 -0800 added some blame and merge stuff 探していたのは、1c002dd.... で始まるコミットです。git show でこのコミットを⾒ると きは、次のどのコマンドでも同じ結果になります (短いバージョンで、重複するコミット はないものとします)。 $ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b $ git show 1c002dd4b536e7479f $ git show 1c002d ⼀意に特定できる範囲での SHA-1 の短縮形を Git に⾒つけさせることもできます。git log コマンドで --abbrev-commit を指定すると、コミットを⼀意に特定できる範囲の省略形 で出⼒します。デフォルトでは 7 ⽂字ぶん表⽰しますが、それだけで SHA-1 を特定でき ない場合はさらに⻑くなります。 $ git log --abbrev-commit --pretty=oneline ca82a6d changed the version number 085bb3b removed unnecessary test code a11bef0 first commit ひとつのプロジェクト内での⼀意性を確保するには、普通は 8 ⽂字から 10 ⽂字もあれ ば⼗分すぎることでしょう。最も⼤規模な Git プロジェクトのひとつである Linux カー ネルの場合は、40 ⽂字のうち先頭の 12 ⽂字を指定しないと⼀意性を確保できません。 6.1.3 SHA-1 に関するちょっとしたメモ 「リポジトリ内のふたつのオブジェクトがたまたま同じ SHA-1 ハッシュ値を持ってし まったらどうするの?」と⼼配する⼈も多いでしょう。実際、どうなるのでしょう? 158 Scott Chacon Pro Git 6.1節 リビジョンの選択 すでにリポジトリに存在するオブジェクトと同じ SHA-1 値を持つオブジェクトをコ ミットしてした場合、Git はすでにそのオブジェクトがデータベースに格納されているも のと判断します。そのオブジェクトを後からどこかで取得しようとすると、常に最初のオ ブジェクトのデータが⼿元にやってきます (訳注: つまり、後からコミットした内容は存 在しないことになってしまう)。 しかし、そんなことはまず起こりえないということを知っておくべきでしょう。SHA-1 ダイジェストの⼤きさは 20 バイト (160 ビット) です。ランダムなハッシュ値がつけられ た中で、たった⼀つの衝突が 50% の確率で発⽣するために必要なオブジェクトの数は約 280 となります (衝突の可能性の計算式は p = n(n−1) 2 × 1 2160 です)。280 は、ほぼ 1.2×1024 、つまり⼀兆⼆千億のそのまた⼀兆倍です。これは、地球上にあるすべての砂粒の数の千 ⼆百倍にあたります。 SHA-1 の衝突を⾒るにはどうしたらいいのか、ひとつの例をごらんに⼊れましょう。 地球上の⼈類 65 億⼈が全員プログラムを書いていたとします。そしてその全員が、 Linux カーネルのこれまでの開発履歴 (100 万の Git オブジェクト) と同等のコードを⼀ 秒で書き上げ、⾺⿅でかい単⼀の Git リポジトリにプッシュしていくとします。これを 五年間続けたとして、SHA-1 オブジェクトの衝突がひとつでも発⽣する可能性がやっと 50% になります。それよりも「あなたの所属する開発チームの全メンバーが、同じ夜に それぞれまったく無関係の事件で全員オオカミに殺されてしまう」可能性のほうがよっぽ ど⾼いことでしょう。 6.1.4 ブランチの参照 特定のコミットを参照するのに⼀番直感的なのは、そのコミットを指すブランチがある 場合です。コミットオブジェクトや SHA-1 値を指定する場⾯ではどこでも、その代わり にブランチ名を指定することができます。たとえば、あるブランチ上の最新のコミットを 表⽰したい場合は次のふたつのコマンドが同じ意味となります (topic1 ブランチが ca82a6d を指しているものとします)。 $ git show ca82a6dff817ec66f44342007202690a93763949 $ git show topic1 あるブランチがいったいどの SHA を指しているのか、あるいはその他の例の内容が結 局のところどの SHA に⾏き着くのかといったことを知るには、Git の調査⽤ツールであ る rev-parse を使います。こういった調査⽤ツールのより詳しい情報は第9章 で説明しま す。rev-parse は低レベルでの操作⽤のコマンドであり、⽇々の操作で使うためのものでは ありません。しかし、今実際に何が起こっているのかを知る必要があるときなどには便利 です。ブランチ上で rev-parse を実⾏すると、このようになります。 $ git rev-parse topic1 ca82a6dff817ec66f44342007202690a93763949 159 第6章 Git のさまざまなツール Scott Chacon Pro Git 6.1.5 参照ログの短縮形 あなたがせっせと働いている間に Git が裏でこっそり⾏っていることのひとつが、参照 ログ (reflog) の管理です。これは、HEAD とブランチの参照が過去数ヶ⽉間どのように 動いてきたかをあらわすものです。 参照ログを⾒るには git reflog を使います。 $ git reflog 734713b HEAD@{0}: commit: fixed refs handling, added gc auto, updated d921970 HEAD@{1}: merge phedders/rdocs: Merge made by recursive. 1c002dd HEAD@{2}: commit: added some blame and merge stuff 1c36188 HEAD@{3}: rebase -i (squash): updating HEAD 95df984 HEAD@{4}: commit: # This is a combination of two commits. 1c36188 HEAD@{5}: rebase -i (squash): updating HEAD 7e05da5 HEAD@{6}: rebase -i (pick): updating HEAD 何らかの理由でブランチの先端が更新されるたびに、Git はその情報をこの⼀時履歴に 格納します。そして、このデータを使って過去のコミットを指定することもできます。リ ポジトリの HEAD の五つ前の状態を知りたい場合は、先ほど⾒た reflog の出⼒のように @{n} 形式で参照することができます。 $ git show HEAD@{5} この構⽂を使うと、指定した期間だけさかのぼったときに特定のブランチがどこを指し ていたかを知ることもできます。たとえば master ブランチの昨⽇の状態を知るには、この ようにします。 $ git show master@{yesterday} こうすると、そのブランチの先端が昨⽇どこを指していたかを表⽰します。この技が使 えるのは参照ログにデータが残っている間だけなので、直近数ヶ⽉よりも前のコミットに ついては使うことができません。 参照ログの情報を git log の出⼒⾵の表記で⾒るには git log -g を実⾏します。 $ git log -g master commit 734713bc047d87bf7eac9674765ae793478c50d3 Reflog: master@{0} (Scott Chacon <[email protected]>) Reflog message: commit: fixed refs handling, added gc auto, updated Author: Scott Chacon <[email protected]> Date: 160 Fri Jan 2 18:32:33 2009 -0800 Scott Chacon Pro Git 6.1節 リビジョンの選択 fixed refs handling, added gc auto, updated tests commit d921970aadf03b3cf0e71becdaab3147ba71cdef Reflog: master@{1} (Scott Chacon <[email protected]>) Reflog message: merge phedders/rdocs: Merge made by recursive. Author: Scott Chacon <[email protected]> Date: Thu Dec 11 15:08:43 2008 -0800 Merge commit 'phedders/rdocs' 参照ログの情報は、完全にローカルなものであることに気をつけましょう。これは、あ なた⾃⾝が⾃分のリポジトリで何をしたのかを⽰す記録です。つまり、同じリポジトリを コピーした別の⼈の参照ログとは異なる内容になります。また、最初にリポジトリをク ローンした直後の参照ログは空となります。まだリポジトリ上であなたが何もしていない からです。git show HEAD@{2.months.ago} が動作するのは、少なくとも⼆ヶ⽉以上前にその リポジトリをクローンした場合のみで、もしつい 5 分前にクローンしたばかりなら何も 結果を返しません。 6.1.6 家系の参照 コミットを特定する⽅法として他によく使われるのが、その家系をたどっていく⽅法で す。参照の最後に ˆ をつけると、Git はそれを「指定したコミットの親」と解釈します。 あなたのプロジェクトの歴史がこのようになっていたとしましょう。 $ git log --pretty=format:'%h %s' --graph * 734713b fixed refs handling, added gc auto, updated tests * d921970 Merge commit 'phedders/rdocs' |\ | * 35cfb2b Some rdoc changes * | 1c002dd added some blame and merge stuff |/ * 1c36188 ignore *.gem * 9b29157 add open3_detach to gemspec file list 直前のコミットを⾒るには HEADˆ を指定します。これは 『HEAD の親』 という意味に なります。 $ git show HEAD^ commit d921970aadf03b3cf0e71becdaab3147ba71cdef Merge: 1c002dd... 35cfb2b... Author: Scott Chacon <[email protected]> 161 第6章 Git のさまざまなツール Date: Scott Chacon Pro Git Thu Dec 11 15:08:43 2008 -0800 Merge commit 'phedders/rdocs' ˆ の後に数字を指定することもできます。たとえば d921970ˆ2 は 『d921970 の⼆番⽬の 親』 という意味になります。これが役⽴つのはマージコミット (親が複数存在する) のと きくらいでしょう。最初の親はマージを実⾏したときにいたブランチとなり、⼆番⽬の親 は取り込んだブランチ上のコミットとなります。 $ git show d921970^ commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b Author: Scott Chacon <[email protected]> Date: Thu Dec 11 14:58:32 2008 -0800 added some blame and merge stuff $ git show d921970^2 commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548 Author: Paul Hedderly <[email protected]> Date: Wed Dec 10 22:22:03 2008 +0000 Some rdoc changes 家系の指定⽅法としてもうひとつよく使うのが ~ です。これも最初の親を指します。 つまり HEAD~ と HEADˆ は同じ意味になります。違いが出るのは、数字を指定したときで す。HEAD~2 は 『最初の親の最初の親』 つまり 『祖⽗⺟』 という意味になります。指定し た数だけ、順に最初の親をさかのぼっていくことになります。たとえば、先ほど⽰したよ うな歴史上では HEAD~3 は次のようになります。 $ git show HEAD~3 commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d Author: Tom Preston-Werner <[email protected]> Date: Fri Nov 7 13:47:59 2008 -0500 ignore *.gem これは HEADˆˆˆ のようにあらわすこともできます。これは「最初の親の最初の親の最初 の親」という意味になります。 162 Scott Chacon Pro Git 6.1節 リビジョンの選択 $ git show HEAD^^^ commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d Author: Tom Preston-Werner <[email protected]> Date: Fri Nov 7 13:47:59 2008 -0500 ignore *.gem これらふたつの構⽂を組み合わせることもできます。直近の参照 (マージコミットだっ たとします) の⼆番⽬の親を取得するには HEAD~3ˆ2 などとすればいいのです。 6.1.7 コミットの範囲指定 個々のコミットを指定できるようになったので、次はコミットの範囲を指定する⽅法を 覚えていきましょう。これは、ブランチをマージするときに便利です。たくさんのブラン チを持っている場合など「で、このブランチの作業のなかでまだメインブランチにマージ していないのはどれだったっけ?」といった疑問に答えるために範囲指定を使えます。 ダブルドット 範囲指定の⽅法としてもっとも⼀般的なのが、ダブルドット構⽂です。これは、ひとつ のコミットからはたどれるけれどもうひとつのコミットからはたどれないというコミット の範囲を Git に調べさせるものです。図 6-1 のようなコミット履歴を例に考えましょう。 図 6.1: 範囲指定選択用の歴史の例 experiment ブランチの内容のうち、まだ master ブランチにマージされていないものを 調べることになりました。対象となるコミットのログを⾒るには、Git に master..experiment と指⽰します。これは 『experiment からはたどれるけれど、master からはたどれない すべてのコミット』 という意味です。説明を短く簡潔にするため、実際のログの出⼒の かわりに上の図の中でコミットオブジェクトをあらわす⽂字を使うことにします。 $ git log master..experiment D C もし逆に、master には存在するけれども experiment には存在しないすべてのコミッ トが知りたいのなら、ブランチ名を逆にすればいいのです。experiment..master とすれ ば、master のすべてのコミットのうち experiment からたどれないものを取得できます。 163 第6章 Git のさまざまなツール Scott Chacon Pro Git $ git log experiment..master F E これは、experiment ブランチを最新の状態に保つために何をマージしなければならない のかを知るのに便利です。もうひとつ、この構⽂をよく使う例としてあげられるのが、こ れからリモートにプッシュしようとしている内容を知りたいときです。 $ git log origin/master..HEAD このコマンドは、現在のブランチ上でのコミットのうち、リモート origin の master ブ ランチに存在しないものをすべて表⽰します。現在のブランチが origin/master を追跡し ているときに git push を実⾏すると、git log origin/master..HEAD で表⽰されたコミットが サーバーに転送されます。この構⽂で、どちらか⽚⽅を省略することもできます。その場 合、Git は省略したほうを HEAD とみなします。たとえば、git log origin/master.. と⼊ ⼒すると先ほどの例と同じ結果が得られます。Git は、省略した側を HEAD に置き換え て処理を進めるのです。 複数のポイント ダブルドット構⽂は、とりあえず使うぶんには便利です。しかし、⼆つよりもっと多く のブランチを指定してリビジョンを特定したいこともあるでしょう。複数のブランチの中 から現在いるブランチには存在しないコミットを⾒つける場合などです。Git でこれを⾏ うには ˆ ⽂字を使うか、あるいはそこからたどりつけるコミットが不要な参照の前に -not をつけます。これら三つのコマンドは、同じ意味となります。 $ git log refA..refB $ git log ^refA refB $ git log refB --not refA これらの構⽂が便利なのは、⼆つよりも多くの参照を使って指定できるというところ です。ダブルドット構⽂では⼆つの参照しか指定できませんでした。たとえば、refA と refB のどちらかからはたどれるけれども refC からはたどれないコミットを取得したい場 合は、次のいずれかを実⾏します。 $ git log refA refB ^refC $ git log refA refB --not refC この⾮常に強⼒なリビジョン問い合わせシステムを使えば、今あなたのブランチに何が あるのかを知るのに⾮常に役⽴つことでしょう。 164 Scott Chacon Pro Git 6.2節 対話的なステージング トリプルドット 範囲指定選択の主な構⽂であとひとつ残っているのがトリプルドット構⽂です。これ は、ふたつの参照のうちどちらか⼀⽅からのみたどれるコミット (つまり、両⽅からたど れるコミットは含まない) を指定します。図 6-1 で⽰したコミット履歴の例を振り返って みましょう。master あるいは experiment に存在するコミットのうち、両⽅に存在するもの を除いたコミットを知りたい場合は次のようにします。 $ git log master...experiment F E D C これは通常の log の出⼒と同じですが、これら四つのコミットについての情報しか表⽰ しません。表⽰順は、従来どおりコミット⽇時順となります。 この場合に log コマンドでよく使⽤するスイッチが --left-right です。このスイッチ は、それぞれのコミットがどちら側に存在するのかを表⽰します。これを使うとデータを より活⽤しやすくなるでしょう。 $ git log --left-right master...experiment < F < E > D > C これらのツールを使えば、より簡単に「どれを調べたいのか」を Git に伝えられるよう になります。 6.2 対話的なステージング Git には、コマンドラインでの作業をしやすくするためのスクリプトがいくつか付属し ています。ここでは、対話コマンドをいくつか紹介しましょう。これらを使うと、コミッ トの内容に細⼯をして特定のコミットだけとかファイルの中の⼀部だけとかを含めるよ うにすることが簡単にできるようになります。⼤量のファイルを変更した後に、それを ひとつの⾺⿅でかいコミットにしてしまうのではなくテーマごとの複数のコミットに分 けて処理したい場合などに⾮常に便利です。このようにして各コミットを論理的に独⽴ した状態にしておけば、同僚によるレビューも容易になります。git add に -i あるいは -interactive というオプションをつけて実⾏すると、Git は対話シェルモードに移⾏し、こ のように表⽰されます。 165 第6章 Git のさまざまなツール Scott Chacon Pro Git $ git add -i staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> このコマンドは、ステージングエリアに関する情報を違った観点で表⽰します。git status で得られる情報と基本的には同じですが、より簡潔で有益なものとなっています。 ステージした変更が左側、そしてステージしていない変更が右側に表⽰されます。 Commands セクションでは、さまざまなことができるようになっています。ファイル をステージしたりステージングエリアから戻したり、ファイルの⼀部だけをステージした りまだ追跡されていないファイルを追加したり、あるいは何がステージされたのかを diff で⾒たりといったことが可能です。 6.2.1 ファイルのステージとその取り消し What now> プロンプトで 2 または u と⼊⼒すると、どのファイルをステージするかを聞い てきます。 What now> 2 staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>> TODO と index.html をステージするには、その番号を⼊⼒します。 Update>> 1,2 staged unstaged path * 1: unchanged +0/-1 TODO * 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>> ファイル名の横に * がついていれば、そのファイルがステージ対象として選択されたこ 166 Scott Chacon Pro Git 6.2節 対話的なステージング とを意味します。Update>> プロンプトで何も⼊⼒せずに Enter を押すと、選択されたすべ てのファイルを Git がステージします。 Update>> updated 2 paths *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 1 staged unstaged path 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb TODO と index.html がステージされ、simplegit.rb はまだステージされていないまま です。ここで仮に TODO ファイルのステージを取り消したくなったとしたら、3 あるい は r (revert の r) を選択します。 *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 3 staged unstaged path 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> 1 staged unstaged path * 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> [enter] reverted one path もう⼀度 Git のステータスを⾒ると、TODO ファイルのステージが取り消されている ことがわかります。 *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help 167 第6章 Git のさまざまなツール Scott Chacon Pro Git What now> 1 staged 1: unchanged 2: +1/-1 3: unchanged unstaged path +0/-1 TODO nothing index.html +5/-1 lib/simplegit.rb ステージした変更の diff を⾒るには、6 あるいは d (diff の d) を使⽤します。このコマ ンドは、ステージしたファイルの⼀覧を表⽰します。その中から、ステージされた diff を ⾒たいファイルを選択します。これは、コマンドラインで git diff --cached を使⽤するの と同じようなことです。 *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 6 staged 1: +1/-1 unstaged path nothing index.html Review diff>> 1 diff --git a/index.html b/index.html index 4d07108..4335f49 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ Date Finder <p id="out">...</p> -<div id="footer">contact : [email protected]</div> +<div id="footer">contact : [email protected]</div> <script type="text/javascript"> これらの基本的なコマンドを使えば、ステージングエリアでの対話的な追加モードを多 少簡単に扱えるようになるでしょう。 6.2.2 パッチのステージ Git では、ファイルの特定の箇所だけをステージして他の部分はそのままにしておくと いうこともできます。たとえば、simplegit.rb のふたつの部分を変更したけれど、そのう ちの⼀⽅だけをステージしたいという場合があります。Git なら、そんなことも簡単で す。対話モードのプロンプトで 5 あるいは p (patch の p) と⼊⼒しましょう。Git は、ど のファイルを部分的にステージしたいのかを聞いてきます。その後、選択したファイルの それぞれについて diff のハンクを順に表⽰し、ステージするかどうかをひとつひとつたず ねます。 168 Scott Chacon Pro Git 6.2節 対話的なステージング diff --git a/lib/simplegit.rb b/lib/simplegit.rb index dd5ecc4..57399e0 100644 --- a/lib/simplegit.rb +++ b/lib/simplegit.rb @@ -22,7 +22,7 @@ class SimpleGit end def log(treeish = 'master') - command("git log -n 25 #{treeish}") + command("git log -n 30 #{treeish}") end def blame(path) Stage this hunk [y,n,a,d,/,j,J,g,e,?]? ここでは多くの選択肢があります。何ができるのかを⾒るには ? を⼊⼒しましょう。 Stage this hunk [y,n,a,d,/,j,J,g,e,?]? ? y - stage this hunk n - do not stage this hunk a - stage this and all the remaining hunks in the file d - do not stage this hunk nor any of the remaining hunks in the file g - select a hunk to go to / - search for a hunk matching the given regex j - leave this hunk undecided, see next undecided hunk J - leave this hunk undecided, see next hunk k - leave this hunk undecided, see previous undecided hunk K - leave this hunk undecided, see previous hunk s - split the current hunk into smaller hunks e - manually edit the current hunk ? - print help たいていは、y か n で各ハンクをステージするかどうかを指定していくでしょう。しか し、それ以外にも「このファイルの残りのハンクをすべてステージする」とか「このハン クをステージするかどうかの判断を先送りする」などというオプションも便利です。ある ファイルのひとつの箇所だけをステージして残りはそのままにした場合、ステータスの出 ⼒はこのようになります。 What now> 1 1: staged unstaged path unchanged +0/-1 TODO 169 第6章 Git のさまざまなツール 2: +1/-1 3: +1/-1 Scott Chacon Pro Git nothing index.html +4/-0 lib/simplegit.rb simplegit.rb のステータスがおもしろいことになっています。ステージされた⾏もあれ ば、ステージされていない⾏もあるという状態です。つまり、このファイルを部分的にス テージしたというわけです。この時点で対話的追加モードを抜けて git commit を実⾏する と、ステージした部分だけをコミットすることができます。 最後に、この対話的追加モードを使わずに部分的なステージを⾏いたい場合は、コマン ドラインから git add -p あるいは git add --patch を実⾏すれば同じことができます。 6.3 作業を隠す 何らかのプロジェクトの⼀員として作業している場合にありがちなのですが、ある作業 が中途半端な状態になっているときに、ブランチを切り替えてちょっとだけ別の作業をし たくなることがあります。中途半端な状態をコミットしてしまうのはいやなので、できれ ばコミットせずにしておいて後でその状態から作業を再開したいものです。そんなときに 使うのが git stash コマンドです。 これは、作業ディレクトリのダーティな状態 (追跡しているファイルのうち変更された もの、そしてステージされた変更) を受け取って未完了の作業をスタックに格納し、あと で好きなときに再度それを適⽤できるようにするものです。 6.3.1 自分の作業を隠す 例を⾒てみましょう。⾃分のプロジェクトでいくつかのファイルを編集し、その中のひ とつをステージしたとします。ここで git status を実⾏すると、ダーティな状態を確認す ることができます。 $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: index.html # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: lib/simplegit.rb # ここで別のブランチに切り替えることになりましたが、現在の作業内容はまだコミット したくありません。そこで、変更をいったん隠すことにします。新たにスタックに隠すに は git stash を実⾏します。 170 Scott Chacon Pro Git 6.3節 作業を隠す $ git stash Saved working directory and index state \ "WIP on master: 049d078 added the index file" HEAD is now at 049d078 added the index file (To restore them type "git stash apply") これで、作業ディレクトリはきれいな状態になりました。 $ git status # On branch master nothing to commit, working directory clean これで、簡単にブランチを切り替えて別の作業をできるようになりました。これまで の変更内容はスタックに格納されています。今までに格納した内容を⾒るには git stash list を使います。 $ git stash list stash@{0}: WIP on master: 049d078 added the index file stash@{1}: WIP on master: c264051 Revert "added file_size" stash@{2}: WIP on master: 21d80a5 added number to log この例では、以前にも⼆回ほど作業を隠していたようです。そこで、三種類の異なる作 業にアクセスできるようになっています。先ほど隠した変更を再度適⽤するには、stash コマンドの出⼒に書かれていたように git stash apply コマンドを実⾏します。それより もっと前に隠したものを適⽤したい場合は git stash apply stash@{2} のようにして名前を指 定することもできます。名前を指定しなければ、Git は直近に隠された変更を再適⽤しま す。 $ git stash apply # On branch master # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: index.html # modified: lib/simplegit.rb # Git がファイルを変更して、未コミットのファイルが先ほどスタックに隠したときと同 じ状態に戻ったことがわかるでしょう。今回は、作業ディレクトリがきれいな状態で変更 171 第6章 Git のさまざまなツール Scott Chacon Pro Git を書き戻しました。また、変更を隠したときと同じブランチに書き戻しています。しか し、隠した内容を再適⽤するためにこれらが必須条件であるというわけではありません。 あるブランチの変更を隠し、別のブランチに移動して移動先のブランチにそれを書き戻す こともできます。また、隠した変更を書き戻す際に、現在のブランチに未コミットの変更 があってもかまいません。もしうまく書き戻せなかった場合は、マージ時のコンフリクト と同じようになります。 さて、ファイルへの変更はもとどおりになりましたが、以前にステージしていたファイ ルはステージされていません。これを⾏うには、git stash apply コマンドに --index オプ ションをつけて実⾏し、変更のステージ処理も再適⽤するよう指⽰しなければなりませ ん。先ほどのコマンドのかわりにこれを実⾏すると、元の状態に戻ります。 $ git stash apply --index # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: index.html # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: lib/simplegit.rb # apply オプションは、スタックに隠した作業を再度適⽤するだけで、スタックにはまだ その作業が残ったままになります。スタックから削除するには、git stash drop に削除し たい作業の名前を指定して実⾏します。 $ git stash list stash@{0}: WIP on master: 049d078 added the index file stash@{1}: WIP on master: c264051 Revert "added file_size" stash@{2}: WIP on master: 21d80a5 added number to log $ git stash drop stash@{0} Dropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43) あるいは git stash pop を実⾏すれば、隠した内容を再適⽤してその後スタックからも削 除してくれます。 6.3.2 隠した内容の適用の取り消し 隠した変更を適⽤して何らかの作業をした後に、先ほどの適⽤を取り消してしまいたく なることもあるでしょう。そんなときに使えそうな stash unapply コマンドは git にはあり ませんが、同じような操作をすることはできます。適⽤した変更を表すパッチを取得し て、それを逆に適⽤すればいいのです。 172 Scott Chacon Pro Git 6.3節 作業を隠す $ git stash show -p stash@{0} | git apply -R 名前を指定しなければ、Git は直近に隠した変更を使うものとみなします。 $ git stash show -p | git apply -R 次の例のようにエイリアスを作れば、Git に stash-unapply コマンドを追加したのと事実 上同じことになります。 $ git config --global alias.stash-unapply '!git stash show -p | git apply -R' $ git stash $ #... 何か作業をして ... $ git stash-unapply 6.3.3 隠した変更からのブランチの作成 作業をいったん隠し、しばらくそのブランチで作業を続けていると、隠した内容を再適 ⽤するときに問題が発⽣する可能性があります。隠した後に何らかの変更をしたファイ ルに変更を再適⽤しようとすると、マージ時にコンフリクトが発⽣してそれを解決しな ければならなくなるでしょう。もう少しお⼿軽な⽅法で以前の作業を確認したい場合は git stash branch を実⾏します。このコマンドは、まず新しいブランチを作成し、作業をス タックに隠したときのコミットをチェックアウトし、スタックにある作業を再適⽤し、そ れに成功すればスタックからその作業を削除します。 $ git stash branch testchanges Switched to a new branch "testchanges" # On branch testchanges # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: index.html # # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # # modified: lib/simplegit.rb # Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359) 173 第6章 Git のさまざまなツール Scott Chacon Pro Git これを使うと、保存していた作業をお⼿軽に復元して新しいブランチで作業をすること ができます。 6.4 歴史の書き換え Git を使って作業をしていると、何らかの理由でコミットの歴史を書き換えたくなるこ とが多々あります。Git のすばらしい点のひとつは、何をどうするかの決断をぎりぎりま で先送りできることです。どのファイルをどのコミットに含めるのかは、ステージングエ リアの内容をコミットする直前まで変更することができますし、既に作業した内容でも stash コマンドを使えばまだ作業していないことにできます。また、すでにコミットして しまった変更についても、それを書き換えてまるで別の⽅法で⾏ったかのようにすること もできます。コミットの順序を変更したり、コミットメッセージやコミットされるファイ ルを変更したり、複数のコミットをひとつにまとめたりひとつのコミットを複数に分割し たり、コミットそのものをなかったことにしたり……といった作業を、変更内容を他のメ ンバーに公開する前ならいつでもすることができます。 このセクションでは、これらの便利な作業の⽅法について扱います。これで、あなたの コミットの歴史を思い通りに書き換えてから他の⼈と共有できるようになります。 6.4.1 直近のコミットの変更 直近のコミットを変更するというのは、歴史を書き換える作業のうちもっともよくある ものでしょう。直近のコミットに対して⼿を加えるパターンとしては、コミットメッセー ジを変更したりそのコミットで記録されるスナップショットを変更 (ファイルを追加・変 更あるいは削除) したりといったものがあります。 単に直近のコミットメッセージを変更したいだけの場合は⾮常にシンプルです。 $ git commit --amend これを実⾏するとテキストエディタが開きます。すでに直近のコミットメッセージが書 き込まれた状態になっており、それを変更することができます。変更を保存してエディタ を終了すると、変更後のメッセージを含む新しいコミットを作成して直近のコミットをそ れで置き換えます。 いったんコミットしたあとで、そこにさらにファイルを追加したり変更したりしたく なったとしましょう。「新しく作ったファイルを追加し忘れた」とかがありそうですね。 この場合の⼿順も基本的には同じです。ファイルを編集して git add したり追跡中のファ イルを git rm したりしてステージングエリアをお好みの状態にしたら、続いて git commit --amend を実⾏します。すると、現在のステージングエリアの状態を次回のコミット⽤の スナップショットにします。 この技を使う際には注意が必要です。この処理を⾏うとコミットの SHA-1 が変わるか らです。いわば、⾮常に⼩規模なリベースのようなものです。すでにプッシュしているコ ミットは書き換えないようにしましょう。 174 Scott Chacon Pro Git 6.4節 歴史の書き換え 6.4.2 複数のコミットメッセージの変更 さらに歴史をさかのぼったコミットを変更したい場合は、もう少し複雑なツールを使わ なければなりません。Git には歴史を修正するツールはありませんが、リベースツールを 使って⼀連のコミットを (別の場所ではなく) もともとあった場所と同じ HEAD につなげ るという⽅法を使うことができます。対話的なリベースツールを使えば、各コミットにつ いてメッセージを変更したりファイルを追加したりお望みの変更をすることができます。 対話的なリベースを⾏うには、git rebase に -i オプションを追加します。どこまでさかの ぼってコミットを書き換えるかを指⽰するために、どのコミットにリベースするかを指定 しなければなりません。 直近の三つのコミットメッセージあるいはそのいずれかを変更したくなった場合、変更 したい最古のコミットの親を git rebase -i の引数に指定します。ここでは HEAD~2ˆ あるい は HEAD~3 となります。直近の三つのコミットを編集しようと考えているのだから、~3 の ほうが覚えやすいでしょう。しかし、実際のところは四つ前 (変更したい最古のコミット の親) のコミットを指定していることに注意しましょう。 $ git rebase -i HEAD~3 これはリベースコマンドであることを認識しておきましょう。 HEAD~3..HEAD に含まれる すべてのコミットは、実際にメッセージを変更したか否かにかかわらずすべて書き換えら れます。すでに中央サーバーにプッシュしたコミットをここに含めてはいけません。含め てしまうと、同じ変更が別のバージョンで⾒えてしまうことになって他の開発者が混乱し ます。 このコマンドを実⾏すると、テキストエディタが開いてコミットの⼀覧が表⽰され、こ のようになります。 pick f7f3f6d changed my name a bit pick 310154e updated README formatting and added blame pick a5f4a0d added cat-file # Rebase 710f0f8..a5f4a0d onto 710f0f8 # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. 175 第6章 Git のさまざまなツール Scott Chacon Pro Git # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out このコミット⼀覧の表⽰順は、log コマンドを使ったときの通常の表⽰順とは逆になる ことに注意しましょう。log を実⾏すると、このようになります。 $ git log --pretty=format:"%h %s" HEAD~3..HEAD a5f4a0d added cat-file 310154e updated README formatting and added blame f7f3f6d changed my name a bit 逆順になっていますね。対話的なリベースを実⾏するとスクリプトが出⼒されるので、 それをあとで実⾏することになります。このスクリプトはコマンドラインで指定したコ ミット (HEAD~3) から始まり、それ以降のコミットを古い順に再現していきます。最新のも のからではなく古いものから表⽰されているのは、最初に再現するのがいちばん古いコ ミットだからです。 このスクリプトを編集し、⼿を加えたいコミットのところでスクリプトを停⽌させるよ うにします。そのためには、各コミットのうちスクリプトを停⽌させたいものについて 「pick」を「edit」に変更します。たとえば、三番⽬のコミットメッセージだけを変更し たい場合はこのようにファイルを変更します。 edit f7f3f6d changed my name a bit pick 310154e updated README formatting and added blame pick a5f4a0d added cat-file これを保存してエディタを終了すると、Git はそのリストの最初のコミットまで処理を 巻き戻し、次のようなメッセージとともにコマンドラインを返します。 $ git rebase -i HEAD~3 Stopped at 7482e0d... updated the gemspec to hopefully work better You can amend the commit now, with git commit --amend Once you’re satisfied with your changes, run git rebase --continue 176 Scott Chacon Pro Git 6.4節 歴史の書き換え この指⽰が、まさにこれからすべきことを教えてくれています。 $ git commit --amend と打ち込んでコミットメッセージを変更してからエディタを終了し、次に $ git rebase --continue を実⾏します。このコマンドはその他のふたつのコミットも⾃動的に適⽤するので、こ れで作業は終了です。複数⾏で「pick」を「edit」に変更した場合は、これらの作業を各 コミットについてくりかえすことになります。それぞれの場⾯で Git が停⽌するので、 amend でコミットを書き換えて continue で処理を続けます。 6.4.3 コミットの並べ替え 対話的なリベースで、コミットの順番を変更したり完全に消し去ってしまったりするこ ともできます。『added cat-file』 のコミットを削除して残りの⼆つのコミットの適⽤順 を反対にしたい場合は、リベーススクリプトを pick f7f3f6d changed my name a bit pick 310154e updated README formatting and added blame pick a5f4a0d added cat-file から pick 310154e updated README formatting and added blame pick f7f3f6d changed my name a bit のように変更します。これを保存してエディタを終了すると、Git はまずこれらのコ ミットの親までブランチを巻き戻してから 310154e を適⽤し、その次に f7f3f6d を適⽤して 停⽌します。これで、効率的にコミット順を変更して 『added cat-file』 のコミットは完 全に取り除くことができました。 6.4.4 コミットのまとめ ⼀連のコミット群をひとつのコミットにまとめて押し込んでしまうことも、対話的なリ ベースツールで⾏うことができます。リベースメッセージの中に、その⼿順が出⼒されて います。 177 第6章 Git のさまざまなツール Scott Chacon Pro Git # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out 「pick」や「edit」のかわりに「squash」を指定すると、Git はその変更と直前の変更 をひとつにまとめて新たなコミットメッセージを書き込めるようにします。つまり、これ らの三つのコミットをひとつのコミットにまとめたい場合は、スクリプトをこのように変 更します。 pick f7f3f6d changed my name a bit squash 310154e updated README formatting and added blame squash a5f4a0d added cat-file これを保存してエディタを終了すると、Git は三つの変更をすべて適⽤してからエディ タに戻るので、そこでコミットメッセージを変更します。 # This is a combination of 3 commits. # The first commit's message is: changed my name a bit # This is the 2nd commit message: updated README formatting and added blame # This is the 3rd commit message: added cat-file 178 Scott Chacon Pro Git 6.4節 歴史の書き換え これを保存すると、さきほどの三つのコミットの内容をすべて含んだひとつのコミット ができあがります。 6.4.5 コミットの分割 コミットの分割は、いったんコミットを取り消してから部分的なステージとコミットを 繰り返して⾏います。たとえば、先ほどの三つのコミットのうち真ん中のものを分割する ことになったとしましょう。『updated README formatting and added blame』 のコ ミットを、『updated README formatting』 と 『added blame』 のふたつに分割しま す。そのためには、rebase -i スクリプトを実⾏してそのコミットの指⽰を「edit」に変更 します。 pick f7f3f6d changed my name a bit edit 310154e updated README formatting and added blame pick a5f4a0d added cat-file 変更を保存してエディタを終了すると、Git はリストの最初のコミットの親まで処理を 巻き戻します。そして最初のコミット (f7f3f6d) と⼆番⽬のコミット (310154e) を適⽤して からコンソールに戻ります。コミットをリセットするには git reset HEADˆ を実⾏します。 これはコミット⾃体を取り消し、変更されたファイルはステージしていない状態にしま す。ここまでくれば、取り消された変更点から必要なものだけを選択してコミットするこ とができます。⼀連のコミットが終わったら、以下のようにgit rebase --continue を実⾏し ましょう。 $ git reset HEAD^ $ git add README $ git commit -m 'updated README formatting' $ git add lib/simplegit.rb $ git commit -m 'added blame' $ git rebase --continue Git はスクリプトの最後のコミット (a5f4a0d) を適⽤し、歴史はこのようになります。 $ git log -4 --pretty=format:"%h %s" 1c002dd added cat-file 9b29157 added blame 35cfb2b updated README formatting f3cc40e changed my name a bit 念のためにもう⼀度⾔いますが、この変更はリスト内のすべてのコミットの SHA を変 更します。すでに共有リポジトリにプッシュしたコミットは、このリストに表⽰させない ようにしましょう。 179 第6章 Git のさまざまなツール Scott Chacon Pro Git 6.4.6 最強のオプション: filter-branch 歴史を書き換える⽅法がもうひとつあります。これは、⼤量のコミットの書き換えを機 械的に⾏いたい場合 (メールアドレスを⼀括変更したりすべてのコミットからあるファイ ルを削除したりなど) に使うものです。そのためのコマンドが filter-branch です。これは 歴史を⼤規模にばさっと書き換えることができるものなので、プロジェクトを⼀般に公開 した後や書き換え対象のコミットを元にしてだれかが作業を始めている場合はまず使うこ とはありません。しかし、これは⾮常に便利なものでもあります。⼀般的な使⽤例をいく つか説明するので、それをもとにこの機能を使いこなせる場⾯を考えてみましょう。 全コミットからのファイルの削除 これは、相当よくあることでしょう。誰かが不注意で git add . をした結果、巨⼤なバ イナリファイルが間違えてコミットされてしまったとしましょう。これを何とか削除して しまいたいものです。あるいは、間違ってパスワードを含むファイルをコミットしてし まったとしましょう。このプロジェクトをオープンソースにしたいと思ったときに困りま す。filter-branch は、こんな場合に歴史全体を洗うために使うツールです。passwords.txt というファイルを歴史から完全に抹殺してしまうには、filter-branch の --tree-filter オプ ションを使います。 $ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21) Ref 'refs/heads/master' was rewritten --tree-filter オプションは、プロジェクトの各チェックアウトに対して指定したコマ ンドを実⾏し、結果を再コミットします。この場合は、すべてのスナップショットから passwords.txt というファイルを削除します。間違えてコミットしてしまったエディタの バックアップファイルを削除するには、git filter-branch --tree-filter "rm -f *~" HEAD のよ うに実⾏します。 Git がツリーを書き換えてコミットし、ブランチのポインタを末尾に移動させる様⼦が ごらんいただけるでしょう。この作業は、まずはテスト⽤ブランチで実⾏してから結果を よく吟味し、それから master ブランチに適⽤することをおすすめします。filter-branch をすべてのブランチで実⾏するには、このコマンドに --all を渡します。 サブディレクトリを新たなルートへ 別のソース管理システムからのインポートを終えた後、無意味なサブディレクトリ (trunk、tags など) が残っている状態を想定しましょう。すべてのコミットの trunk ディ レクトリを新たなプロジェクトルートとしたい場合にも、filter-branch が助けになりま す。 $ git filter-branch --subdirectory-filter trunk HEAD Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12) Ref 'refs/heads/master' was rewritten 180 Scott Chacon Pro Git 6.5節 Git によるデバッグ これで、新たなプロジェクトルートはそれまで trunk ディレクトリだった場所になりま す。Git は、このサブディレクトリに影響を及ぼさないコミットを⾃動的に削除します。 メールアドレスの一括変更 もうひとつよくある例としては、「作業を始める前に git config で名前とメールアドレ スを設定することを忘れていた」とか「業務で開発したプロジェクトをオープンソースに するにあたって、職場のメールアドレスをすべて個⼈アドレスに変更したい」などがあり ます。どちらの場合についても、複数のコミットのメールアドレスを⼀括で変更すること になりますが、これも filter-branch ですることができます。注意して、あなたのメールア ドレスのみを変更しなければなりません。そこで、--commit-filter を使います。 $ git filter-branch --commit-filter ' if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ]; then GIT_AUTHOR_NAME="Scott Chacon"; GIT_AUTHOR_EMAIL="[email protected]"; git commit-tree "$@"; else git commit-tree "$@"; fi' HEAD これで、すべてのコミットであなたのアドレスを新しいものに書き換えます。コミット にはその親の SHA-1 値が含まれるので、このコマンドは (マッチするメールアドレスが 存在するものだけではなく) すべてのコミットを書き換えます。 6.5 Git によるデバッグ Git には、プロジェクトで発⽣した問題をデバッグするためのツールも⽤意されていま す。Git はほとんどあらゆる種類のプロジェクトで使えるように設計されているので、こ のツールも⾮常に汎⽤的なものです。しかし、バグを⾒つけたり不具合の原因を探したり するための助けとなるでしょう。 6.5.1 ファイルの注記 コードのバグを追跡しているときに「それが、いつどんな理由で追加されたのか」が知 りたくなることがあるでしょう。そんな場合にもっとも便利なのが、ファイルの注記で す。これは、ファイルの各⾏について、その⾏を最後に更新したのがどのコミットかを表 ⽰します。もしコードの中の特定のメソッドにバグがあることを⾒つけたら、そのファイ ルを git blame しましょう。そうすれば、そのメソッドの各⾏がいつ誰によって更新され たのかがわかります。この例では、-L オプションを使って 12 ⾏⽬から 22 ⾏⽬までに出 ⼒を限定しています。 181 第6章 Git のさまざまなツール Scott Chacon Pro Git $ git blame -L 12,22 simplegit.rb ^4832fe2 (Scott Chacon 2008-03-15 10:31:28 -0700 12) ^4832fe2 (Scott Chacon 2008-03-15 10:31:28 -0700 13) ^4832fe2 (Scott Chacon 2008-03-15 10:31:28 -0700 14) ^4832fe2 (Scott Chacon 2008-03-15 10:31:28 -0700 15) 9f6560e4 (Scott Chacon 2008-03-17 21:52:20 -0700 16) 79eaf55d (Scott Chacon 2008-04-06 10:15:08 -0700 17) 9f6560e4 (Scott Chacon 2008-03-17 21:52:20 -0700 18) 9f6560e4 (Scott Chacon 2008-03-17 21:52:20 -0700 19) 42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 20) 42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 21) 42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 22) def show(tree = 'master') command("git show #{tree}") end def log(tree = 'master') command("git log #{tree}") end def blame(path) command("git blame #{path}") end 最初の項⽬は、その⾏を最後に更新したコミットの SHA-1 の⼀部です。次のふたつの 項⽬は、そのコミットから抽出した作者情報とコミット⽇時です。これで、いつ誰がその ⾏を更新したのかが簡単にわかります。それに続いて、⾏番号とファイルの中⾝が表⽰ されます。ˆ4832fe2 のコミットに関する⾏に注⽬しましょう。これらの⾏は、ファイルが 最初にコミットされたときのままであることを表します。このコミットはファイルがプロ ジェクトに最初に追加されたときのものであり、これらの⾏はそれ以降変更されていませ ん。これはちょっと⼾惑うかも知れません。Git では、これまで紹介してきただけで少な くとも三種類以上の意味で ˆ を使っていますからね。しかし、ここではそういう意味にな るのです。 Git のすばらしいところのひとつに、ファイルのリネームを明⽰的には追跡しないとい うことがあります。スナップショットだけを記録し、もしリネームされていたのなら暗黙 のうちにそれを検出します。この機能の興味深いところは、ファイルのリネームだけで なくコードの移動についても検出できるということです。git blame に -C を渡すと Git は そのファイルを解析し、別のところからコピーされたコード⽚がないかどうかを探しま す。最近私は GITServerHandler.m というファイルをリファクタリングで複数のファイルに 分割しました。そのうちのひとつが GITPackUpload.m です。ここで -C オプションをつけて GITPackUpload.m を調べると、コードのどの部分をどのファイルからコピーしたのかを知る ことができます。 $ git blame -C -L 141,153 GITPackUpload.m f344f58d GITServerHandler.m (Scott 2009-01-04 141) f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC f344f58d GITServerHandler.m (Scott 2009-01-04 143) { 70befddd GITServerHandler.m (Scott 2009-03-22 144) //NSLog(@"GATHER COMMI ad11ac80 GITPackUpload.m (Scott 2009-03-24 145) ad11ac80 GITPackUpload.m (Scott 2009-03-24 146) NSString *parentSha; ad11ac80 GITPackUpload.m (Scott 2009-03-24 147) GITCommit *commit = [g ad11ac80 GITPackUpload.m (Scott 2009-03-24 148) ad11ac80 GITPackUpload.m (Scott 2009-03-24 149) 182 //NSLog(@"GATHER COMMI Scott Chacon Pro Git ad11ac80 GITPackUpload.m 6.5節 Git によるデバッグ (Scott 2009-03-24 150) 56ef2caf GITServerHandler.m (Scott 2009-01-05 151) if(commit) { 56ef2caf GITServerHandler.m (Scott 2009-01-05 152) [refDict setOb 56ef2caf GITServerHandler.m (Scott 2009-01-05 153) これはほんとうに便利です。通常は、そのファイルがコピーされたときのコミットを知 ることになります。コピー先のファイルにおいて最初にその⾏をさわったのが、その内 容をコピーしてきたときだからです。Git は、その⾏が本当に書かれたコミットがどこで あったのかを (たとえ別のファイルであったとしても) 教えてくれるのです。 6.5.2 二分探索 ファイルの注記を使えば、その問題がどの時点で始まったのかを知ることができます。 何がおかしくなったのかがわからず、最後にうまく動作していたときから何⼗何百ものコ ミットが⾏われている場合などは、git bisect に頼ることになるでしょう。bisect コマン ドはコミットの歴史に対して⼆分探索を⾏い、どのコミットで問題が混⼊したのかを可能 な限り⼿早く⾒つけ出せるようにします。 ⾃分のコードをリリースして運⽤環境にプッシュしたあとに、バグ報告を受け取ったと 仮定しましょう。そのバグは開発環境では再現せず、なぜそんなことになるのか想像もつ きません。コードをよく調べて問題を再現させることはできましたが、何が悪かったのか がわかりません。こんな場合に、⼆分探索で原因を特定することができます。まず、git bisect start を実⾏します。そして次に git bisect bad を使って、現在のコミットが壊れた 状態であることをシステムに伝えます。次に、まだ壊れていなかったとわかっている直近 のコミットを git bisect good [good_commit] で伝えます。 $ git bisect start $ git bisect bad $ git bisect good v1.0 Bisecting: 6 revisions left to test after this [ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo Git は、まだうまく動いていたと指定されたコミット (v1.0) と現在の壊れたバージョン の間には 12 のコミットがあるということを検出しました。そして、そのちょうど真ん中 にあるコミットをチェックアウトしました。ここでテストを実⾏すれば、このコミットで 同じ問題が発⽣するかどうかがわかります。もし問題が発⽣したなら、実際に問題が混 ⼊したのはそれより前のコミットだということになります。そうでなければ、それ以降 のコミットで問題が混⼊したのでしょう。ここでは、問題が発⽣しなかったものとしま す。git bisect good で Git にその旨を伝え、旅を続けましょう。 $ git bisect good Bisecting: 3 revisions left to test after this [b047b02ea83310a70fd603dc8cd7a6cd13d15c04] secure this thing 183 第6章 Git のさまざまなツール Scott Chacon Pro Git また別のコミットがやってきました。先ほど調べたコミットと「壊れている」と伝えた コミットの真ん中にあるものです。ふたたびテストを実⾏し、今度はこのコミットで問題 が再現したものとします。それを Git に伝えるには git bisect bad を使います。 $ git bisect bad Bisecting: 1 revisions left to test after this [f71ce38690acf49c1f3c9bea38e09d82a5ce6014] drop exceptions table このコミットはうまく動きました。というわけで、問題が混⼊したコミットを特定す るための情報がこれですべて整いました。Git は問題が混⼊したコミットの SHA-1 を⽰ し、そのコミット情報とどのファイルが変更されたのかを表⽰します。これを使って、 いったい何が原因でバグが発⽣したのかを突き⽌めます。 $ git bisect good b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04 Author: PJ Hyett <[email protected]> Date: Tue Jan 27 14:48:32 2009 -0800 secure this thing :040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730 f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M config 原因がわかったら、作業を始める前に git bisect reset を実⾏して HEAD を作業前の状 態に戻さなければなりません。そうしないと⾯倒なことになってしまいます。 $ git bisect reset この強⼒なツールを使えば、何百ものコミットの中からバグの原因となるコミットを数 分で⾒つけだせるようになります。実際、プロジェクトが正常なときに 0 を返してどこ かおかしいときに 0 以外を返すスクリプトを⽤意しておけば、git bisect を完全に⾃動化 することもできます。まず、先ほどと同じく、壊れているコミットと正しく動作している コミットを指定します。これは bisect start コマンドで⾏うこともできます。まず最初に 壊れているコミット、そしてその後に正しく動作しているコミットを指定します。 $ git bisect start HEAD v1.0 $ git bisect run test-error.sh 184 Scott Chacon Pro Git 6.6節 サブモジュール こうすると、チェックアウトされたコミットに対して⾃動的に test-error.sh を実⾏ し、壊れる原因となるコミットを⾒つけ出すまで⾃動的に処理を続けます。make や make tests、その他⾃動テストを実⾏するためのプログラムなどをここで実⾏させることもで きます。 6.6 サブモジュール あるプロジェクトで作業をしているときに、プロジェクト内で別のプロジェクトを使わ なければならなくなることがよくあります。サードパーティが開発しているライブラリ や、⾃⾝が別途開発していて複数の親プロジェクトから利⽤しているライブラリなどがそ れにあたります。こういったときに出てくるのが「ふたつのプロジェクトはそれぞれ別の ものとして管理したい。だけど、⼀⽅を他⽅の⼀部としても使いたい」という問題です。 例を考えてみましょう。ウェブサイトを制作しているあなたは、Atom フィードを作成 することになりました。Atom ⽣成コードを⾃前で書くのではなく、ライブラリを使う ことに決めました。この場合、CPAN や gem などの共有ライブラリからコードをインク ルードするか、ソースコードそのものをプロジェクトのツリーに取り込むかのいずれかが 必要となります。ライブラリをインクルードする⽅式の問題は、ライブラリのカスタマイ ズが困難であることと配布が⾯倒になるということです。すべてのクライアントにそのラ イブラリを導⼊させなければなりません。コードをツリーに取り込む⽅式の問題は、⼿元 でコードに⼿を加えてしまうと本家の更新に追従しにくくなるということです。 Git では、サブモジュールを使ってこの問題に対応します。サブモジュールを使うと、 ある Git リポジトリを別の Git リポジトリのサブディレクトリとして扱うことができるよ うになります。これで、別のリポジトリをプロジェクト内にクローンしても⾃分のコミッ トは別管理とすることができるようになります。 6.6.1 サブモジュールの作り方 Rack ライブラリ (Ruby のウェブサーバーゲートウェイインターフェイス) を⾃分のプ ロジェクトに取り込むことになったとしましょう。⼿元で変更を加えるかもしれません が、本家で更新があった場合にはそれを取り込み続けるつもりです。まず最初にしなけれ ばならないことは、外部のリポジトリをサブディレクトリにクローンすることです。外部 のプロジェクトをサブモジュールとして追加するには git submodule add コマンドを使⽤し ます。 $ git submodule add git://github.com/chneukirchen/rack.git rack Initialized empty Git repository in /opt/subtest/rack/.git/ remote: Counting objects: 3181, done. remote: Compressing objects: 100% (1534/1534), done. remote: Total 3181 (delta 1951), reused 2623 (delta 1603) Receiving objects: 100% (3181/3181), 675.42 KiB | 422 KiB/s, done. Resolving deltas: 100% (1951/1951), done. これで、プロジェクト内の rack サブディレクトリに Rack プロジェクトが取り込まれ ました。このサブディレクトリに⼊って変更を加えたり、書き込み権限のあるリモートリ 185 第6章 Git のさまざまなツール Scott Chacon Pro Git ポジトリを追加してそこに変更をプッシュしたり、本家のリポジトリの内容を取得して マージしたり、さまざまなことができるようになります。サブモジュールを追加した直後 に git status を実⾏すると、⼆つのものが⾒られます。 $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # new file: .gitmodules # new file: rack # まず気づくのが .gitmodules ファイルです。この設定ファイルには、プロジェクトの URL とそれを取り込んだローカルサブディレクトリの対応が格納されています。 $ cat .gitmodules [submodule "rack"] path = rack url = git://github.com/chneukirchen/rack.git 複数のサブモジュールを追加した場合は、このファイルに複数のエントリが書き込まれ ます。このファイルもまた他のファイルと同様にバージョン管理下に置かれることに注意 しましょう。.gitignore ファイルと同じことです。プロジェクトの他のファイルと同様、 このファイルもプッシュやプルの対象となります。プロジェクトをクローンした⼈は、こ のファイルを使ってサブモジュールの取得元を知ることになります。 git status の出⼒に、もうひとつ rack というエントリが含まれています。これに対して git diff を実⾏すると、ちょっと興味深い結果が得られます。 $ git diff --cached rack diff --git a/rack b/rack new file mode 160000 index 0000000..08d709f --- /dev/null +++ b/rack @@ -0,0 +1 @@ +Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433 rack は作業ディレクトリ内にあるサブディレクトリですが、Git はそれがサブモジュー ルであるとみなし、あなたがそのディレクトリにいない限りその中⾝を追跡することは ありません。そのかわりに、Git はこのサブディレクトリを元のプロジェクトの特定のコ 186 Scott Chacon Pro Git 6.6節 サブモジュール ミットとして記録します。このサブディレクトリ内に変更を加えてコミットすると、親プ ロジェクト側で HEAD が変わったことを検知し、実際の作業内容をコミットとして記録 します。そうすることで、他の⼈がこのプロジェクトをクローンしたときに正しく環境を 作れるようになります。 ここがサブモジュールのポイントです。サブモジュールは、それがある場所の実際のコ ミットとして記録され、master やその他の参照として記録することはできません。 コミットすると、このようになります。 $ git commit -m 'first commit with submodule rack' [master 0550271] first commit with submodule rack 2 files changed, 4 insertions(+), 0 deletions(-) create mode 100644 .gitmodules create mode 160000 rack rack エントリのモードが 160000 となったことに注⽬しましょう。これは Git におけ る特別なモードで、サブディレクトリやファイルではなくディレクトリエントリとしてこ のコミットを記録したことを意味します。 rack ディレクトリを独⽴したプロジェクトとして扱い、ときどき親プロジェクトをアッ プデートして親プロジェクトの最新コミットにポインタを移動させることができます。す べての Git コマンドが、これらふたつのディレクトリで独⽴して使⽤可能です。 $ git log -1 commit 0550271328a0038865aad6331e620cd7238601bb Author: Scott Chacon <[email protected]> Date: Thu Apr 9 09:03:56 2009 -0700 first commit with submodule rack $ cd rack/ $ git log -1 commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433 Author: Christian Neukirchen <[email protected]> Date: Wed Mar 25 14:49:04 2009 +0100 Document version change 6.6.2 サブモジュールを含むプロジェクトのクローン ここでは、内部にサブモジュールを含むプロジェクトをクローンしてみます。すると、 サブモジュールを含むディレクトリは取得できますがその中にはまだ何もファイルが⼊っ ていません。 187 第6章 Git のさまざまなツール Scott Chacon Pro Git $ git clone git://github.com/schacon/myproject.git Initialized empty Git repository in /opt/myproject/.git/ remote: Counting objects: 6, done. remote: Compressing objects: 100% (4/4), done. remote: Total 6 (delta 0), reused 0 (delta 0) Receiving objects: 100% (6/6), done. $ cd myproject $ ls -l total 8 -rw-r-r-- 1 schacon drwxr-xr-x 2 schacon admin admin 3 Apr 9 09:11 README 68 Apr 9 09:11 rack $ ls rack/ $ rack ディレクトリは存在しますが、中⾝がからっぽです。ここで、ふたつのコマンドを 実⾏しなければなりません。まず git submodule init でローカルの設定ファイルを初期化 し、次に git submodule update でプロジェクトからのデータを取得し、親プロジェクトで指 定されている適切なコミットをチェックアウトします。 $ git submodule init Submodule 'rack' (git://github.com/chneukirchen/rack.git) registered for path 'rack' $ git submodule update Initialized empty Git repository in /opt/myproject/rack/.git/ remote: Counting objects: 3181, done. remote: Compressing objects: 100% (1534/1534), done. remote: Total 3181 (delta 1951), reused 2623 (delta 1603) Receiving objects: 100% (3181/3181), 675.42 KiB | 173 KiB/s, done. Resolving deltas: 100% (1951/1951), done. Submodule path 'rack': checked out '08d709f78b8c5b0fbeb7821e37fa53e69afcf433' これで、サブディレクトリ rack の中⾝が先ほどコミットしたときとまったく同じ状態 になりました。別の開発者が rack のコードを変更してコミットしたときにそれを取り込 んでマージするには、もう少し付け加えます。 $ git merge origin/master Updating 0550271..85a3eee Fast forward rack | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) [master*]$ git status # On branch master 188 Scott Chacon Pro Git 6.6節 サブモジュール # Changes not staged for commit: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: rack # このマージで、サブモジュールが指すポインタの位置が変わりました。しかしサブモ ジュールディレクトリ内のコードは更新されていません。つまり、作業ディレクトリ内で ダーティな状態になっています。 $ git diff diff --git a/rack b/rack index 6c5e70b..08d709f 160000 --- a/rack +++ b/rack @@ -1 +1 @@ -Subproject commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0 +Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433 これは、サブモジュールのポインタが指す位置と実際のサブモジュールディレクトリの 中⾝が異なるからです。これを修正するには、ふたたび git submodule update を実⾏しま す。 $ git submodule update remote: Counting objects: 5, done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 1), reused 2 (delta 0) Unpacking objects: 100% (3/3), done. From [email protected]:schacon/rack 08d709f..6c5e70b master -> origin/master Submodule path 'rack': checked out '6c5e70b984a60b3cecd395edd5b48a7575bf58e0' サブモジュールの変更をプロジェクトに取り込んだときには、毎回これをしなければな りません。ちょっと奇妙ですが、これでうまく動作します。 よくある問題が、開発者がサブモジュール内でローカルに変更を加えたけれどそれを公 開サーバーにプッシュしていないときに起こります。ポインタの指す先を⾮公開の状態 にしたまま、それを親プロジェクトにプッシュしてしまうと、他の開発者が git submodule update をしたときにサブモジュールが参照するコミットを⾒つけられなくなります。その コミットは最初の開発者の環境にしか存在しないからです。この状態になると、次のよう なエラーとなります。 189 第6章 Git のさまざまなツール Scott Chacon Pro Git $ git submodule update fatal: reference isn’t a tree: 6c5e70b984a60b3cecd395edd5b48a7575bf58e0 Unable to checkout '6c5e70b984a60b3cecd395edd5ba7575bf58e0' in submodule path 'rack' サブモジュールを最後に更新したのがいったい誰なのかを突き⽌めなければなりませ ん。 $ git log -1 rack commit 85a3eee996800fcfa91e2119372dd4172bf76678 Author: Scott Chacon <[email protected]> Date: Thu Apr 9 09:19:14 2009 -0700 added a submodule reference I will never make public. hahahahaha! 犯⼈がわかったら、メールで彼に怒鳴りつけてやりましょう。 6.6.3 親プロジェクト 時には、⼤規模なプロジェクトのサブディレクトリから今⾃分がいるチームに応じた組 み合わせを取得したくなることもあるでしょう。これは、CVS や Subversion から移⾏ した場合によくあることでしょう。モジュールを定義したりサブディレクトリのコレク ションを定義していたりといったかつてのワークフローをそのまま維持したいというよう な状況です。 Git でこれと同じことをするためのよい⽅法は、それぞれのサブディレクトリを別々の Git リポジトリにして、それらのサブモジュールとして含む親プロジェクトとなる Git リ ポジトリを作ることです。この⽅式の利点は、親プロジェクトのタグやブランチを活⽤し てプロジェクト間の関係をより細やかに定義できることです。 6.6.4 サブモジュールでの問題 しかし、サブモジュールを使っているとなにかしらちょっとした問題が出てくるもので す。まず、サブモジュールのディレクトリで作業をするときはいつも以上に注意深くなら なければなりません。git submodule update を実⾏すると、プロジェクトの特定のバージョ ンをチェックアウトしますが、それはブランチの中にあるものではありません。これを、 切り離された HEAD (detached HEAD) と呼びます。つまり、HEAD が何らかの参照で はなく直接特定のコミットを指している状態です。通常は、HEAD が切り離された状態 で作業をしようとは思わないでしょう。⼿元の変更が簡単に失われてしまうからです。最 初に submodule update し、作業⽤のブランチを作らずにサブモジュールディレクトリ内に コミットし、git submodule update を再び実⾏すると、親プロジェクトでコミットが何もな くても Git は⼿元の変更を断りなく上書きしてしまいます。技術的な意味では⼿元の作業 は失われたわけではないのですが、それを指すブランチが存在しない以上、先ほどの作業 を取り戻すのは困難です。 190 Scott Chacon Pro Git 6.6節 サブモジュール この問題を回避するには、サブモジュールのディレクトリで作業をするときに git checkout -b work などとしてブランチを作っておきます。次にサブモジュールを更新すると きにあなたの作業は消えてしまいますが、少なくとも元に戻すためのポインタは残ってい ます。 サブモジュールを含むブランチを切り替えるのは、これまた⽤⼼が必要です。新しいブ ランチを作成してそこにサブモジュールを追加し、サブモジュールを含まないブランチ に戻ったとしましょう。そこには、サブモジュールのディレクトリが「追跡されていない ディレクトリ」として残ったままになります。 $ git checkout -b rack Switched to a new branch "rack" $ git submodule add [email protected]:schacon/rack.git rack Initialized empty Git repository in /opt/myproj/rack/.git/ ... Receiving objects: 100% (3184/3184), 677.42 KiB | 34 KiB/s, done. Resolving deltas: 100% (1952/1952), done. $ git commit -am 'added rack submodule' [rack cc49a69] added rack submodule 2 files changed, 4 insertions(+), 0 deletions(-) create mode 100644 .gitmodules create mode 160000 rack $ git checkout master Switched to branch "master" $ git status # On branch master # Untracked files: # (use "git add <file>..." to include in what will be committed) # # rack/ これをどこか別の場所に移すか、削除しなければなりません。いずれにせよ、先ほどの ブランチに戻ったときには改めてクローンしなおさなければならず、ローカルでの変更や プッシュしていないブランチは失われてしまうことになります。 最後にもうひとつ、多くの⼈がハマるであろう点を指摘しておきましょう。これは、サ ブディレクトリからサブモジュールへ切り替えるときに起こることです。プロジェクト内 で追跡しているファイルをサブモジュール内に移動したくなったとしましょう。よっぽど 注意しないと、Git に怒られてしまいます。rack のファイルをプロジェクト内のサブディ レクトリで管理しており、それをサブモジュールに切り替えたくなったとしましょう。サ ブディレクトリをいったん削除してから submodule add と実⾏すると、Git に怒鳴りつけら れてしまいます。 $ rm -Rf rack/ $ git submodule add [email protected]:schacon/rack.git rack 191 第6章 Git のさまざまなツール Scott Chacon Pro Git 'rack' already exists in the index まず最初に rack ディレクトリをアンステージしなければなりません。それからだと、 サブモジュールを追加することができます。 $ git rm -r rack $ git submodule add [email protected]:schacon/rack.git rack Initialized empty Git repository in /opt/testsub/rack/.git/ remote: Counting objects: 3184, done. remote: Compressing objects: 100% (1465/1465), done. remote: Total 3184 (delta 1952), reused 2770 (delta 1675) Receiving objects: 100% (3184/3184), 677.42 KiB | 88 KiB/s, done. Resolving deltas: 100% (1952/1952), done. これをどこかのブランチで⾏ったとしましょう。そこから、(まだサブモジュールへの 切り替えがすんでおらず実際のツリーがある状態の) 別のブランチに切り替えようとする と、このようなエラーになります。 $ git checkout master error: Untracked working tree file 'rack/AUTHORS' would be overwritten by merge. いったん rack サブモジュールのディレクトリを別の場所に追い出してからでないと、 サブモジュールを持たないブランチに切り替えることはできません。 $ mv rack /tmp/ $ git checkout master Switched to branch "master" $ ls README rack さて、戻ってきたら、空っぽの rack ディレクトリが得られました。ここで git submodule update を実⾏して再クローンするか、あるいは /tmp/rack ディレクトリを書き戻します。 6.7 サブツリーマージ サブモジュールの仕組みに関する問題を⾒てきました。今度は同じ問題を解決するため の別の⽅法を⾒ていきましょう。Git でマージを⾏うときには、何をマージしなければな らないのかを Git がまず調べてそれに応じた適切なマージ⼿法を選択します。ふたつの ブランチをマージするときに Git が使うのは、再帰 (recursive) 戦略です。三つ以上のブ 192 Scott Chacon Pro Git 6.7節 サブツリーマージ ランチをマージするときには、Git は たこ⾜ (octopus) 戦略を選択します。どちらの戦略 を使うかは、Git が⾃動的に選択します。再帰戦略は複雑な三⽅向のマージ (共通の先祖 が複数あるなど) もこなせますが、ふたつのブランチしか処理できないからです。たこ⾜ マージは三つ以上のブランチを扱うことができますが、難しいコンフリクトを避けるため により慎重になります。そこで、三つ以上のブランチをマージするときのデフォルトの戦 略として選ばれています。しかし、それ以外にも選べる戦略があります。そのひとつが サブツリー (subtree) マージで、これを使えば先ほどのサブプロジェクト問題に対応する ことができます。先ほどのセクションと同じような rack の取り込みを、サブツリーマー ジを⽤いて⾏う⽅法を紹介しましょう。 サブツリーマージの考え⽅は、ふたつのプロジェクトがあるときに⼀⽅のプロジェクト をもうひとつのプロジェクトのサブディレクトリに位置づけたりその逆を⾏ったりすると いうものです。サブツリーマージを指定すると、Git は⼀⽅が他⽅のサブツリーであるこ とを理解して適切にマージを⾏います。驚くべきことです。 まずは Rack アプリケーションをプロジェクトに追加します。つまり、Rack プロジェ クトをリモート参照として⾃分のプロジェクトに追加し、そのブランチにチェックアウト します。 $ git remote add rack_remote [email protected]:schacon/rack.git $ git fetch rack_remote warning: no common commits remote: Counting objects: 3184, done. remote: Compressing objects: 100% (1465/1465), done. remote: Total 3184 (delta 1952), reused 2770 (delta 1675) Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done. Resolving deltas: 100% (1952/1952), done. From [email protected]:schacon/rack * [new branch] build -> rack_remote/build * [new branch] master -> rack_remote/master * [new branch] rack-0.4 -> rack_remote/rack-0.4 * [new branch] rack-0.9 -> rack_remote/rack-0.9 $ git checkout -b rack_branch rack_remote/master Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master. Switched to a new branch "rack_branch" これで Rack プロジェクトのルートが rack_branch ブランチに取得でき、あなたのプロ ジェクトが master ブランチにある状態になりました。まずどちらかをチェックアウトして それからもう⼀⽅に移ると、それぞれ別のプロジェクトルートとなっていることがわかり ます。 $ ls AUTHORS KNOWN-ISSUES Rakefile contrib lib COPYING README bin example test $ git checkout master 193 第6章 Git のさまざまなツール Scott Chacon Pro Git Switched to branch "master" $ ls README Rack プロジェクトを master プロジェクトのサブディレクトリとして取り込みたくなっ たときには、git read-tree を使います。read-tree とその仲間たちについては第9章 で詳し く説明します。現時点では、とりあえず「あるブランチのルートツリーを読み込んで、そ れを現在のステージングエリアと作業ディレクトリに書き込むもの」だと認識しておけ ばよいでしょう。まず master ブランチに戻り、rack ブランチの内容を master ブランチの rack サブディレクトリに取り込みます。 $ git read-tree --prefix=rack/ -u rack_branch これをコミットすると、Rack のファイルをすべてサブディレクトリに取り込んだよ うになります。そう、まるで tarball からコピーしたかのような状態です。おもしろいの は、あるブランチでの変更を簡単に別のブランチにマージできるということです。もし Rack プロジェクトが更新されたら、そのブランチに切り替えてプルするだけで本家の変 更を取得できます。 $ git checkout rack_branch $ git pull これで、変更を master ブランチにマージできるようになりました。git merge -s subtree を使えばうまく動作します。が、Git は歴史もともにマージしようとします。おそらくこ れはお望みの動作ではないでしょう。変更をプルしてコミットメッセージを埋めるには、 戦略を指定するオプション -s subtree のほかに --squash オプションと --no-commit オプショ ンを使います。 $ git checkout master $ git merge --squash -s subtree --no-commit rack_branch Squash commit -- not updating HEAD Automatic merge went well; stopped before committing as requested Rack プロジェクトでのすべての変更がマージされ、ローカルにコミットできる準備が 整いました。この逆を⾏うこともできます。master ブランチの rack サブディレクトリで 変更した内容を後で rack_branch ブランチにマージし、それをメンテナに投稿したり本家 にプッシュしたりといったことも可能です。 rack サブディレクトリの内容と rack_branch ブランチのコードの差分を取得する (そし て、マージしなければならない内容を知る) には、通常の diff コマンドを使うことはでき ません。そのかわりに、git diff-tree で⽐較対象のブランチを指定します。 194 Scott Chacon Pro Git 6.8節 まとめ $ git diff-tree -p rack_branch あるいは、rack サブディレクトリの内容と前回取得したときのサーバーの master ブラン チとを⽐較するには、次のようにします。 $ git diff-tree -p rack_remote/master 6.8 まとめ さまざまな⾼度な道具を使い、コミットやステージングエリアをより細やかに操作でき る⽅法をまとめました。何か問題が起こったときには、いつ誰がどのコミットでそれを仕 込んだのかを容易に⾒つけられるようになったことでしょう。また、プロジェクトの中 で別のプロジェクトを使いたくなったときのための⽅法もいくつか紹介しました。Git を 使った⽇々のコマンドラインでの作業の⼤半を、⾃信を持ってできるようになったことで しょう。 195 第7章 Git のカスタマイズ ここまで本書では、Git の基本動作やその使⽤法について扱ってきました。また、Git をより簡単に効率よく使うためのさまざまなツールについても紹介しました。本章では、 Git をよりカスタマイズするための操作⽅法を扱います。重要な設定項⽬やフックシステ ムについても説明します。これらを利⽤すれば、みなさん⾃⾝やその勤務先、所属グルー プのニーズにあわせた⽅法で Git を活⽤できるようになるでしょう。 7.1 Git の設定 第1章 で⼿短にごらんいただいたように、git config コマンドで Git の設定をすること ができます。まず最初にすることと⾔えば、名前とメールアドレスの設定でしょう。 $ git config --global user.name "John Doe" $ git config --global user.email [email protected] ここでは、同じようにして設定できるより興味深い項⽬をいくつか⾝につけ、Git をカ スタマイズしてみましょう。 Git の設定については最初の章でちらっと説明しましたが、ここでもう⼀度振り返って おきます。Git では、いくつかの設定ファイルを使ってデフォルト以外の挙動を定義しま す。まず最初に Git が⾒るのは /etc/gitconfig で、ここにはシステム上の全ユーザーの全 リポジトリ向けの設定値を記述します。git config にオプション --system を指定すると、 このファイルの読み書きを⾏います。 次に Git が⾒るのは ~/.gitconfig で、これは各ユーザー専⽤のファイルです。Git でこ のファイルの読み書きをするには、--global オプションを指定します。 最後に Git が設定値を探すのは、現在使⽤中のリポジトリの設定ファイル (.git/config) です。この値は、そのリポジトリだけで有効なものです。後から読んだ値がその前の値を 上書きします。したがって、たとえば .git/config に書いた値は /etc/gitconfig での設定よ りも優先されます。これらのファイルを⼿動で編集して正しい構⽂で値を追加することも できますが、通常は git config コマンドを使ったほうが簡単です。 197 第7章 Git のカスタマイズ Scott Chacon Pro Git 7.1.1 基本的なクライアントのオプション Git の設定オプションは、おおきく⼆種類に分類できます。クライアント側のオプショ ンとサーバー側のオプションです。⼤半のオプションは、クライアント側のもの、つまり 個⼈的な作業環境を設定するためのものとなります。⼤量のオプションがありますが、こ こでは⼀般的に使われているものやワークフローに⼤きな影響を及ぼすものに絞っていく つかを紹介します。その他のオプションの多くは特定の場合にのみ有⽤なものなので、こ こでは扱いません。Git で使えるすべてのオプションを知りたい場合は、次のコマンドを 実⾏しましょう。 $ git config --help また、git config のマニュアルページには、利⽤できるすべてのオプションについて詳 しい説明があります。 core.editor コミットやタグのメッセージを編集するときに使うエディタは、ユーザーがデフォルト エディタとして設定したものとなります。デフォルトエディタが設定されていない場合は Vi エディタを使います。このデフォルト設定を別のものに変更するには core.editor を設 定します。 $ git config --global core.editor emacs これで、シェルのデフォルトエディタを設定していない場合に Git が起動するエディタ が Emacs に変わりました。 commit.template システム上のファイルへのパスをここに設定すると、Git はそのファイルをコミット時 のデフォルトメッセージとして使います。たとえば、次のようなテンプレートファイルを 作って $HOME/.gitmessage.txt においたとしましょう。 subject line what happened [ticket: X] git commit の と き に エ ディ タ に 表 ⽰ さ れ る デ フォ ル ト メッ セー ジ を こ れ に す る に は、commit.template の設定を変更します。 198 Scott Chacon Pro Git 7.1節 Git の設定 $ git config --global commit.template $HOME/.gitmessage.txt $ git commit すると、コミットメッセージの雛形としてこのような内容がエディタに表⽰されます。 subject line what happened [ticket: X] # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # modified: lib/test.rb # ~ ~ ".git/COMMIT_EDITMSG" 14L, 297C コミットメッセージについて所定の決まりがあるのなら、その決まりに従ったテンプ レートをシステム上に作って Git にそれを使わせるようにするとよいでしょう。そうすれ ば、その決まりに従ってもらいやすくなります。 core.pager core.pager は、 Git が log や diff な ど を 出 ⼒ す る と き に 使 う ペー ジャ を 設 定 し ま す。more などのお好みのページャを設定したり (デフォルトは less です)、空⽂字列を 設定してページャを使わないようにしたりすることができます。 $ git config --global core.pager '' これを実⾏すると、すべてのコマンドの出⼒を、どんなに⻑くなったとしても全部 Git が出⼒するようになります。 user.signingkey 署名⼊りの注釈付きタグ (第2章 で取り上げました) を作る場合は、GPG 署名⽤の鍵を 登録しておくと便利です。鍵の ID を設定するには、このようにします。 199 第7章 Git のカスタマイズ Scott Chacon Pro Git $ git config --global user.signingkey <gpg-key-id> これで、git tag コマンドでいちいち鍵を指定しなくてもタグに署名できるようになり ました。 $ git tag -s <tag-name> core.excludesfile プロジェクトごとの .gitignore ファイルでパターンを指定すると、git add したときに Git がそのファイルを無視してステージしないようになります。これについては第2章 で 説明しました。しかし、これらの内容をプロジェクトの外部で管理したい場合は、その ファイルがどこにあるのかを core.excludesfile で設定します。ここに設定する内容はファ イルのパスです。ファイルの中⾝は .gitignore と同じ形式になります。 help.autocorrect このオプションが使えるのは Git 1.6.1 以降だけです。Git でコマンドを打ち間違える と、こんなふうに表⽰されます。 $ git com git: 'com' is not a git-command. See 'git --help'. Did you mean this? commit help.autocorrect を 1 にしておくと、同じような場⾯でもし候補がひとつしかなければ ⾃動的にそれを実⾏します。 7.1.2 Git における色 Git では、ターミナルへの出⼒に⾊をつけることができます。ぱっと⾒て、すばやくお ⼿軽に出⼒内容を把握できるようになるでしょう。さまざまなオプションで、お好みに合 わせて⾊を設定しましょう。 color.ui あらかじめ指定しておけば、Git は⾃動的に⼤半の出⼒に⾊づけをします。何にどのよ うな⾊をつけるかをこと細かに指定することもできますが、すべてをターミナルのデフォ ルト⾊設定にまかせるなら color.ui を true にします。 200 Scott Chacon Pro Git 7.1節 Git の設定 $ git config --global color.ui true これを設定すると、出⼒がターミナルに送られる場合に Git がその出⼒を⾊づけしま す。ほかに false という値を指定することもでき、これは出⼒に決して⾊をつけません。 また always を指定すると、すべての場合に⾊をつけます。すべての場合とは、Git コマ ンドをファイルにリダイレクトしたり他のコマンドにパイプでつないだりする場合も含み ます。 color.ui = always を使うことは、まずないでしょう。たいていの場合は、カラーコード を含む結果をリダイレクトしたい場合は Git コマンドに --color フラグを渡してカラー コードの使⽤を強制します。ふだんは color.ui = true の設定で要望を満たせるでしょう。 color.* どのコマンドをどのように⾊づけするかをより細やかに指定したい場合、コマンド単位 の⾊づけ設定を使⽤します。これらの項⽬には true、false あるいは always を指定するこ とができます。 color.branch color.diff color.interactive color.status さらに、これらの項⽬ではサブ設定が使え、出⼒の⼀部について特定の⾊を使うように 指定することもできます。たとえば、diff の出⼒でのメタ情報を⻘の太字で出⼒させたい 場合は次のようにします。 $ git config --global color.diff.meta "blue black bold" ⾊として指定できる値は normal、black、red、green、yellow、blue、magenta、 cyan あるいは white のいずれかです。先ほどの例の bold のように属性を指定すること もできます。bold、dim、ul、blink および reverse のいずれかを指定できます。 git config のマニュアルページに、すべてのサブ設定がまとめられていますので参照く ださい。 7.1.3 外部のマージツールおよび Diff ツール Git には diff の実装が組み込まれておりそれを使うことができますが、外部のツール を使うよう設定することもできます。また、コンフリクトを⼿動で解決するのではなく グラフィカルなコンフリクト解消ツールを使うよう設定することもできます。ここでは Perforce Visual Merge Tool (P4Merge) を使って diff の表⽰とマージの処理を⾏えるよ うにする例を⽰します。これはすばらしいグラフィカルツールで、しかもフリーだからで す。 201 第7章 Git のカスタマイズ Scott Chacon Pro Git P4Merge はすべての主要プラットフォーム上で動作するので、実際に試してみたい⼈ は試してみるとよいでしょう。この例では、Mac や Linux 形式のパス名を例に使いま す。Windows の場合は、/usr/local/bin のところを環境に合わせたパスに置き換えてくだ さい。 まず、P4Merge をここからダウンロードします。 http://www.perforce.com/perforce/downloads/component.html 最初に、コマンドを実⾏するための外部ラッパースクリプトを⽤意します。この例で は、Mac ⽤の実⾏パスを使います。他のシステムで使う場合は、p4merge のバイナリがイ ンストールされた場所に置き換えてください。次のようなマージ⽤ラッパースクリプト extMerge を⽤意しました。これは、すべての引数を受け取ってバイナリをコールします。 $ cat /usr/local/bin/extMerge #!/bin/sh /Applications/p4merge.app/Contents/MacOS/p4merge $* diff のラッパーは、7 つの引数が渡されていることを確認したうえでそのうちのふたつ をマージスクリプトに渡します。デフォルトでは、Git は次のような引数を diff プログラ ムに渡します。 path old-file old-hex old-mode new-file new-hex new-mode ここで必要な引数は old-file と new-file だけなので、ラッパースクリプトではこれらを 渡すようにします。 $ cat /usr/local/bin/extDiff #!/bin/sh [ $# -eq 7 ] && /usr/local/bin/extMerge "$2" "$5" また、これらのツールは実⾏可能にしておかなければなりません。 $ sudo chmod +x /usr/local/bin/extMerge $ sudo chmod +x /usr/local/bin/extDiff これで、⾃前のマージツールや diff ツールを使えるように設定する準備が整いました。 設定項⽬はひとつだけではありません。まず merge.tool でどんなツールを使うのかを Git に伝え、mergetool.*.cmd でそのコマンドを実⾏する⽅法を指定し、mergetool.trustExitCode では「そのコマンドの終了コードでマージが成功したかどうかを判断できるのか」を指定 し、diff.external では diff の際に実⾏するコマンドを指定します。つまり、このような 4 つのコマンドを実⾏することになります。 202 Scott Chacon Pro Git 7.1節 Git の設定 $ git config --global merge.tool extMerge $ git config --global mergetool.extMerge.cmd \ 'extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"' $ git config --global mergetool.trustExitCode false $ git config --global diff.external extDiff あるいは、~/.gitconfig ファイルを編集してこのような⾏を追加します。 [merge] tool = extMerge [mergetool "extMerge"] cmd = extMerge \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\" trustExitCode = false [diff] external = extDiff すべて設定し終えたら、 $ git diff 32d1776b1^ 32d1776b1 このような diff コマンドを実⾏すると、結果をコマンドラインに出⼒するかわりに P4Merge を⽴ち上げ、図 7-1 のようになります。 図 7.1: P4Merge ふたつのブランチをマージしてコンフリクトが発⽣した場合は git mergetool を実⾏しま す。すると P4Merge が⽴ち上がり、コンフリクトの解決を GUI ツールで⾏えるように なります。 このようなラッパーを設定しておくと、あとで diff ツールやマージツールを変更した くなったときにも簡単に変更することができます。たとえば extDiff や extMerge で KDiff3 203 第7章 Git のカスタマイズ Scott Chacon Pro Git を実⾏させるように変更するには extMerge ファイルをこのように変更するだけでよいので す。 $ cat /usr/local/bin/extMerge #!/bin/sh /Applications/kdiff3.app/Contents/MacOS/kdiff3 $* これで、Git での diff の閲覧やコンフリクトの解決の際に KDiff3 が⽴ち上がるように なりました。 Git にはさまざまなマージツール⽤の設定が事前に準備されており、特に設定しなくて も利⽤することができます。事前に設定が準備されているツールは kdiff3、opendiff、 tkdiff、meld、xxdiff、emerge、vimdiff そして gvimdiff です。KDiff3 を diff ツールと してではなくマージのときにだけ使いたい場合は、kdiff3 コマンドにパスが通っている状 態で次のコマンドを実⾏します。 $ git config --global merge.tool kdiff3 extMerge や extDiff を準備せずにこのコマンドを実⾏すると、マージの解決の際には KDiff3 を⽴ち上げて diff の際には通常の Git の diff ツールを使うようになります。 7.1.4 書式設定と空白文字 書式設定や空⽩⽂字の問題は微妙にうっとうしいもので、とくにさまざまなプラット フォームで開発している⼈たちと共同作業をするときに問題になりがちです。使っている エディタが知らぬ間に空⽩⽂字を埋め込んでしまっていたり Windows で開発している⼈ が⾏末にキャリッジリターンを付け加えてしまったりなどしてパッチが⾯倒な状態になっ てしまうことも多々あります。Git では、こういった問題に対処するための設定項⽬も⽤ 意しています。 core.autocrlf ⾃分が Windows で開発していたり、チームの中に Windows で開発している⼈がいた りといった場合に、改⾏コードの問題に巻き込まれることがありがちです。Windows で はキャリッジリターンとラインフィードでファイルの改⾏を表すのですが、Mac や Linux ではラインフィードだけで改⾏を表すという違いが原因です。ささいな違いではあります が、さまざまなプラットフォームにまたがる作業では⾮常に⾯倒なものです。 Git はこの問題に対処するために、コミットする際には⾏末の CRLF を LF に⾃動変換 し、ファイルシステム上にチェックアウトするときには逆の変換を⾏うようにすることが できます。この機能を使うには core.autocrlf を設定します。Windows で作業をするとき にこれを true に設定すると、コードをチェックアウトするときに⾏末の LF を CRLF に ⾃動変換してくれます。 204 Scott Chacon Pro Git 7.1節 Git の設定 $ git config --global core.autocrlf true Linux や Mac などの⾏末に LF を使うシステムで作業をしている場合は、Git にチェッ クアウト時の⾃動変換をされてしまうと困ります。しかし、⾏末が CRLF なファイルが紛 れ込んでしまった場合には Git に⾃動修正してもらいたいものです。コミット時の CRLF から LF への変換はさせたいけれどもそれ以外の⾃動変換が不要な場合は、core.autocrlf を input に設定します。 $ git config --global core.autocrlf input この設定は、Windows にチェックアウトしたときの CRLF への変換は⾏いますが、 Mac や Linux へのチェックアウト時は LF のままにします。またリポジトリにコミット する際には LF への変換を⾏います。 Windows のみのプロジェクトで作業をしているのなら、この機能を無効にしてキャ リッジリターンをそのままリポジトリに記録してもよいでしょう。その場合は、値 false を設定します。 $ git config --global core.autocrlf false core.whitespace Git には、空⽩⽂字に関する問題を⾒つけて修正するための設定もあります。空⽩⽂字 に関する主要な四つの問題に対応するもので、そのうち⼆つはデフォルトで有効になって います。残りの⼆つはデフォルトでは有効になっていませんが、有効化することができま す。 デフォルトで有効になっている設定は、⾏末の空⽩⽂字を⾒つける trailing-space と⾏ 頭のタブ⽂字より前にある空⽩⽂字を⾒つける space-before-tab です。 デフォルトでは無効だけれども有効にすることもできる設定は、⾏頭にある⼋⽂字以上 の空⽩⽂字を⾒つける indent-with-non-tab と⾏末のキャリッジリターンを許容する cr-ateol です。 これらのオン・オフを切り替えるには、core.whitespace にカンマ区切りで項⽬を指定し ます。無効にしたい場合は、設定⽂字列でその項⽬を省略するか、あるいは項⽬名の前に - をつけます。たとえば cr-at-eol 以外のすべてを設定したい場合は、このようにします。 $ git config --global core.whitespace \ trailing-space,space-before-tab,indent-with-non-tab git diff コマンドを実⾏したときに Git がこれらの問題を検出すると、その部分を⾊付 けして表⽰します。修正してからコミットするようにしましょう。この設定は、git apply 205 第7章 Git のカスタマイズ Scott Chacon Pro Git でパッチを適⽤する際にも助けとなります。空⽩に関する問題を含むパッチを適⽤すると きに警告を発してほしい場合には、次のようにします。 $ git apply --whitespace=warn <patch> あるいは、問題を⾃動的に修正してからパッチを適⽤したい場合は、次のようにしま す。 $ git apply --whitespace=fix <patch> これらの設定は、git rebaseコマンドにも適⽤されます。空⽩に関する問題を含むコ ミットをしたけれどまだそれを公開リポジトリにプッシュしていない場合は、rebase に -whitespace=fix オプションをつけて実⾏すれば、パッチを書き換えて空⽩問題を⾃動修正 してくれます。 7.1.5 サーバーの設定 Git のサーバー側の設定オプションはそれほど多くありませんが、いくつか興味深いも のがあるので紹介します。 receive.fsckObjects デフォルトでは、Git はプッシュで受け取ったオブジェクトの⼀貫性をチェックしませ ん。各オブジェクトの SHA-1 チェックサムが⼀致していて有効なオブジェクトを指して いるということを Git にチェックさせることもできますが、デフォルトでは毎回のプッ シュ時のチェックは⾏わないようになっています。このチェックは⽐較的重たい処理であ り、リポジトリのサイズが⼤きかったりプッシュする量が多かったりすると、毎回チェッ クさせるのには時間がかかるでしょう。毎回のプッシュの際に Git にオブジェクトの⼀貫 性をチェックさせたい場合は、receive.fsckObjects を true にして強制的にチェックさせる ようにします。 $ git config --system receive.fsckObjects true これで、Git がリポジトリの整合性を確認してからでないとプッシュが認められないよ うになります。壊れたデータをまちがって受け⼊れてしまうことがなくなりました。 receive.denyNonFastForwards すでにプッシュしたコミットをリベースしてもう⼀度プッシュした場合、あるいはリ モートブランチが現在指しているコミットを含まないコミットをプッシュしようとした場 合は、プッシュが拒否されます。これは悪くない⽅針でしょう。しかしリベースの場合 は、⾃分が何をしているのかをきちんと把握していれば、プッシュの際に -f フラグを指 定して強制的にリモートブランチを更新することができます。 206 Scott Chacon Pro Git 7.2節 Git の属性 このような強制更新機能を無効にするには、receive.denyNonFastForwards を設定します。 $ git config --system receive.denyNonFastForwards true もうひとつの⽅法として、サーバー側の receive フックを使うこともできます。こちら の⽅法については後ほど簡単に説明します。receive フックを使えば、特定のユーザーだ け強制更新を無効にするなどより細やかな制御ができるようになります。 receive.denyDeletes denyNonFastForwards の制限を回避する⽅法として、いったんブランチを削除してから新 しいコミットを参照するブランチをプッシュしなおすことができます。その対策として、 新しいバージョン (バージョン 1.6.1 以降) の Git では receive.denyDeletes を true に設定 することができます。 $ git config --system receive.denyDeletes true これは、プッシュによるブランチやタグの削除を⼀切拒否し、誰も削除できないように します。リモートブランチを削除するには、サーバー上の ref ファイルを⼿で削除しなけ ればなりません。ACL を使って、ユーザー単位でこれを制限することもできますが、そ の⽅法は本章の最後で扱います。 7.2 Git の属性 設定項⽬の中には、パスにも指定できるものがあります。Git はその設定を、指定した パスのサブディレクトリやファイルにのみ適⽤するのです。これらのパス固有の設定は Git の属性と呼ばれ、あるディレクトリ (通常はプロジェクトのルートディレクトリ) の直 下の .gitattributes か、あるいはそのファイルをプロジェクトとともにコミットしたくな い場合は .git/info/attributes に設定します。 属性を使うと、ファイルやディレクトリ単位で個別のマージ戦略を指定したりテキスト ファイル以外での diff の取得⽅法を指⽰したり、あるいはチェックインやチェックアウト の前に Git にフィルタリングさせたりすることができます。このセクションでは、Git プ ロジェクトでパスに設定できる属性のいくつかについて学び、実際にその機能を使う例を ⾒ていきます。 7.2.1 バイナリファイル Git の属性を使ってできるちょっとした技として、どのファイルがバイナリファイルな のかを (その他の⽅法で判別できない場合のために) 指定して Git に対してバイナリファ イルの扱い⽅を指⽰するというものがあります。たとえば、機械で⽣成したテキストファ イルの中には diff が取得できないものがありますし、バイナリファイルであっても diff が取得できるものもあります。それを Git に指⽰する⽅法を紹介します。 207 第7章 Git のカスタマイズ Scott Chacon Pro Git バイナリファイルの特定 テキストファイルのように⾒えるファイルであっても、何らかの⽬的のために意図的 にバイナリデータとして扱いたいこともあります。たとえば、Mac の Xcode プロジェク トの中には .pbxproj で終わる名前のファイルがあります。これは JSON (プレーンテキス ト形式の javascript のデータフォーマット) のデータセットで、IDE がビルド設定などを ディスクに書き出したものです。すべて ASCII で構成されるので、理論上はこれはテキ ストファイルです。しかしこのファイルをテキストファイルとして扱いたくはありませ ん。実際のところ、このファイルは軽量なデータベースとして使われているからです。他 の⼈が変更した内容をマージすることはできませんし、diff をとってもあまり意味があり ません。このファイルは、基本的に機械が処理するものなのです。要するに、バイナリ ファイルと同じように扱いたいということです。 すべての pbxproj ファイルをバイナリデータとして扱うよう Git に指定するには、次の ⾏を .gitattributes ファイルに追加します。 *.pbxproj -crlf -diff これで、Git が CRLF 問題の対応をすることもなくなりますし、git show や git diff を 実⾏したときにもこのファイルの diff を調べることはなくなります。また、次のようなマ クロbinaryを使うこともできます。これは -crlf -diff と同じ意味です。 *.pbxproj binary バイナリファイルの差分 Gitでは、バイナリファイルの差分を効果的に扱うためにGitの属性機能を使うことがで きます。通常のdiff機能を使って⽐較を⾏うことができるように、バイナリデータをテキ ストデータに変換する⽅法をGitに教えればいいのです。ただし問題があります。バイナ リデータをどうやってテキストに変換するか?ということです。この場合、⼀番いい⽅法 はバイナリファイル形式ごとに専⽤の変換ツールを使うことです。とはいえ、判読可能な テキストに変換可能なバイナリファイル形式はそう多くありません(⾳声データをテキス ト形式に変換?うまくいかなさそうです…)。ただ、仮にそういった事例に出くわしデー タをテキスト形式にできなかったとしても、ファイルの内容についての説明、もしくはメ タデータを取得することはそれほど難しくないでしょう。もちろん、そのファイルについ ての全てがメタデータから読み取れるわけではありませんが、何もないよりはよっぽどよ いはずです。 これから、上述の2⼿法を⽤い、よく使われてるバイナリファイル形式から有⽤な差分 を取得する⽅法を説明します。 補⾜: バイナリファイル形式で、かつデータがテキストで記述されているけれど、テキ スト形式に変換するためのツールがないケースがよくあります。そういった場合、strings プログラムを使ってそのファイルからテキストを抽出できるかどうか、試してみるとよい でしょう。UTF-16 などのエンコーディングで記述されている場合だと、strings プログラ ムではうまくいかないかもしれません。どこまで変換できるかはケースバイケースでしょ 208 Scott Chacon Pro Git 7.2節 Git の属性 う。とはいえ、strings プログラムは ⼤半の Mac と Linux で使えるので、バイナリファ イル形式を取り扱う最初の⼀⼿としては⼗分でしょう。 MS Word ファイル あなたはまずこれらのテクニックを使って、⼈類にとって最も厄介 な問題のひとつ、Wordで作成した⽂書のバージョン管理を解決したいと思うでしょう。 奇妙なことに、Wordは最悪のエディタだと全ての⼈が知っているにも係わらず、皆が Wordを使っています。Word⽂書をバージョン管理したいと思ったなら、Gitのリポジト リにそれらを追加して、まとめてコミットすればいいのです。しかし、それでいいので しょうか? あなたが git diff をいつも通りに実⾏すると、次のように表⽰されるだけで す。 $ git diff diff --git a/chapter1.doc b/chapter1.doc index 88839c4..4afcb7c 100644 Binary files a/chapter1.doc and b/chapter1.doc differ これでは2つのバージョンをcheckoutしてそれらを⾃分で⾒⽐べてみない限り、⽐較す ることは出来ませんよね? Gitの属性を使えば、うまく解決できます。.gitattributesに次 の⾏を追加して下さい。 *.doc diff=word これは、指定したパターン(.doc)にマッチした全てのファイルに対して、差分を表⽰す る時には『word』というフィルタを使うべきであるとGitに教えているのです。『word』 フィルタとは何でしょうか? それはあなたが⽤意しなければなりません。Word⽂書をテ キストファイルに変換するプログラムとして catdoc を使うように次のようにGitを設定し てみましょう。なお、catdoc とは、差分を正しく表⽰するために、Word⽂書からテキス トを取り出す専⽤のツール( http://www.wagner.pp.ru/~vitus/software/catdoc/ からダウンロー ドできます。)です。 $ git config diff.word.textconv catdoc このコマンドは、.git/config に次のようなセクションを追加します。 [diff "word"] textconv = catdoc これで、.doc という拡張⼦をもったファイルはそれぞれのファイルに catdoc というプ ログラムとして定義された『word』フィルタを通してからdiffを取るべきだということを Gitは知っていることになります。こうすることで、Wordファイルに対して直接差分を取 209 第7章 Git のカスタマイズ Scott Chacon Pro Git るのではなく、より効果的なテキストベースでの差分を取ることができるようになりま す。 例を⽰しましょう。この本の第1章をGitリポジトリに登録した後、ある段落にいくつ かの⽂章を追加して保存し、それから、変更箇所を確認するためにgit diffを実⾏しまし た。 $ git diff diff --git a/chapter1.doc b/chapter1.doc index c1c8a0a..b93c9e4 100644 --- a/chapter1.doc +++ b/chapter1.doc @@ -128,7 +128,7 @@ and data size) Since its birth in 2005, Git has evolved and matured to be easy to use and yet retain these initial qualities. It’s incredibly fast, it’s very efficient with large projects, and it has an incredible branching -system for non-linear development. +system for non-linear development (See Chapter 3). Gitは、追加した『(See Chapter 3)』という⽂字列を⾸尾よく、かつ、簡潔に知らせて くれました。正確で、申し分のない動作です! OpenDocument Text ファイル MS Word ファイル (*.doc) と同じ考えかたで、OpenOffice.org の OpenDocument Text ファイル (*.odt) も扱えます。 次の⾏を .gitattributes ファイルに追加しましょう。 *.odt diff=odt そして、odt diff フィルタを .git/config に追加します。 [diff "odt"] binary = true textconv = /usr/local/bin/odt-to-txt OpenDocument ファイルの正体は zip で、複数のファイル (XML 形式のコンテンツや スタイルシート、画像など) を含むディレクトリをまとめたものです。このコンテンツを 展開し、プレーンテキストとして返すスクリプトが必要です。/usr/local/bin/odt-to-txt と いうファイルを作って (ディレクトリはどこでもかまいません)、次のような内容を書きま しょう。 210 Scott Chacon Pro Git 7.2節 Git の属性 #! /usr/bin/env perl # Simplistic OpenDocument Text (.odt) to plain text converter. # Author: Philipp Kempgen if (! defined($ARGV[0])) { print STDERR "No filename given!\n"; print STDERR "Usage: $0 filename\n"; exit 1; } my $content = ''; open my $fh, '-|', 'unzip', '-qq', '-p', $ARGV[0], 'content.xml' or die $!; { local $/ = undef; # slurp mode $content = <$fh>; } close $fh; $_ = $content; s/<text:span\b[^>]*>//g; # remove spans s/<text:h\b[^>]*>/\n\n***** /g; # headers s/<text:list-item\b[^>]*>\s*<text:p\b[^>]*>/\n -- /g; # list items s/<text:list\b[^>]*>/\n\n/g; # lists s/<text:p\b[^>]*>/\n /g; # paragraphs s/<[^>]+>//g; # remove all XML tags s/\n{2,}/\n\n/g; # remove multiple blank lines s/\A\n+//; # remove leading blank lines print "\n", $_, "\n\n"; そして実⾏権限をつけます。 chmod +x /usr/local/bin/odt-to-txt これで、git diff で .odt ファイルの変更点を確認できるようになりました。 画像ファイル その他の興味深い問題としては画像ファイルの差分があります。PNG ファイルに対するひとつの⽅法としては、EXIF情報(多くのファイルでメタデータとし て使われています)を抽出するフィルタを使う⽅法です。exiftoolをダウンロードしインス トールすれば、画像データをメタデータの形でテキストデータとして扱うことができま す。従って、次のように設定すれば、画像データの差分をメタデータの差分という形で表 ⽰することができます。 211 第7章 Git のカスタマイズ Scott Chacon Pro Git $ echo '*.png diff=exif' >> .gitattributes $ git config diff.exif.textconv exiftool 上記の設定をしてからプロジェクトで画像データを置き換えてgit diffと実⾏すれば、 次のように表⽰されることになるでしょう。 diff --git a/image.png b/image.png index 88839c4..4afcb7c 100644 --- a/image.png +++ b/image.png @@ -1,12 +1,12 @@ ExifTool Version Number : 7.74 -File Size : 70 kB -File Modification Date/Time : 2009:04:17 10:12:35-07:00 +File Size : 94 kB +File Modification Date/Time : 2009:04:21 07:02:43-07:00 File Type : PNG MIME Type : image/png -Image Width : 1058 -Image Height : 889 +Image Width : 1056 +Image Height : 827 Bit Depth : 8 Color Type : RGB with Alpha ファイルのサイズと画像のサイズが変更されたことが簡単に⾒て取れるでしょう。 7.2.2 キーワード展開 SubversionやCVSを使っていた開発者から、キーワード展開機能をリクエストされる ことがよくあります。これについてGitにおける主な問題は、Gitはまずファイルのチェッ クサムを⽣成するためにcommitした後にファイルに関する情報を変更できないという点 です。しかし、commitするためにaddする前にファイルをcheckoutしremoveするとい う⼿順を踏めば、その時にファイルにテキストを追加することが可能です。Gitの属性は そうするための⽅法を2つ提供します。 ひとつめの⽅法として、ファイルの$Id$フィールドを⾃動的にblobのSHA-1 checksum を挿⼊するようにできます。あるファイル、もしくはいくつかのファイルに対してこの属 性を設定すれば、次にcheckoutする時、Gitはこの置き換えを⾏うようになるでしょう。 ただし、挿⼊されるチェックサムはcommitに対するものではなく、対象となるblobもの であるという点に注意して下さい。 212 Scott Chacon Pro Git 7.2節 Git の属性 $ echo '*.txt ident' >> .gitattributes $ echo '$Id$' > test.txt 次にtest.txtをcheckoutする時、GitはSHA-1チェックサムを挿⼊します。 $ rm test.txt $ git checkout -- test.txt $ cat test.txt $Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $ しかし、このやりかたには制限があります。CVSやSubversionのキーワード展開では タイムスタンプを含めることができます。対して、SHA-1チェックサムは完全にランダム な値ですから、2つの値の新旧を知るための助けにはなりません。 これには、commit/checkout時にキーワード展開を⾏うためのフィルタを書いてやる ことで対応できます。このために『clean』と『smudge』フィルタがあります。特定の ファイルに対して使⽤するフィルタを設定し、checkoutされる前(『smudge』 図7-2参 照)もしくはcommitされる前(『clean』 図7-3参照)に指定したスクリプトが実⾏させるよ う、.gitattributesファイルで設定できます。これらのフィルタはあらゆる種類の⾯⽩い内 容を実⾏するように設定できます。 図 7.2: checkoutする時に『smudge』フィルタを実行する この機能に対してオリジナルのcommitメッセージは簡単な例を与えてくれています。 それはcommit前にあなたのCのソースコードをindentプログラムに通すというもので す。*.cファイルに対して『indent』フィルタを実⾏するように、.gitattributesファイル にfilter属性を設定することができます。 *.c filter=indent それから、smudgeとcleanで『indent』フィルタが何を⾏えばいいのかをGitに教えま す。 213 第7章 Git のカスタマイズ Scott Chacon Pro Git 図 7.3: ステージする時に『clean』フィルタを実行する。 $ git config --global filter.indent.clean indent $ git config --global filter.indent.smudge cat このケースでは、*.cにマッチするファイルをcommitした時、Gitはcommit前にindent プログラムにファイルを通し、checkoutする前にはcatを通すようにします。catは基本的 に何もしません。⼊⼒されたデータと同じデータを吐き出すだけです。この組み合わせで Cのソースコードに対してcommit前にindentを通すことが効果的に⾏えます。 RCSスタイルの$Date$キーワード展開もまた別の興味深い例です。満⾜のいく形でこれ を⾏うには、ファイル名を受け取って、プロジェクトの最新のcommitの⽇付を⾒付けだ し、その⽇付をファイルに挿⼊するちょっとしたスクリプトが必要になります。そのよう なRubyスクリプトが以下です。 #! /usr/bin/env ruby data = STDIN.read last_date = `git log --pretty=format:"%ad" -1` puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$') このスクリプトは、git logコマンドの出⼒から最新のcommitの⽇付を取得し、標準⼊ ⼒からの⼊⼒中のすべての$Date$⽂字列にその⽇付を追加し、結果を表⽰します。あなた のお気に⼊りのどのような⾔語でスクリプトを書くにしても、簡潔にすべきです。このス クリプトファイルにexpand_dateと名前をつけ、実⾏パスのどこかに置きます。次に、Gitが 使うフィルタ(daterと呼びましょうか)を設定し、checkout時にexpand_dateが実⾏されるよ うにGitに教えてあげましょう。 $ git config filter.dater.smudge expand_date $ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"' このPerlのスクリプトは、開始点に戻るために$Date$⽂字列内の他の⽂字列を削除しま 214 Scott Chacon Pro Git 7.2節 Git の属性 す。さあ、フィルタの準備ができました。ファイルに$Date$キーワードを追加して新しい フィルタに仕事をさせるためにGitの属性を設定して、テストしてみましょう。 $ echo '# $Date$' > date_test.txt $ echo 'date*.txt filter=dater' >> .gitattributes これらの変更をcommitして再度ファイルをcheckoutすれば、キーワード展開が正しく ⾏われているのがわかります。 $ git add date_test.txt .gitattributes $ git commit -m "Testing date expansion in Git" $ rm date_test.txt $ git checkout date_test.txt $ cat date_test.txt # $Date: Tue Apr 21 07:26:52 2009 -0700$ アプリケーションをカスタマイズするためのこのテクニックがどれほど強⼒か、おわか りいただけたと思います。しかし、注意が必要です。.gitattributesファイルはcommitさ れ、プロジェクト内で共有されますが、ドライバ(このケースで⾔えば、dater)はそうはい きません。そう、すべての環境で動くとは限らないのです。あなたがこうしたフィルタを デザインする時、たとえフィルタが正常に動作しなかったとしても、プロジェクトは適切 に動き続けられるようにすべきです。 7.2.3 リポジトリをエクスポートする あなたのプロジェクトのアーカイブをエクスポートする時には、Gitの属性データを 使って興味深いことを⾏うことができます。 export-ignore アーカイヴを⽣成するとき、あるファイルやディレクトリをエクスポートしないように 設定することができます。プロジェクトにはcheckinしたいがアーカイブファイルには含 めたくないディレクトリやファイルがあるなら、それらにexport-ignoreを設定してやるこ とができます。 例えば、test/ディレクトリ以下にいくつかのテストファイルがあって、それらをプロ ジェクトのtarballには含めたくないとしましょう。その場合、次の1⾏をGitの属性ファイ ルに追加します。 test/ export-ignore これで、プロジェクトのtarballを作成するためにgit archiveを実⾏した時、アーカイブ にはtest/ディレクトリが含まれないようになります。 215 第7章 Git のカスタマイズ Scott Chacon Pro Git export-subst アーカイブ作成時にできる別のこととして、いくつかの簡単なキーワード展開がありま す。第2章で紹介した--pretty=formatで指定できるフォーマット指定⼦とともに$Format:$⽂ 字列をファイルに追加することができます。例えば、LAST_COMMITという名前のファイルを プロジェクトに追加し、git archiveを実⾏した時にそれを最新のcommitの⽇付に変換し たい場合、次のように設定します。 $ echo 'Last commit date: $Format:%cd$' > LAST_COMMIT $ echo "LAST_COMMIT export-subst" >> .gitattributes $ git add LAST_COMMIT .gitattributes $ git commit -am 'adding LAST_COMMIT file for archives' git archiveを実⾏したあと、アーカイブを展開すると、LAST_COMMITは以下のような内容 になっているでしょう。 $ cat LAST_COMMIT Last commit date: $Format:Tue Apr 21 08:38:48 2009 -0700$ 7.2.4 マージの戦略 Git属性を使えば、プロジェクトにある指定したファイルに対して異なるマージ戦略を 使うようにすることができます。とても有効なオプションのひとつは、指定したファイル で競合が発⽣した場合に、マージを⾏わずにあなたの変更内容で他の誰かの変更を上書き するように設定するというものです。 これはブランチを分岐させ特別な作業をしている時、そのブランチでの変更をマージさ せたいが、いくつかのファイルの変更はなかったことにしたいというような時に助けにな ります。例えば、database.xmlというデータベースの設定ファイルがあり、ふたつのブ ランチでその内容が異なっているとしましょう。そして、そのデータベースファイルを台 無しすることなしに、⼀⽅のブランチへとマージしたいとします。これは、次のように属 性を設定すれば実現できます。 database.xml merge=ours マージを実⾏すると、database.xmlに関する競合は発⽣せず、次のような結果になり ます。 $ git merge topic Auto-merging database.xml Merge made by recursive. 216 Scott Chacon Pro Git 7.3節 Git フック この場合、database.xmlは元々のバージョンのまま、書き変わりません。 7.3 Git フック 他のバージョンコントロールシステムと同じように、Gitにも特定のアクションが発⽣ した時にスクリプトを叩く⽅法があります。フックはクライアントサイドとサーバーサイ ドの⼆つのグループに分けられます。クライアントサイドフックはコミットやマージと いったクライアントでの操作⽤に、サーバーサイドフックはプッシュされたコミットを受 け取るといったサーバーでの操作⽤に利⽤されます。これらのフックをさまざまなな理由 に⽤いることができます。ここではそのうちのいくつかをご紹介しましょう。 7.3.1 フックをインストールする フックはGitディレクトリのhooksサブディレクトリに格納されています。⼀般的なプロ ジェクトでは、.git/hooksがそれにあたります。Gitはデフォルトでこのディレクトリに例 となるスクリプトを⽣成します。それらの多くはそのままでも⼗分有⽤ですし、引数も記 載されています。全ての例は基本的にシェルスクリプトで書かれています。いくつかPerl を含むものもありますが、適切に命名されたそれらの実⾏可能スクリプトはうまく動きま す。RubyやPython等で⾃作していただいてもかまいません。それらのフックファイルの 末尾は.sampleとなっていますので適時リネームしてください。 フックスクリプトを有効にするには、Gitディレクトリのhooksサブディレクトリに適切 な名前の実⾏可能なファイルを配置する必要があります。これによってファイルが呼び出 されることになります。ここでは重要なフックファイル名をいくつか取り上げます。 7.3.2 クライアントサイドフック クライアントサイドフックにはたくさんの種類があります。ここではコミットワークフ ローフック、Eメールワークフロースクリプト、その他クライアントサイドフックに分類 します。 コミットワークフローフック 最初の4つのフックはコミットプロセスに関するものです。pre-commitフックはコミット メッセージが⼊⼒される前に実⾏されます。これはいまからコミットされるであろうス ナップショットを検査したり、何かし忘れた事を確認したり、事前にテストを実⾏した り、何かしらコードを検査する⽬的で使⽤されます。git commit --no-verifyで回避するこ ともできますが、このフックから0でない値が返るとコミットが中断されます。コーディ ングスタイルの検査(lintを実⾏する等)や、⾏末の空⽩⽂字の検査(デフォルトのフッ クがまさにそうです)、新しく追加されたメソッドのドキュメントが正しいかどうかの検 査といったことが可能です。 prepare-commit-msgフックは、コミットメッセージエディターが起動する直前、デフォル トメッセージが⽣成された直後に実⾏されます。コミットの作者がそれを⽬にする前に デフォルトメッセージを編集することができます。このフックはオプションを必要とし ます: 現在までのコミットメッセージを保存したファイルへのパス、コミットのタイプ、 さらにamendされたコミットの場合はコミットSHA-1が必要です。このフックは普段の コミットにおいてあまり有⽤ではありませんが、テンプレートのコミットメッセージ・ 217 第7章 Git のカスタマイズ Scott Chacon Pro Git mergeコミット・squashコミット・amendコミットのようなデフォルトメッセージが⾃ 動で挿⼊されるコミットにおいて効果を発揮します。テンプレートのコミットメッセージ と組み合わせて、動的な情報をプログラムで挿⼊することができます。 commit-msgフックも、現在のコミットメッセージを保存した⼀時ファイルへのパスをパ ラメータに持つ必要があります。このスクリプトが0以外の値を返した場合Gitはコミッ トプロセスを中断しますので、プロジェクトの状態や許可待ちになっているコミットメッ セージを有効にすることができます 。この章の最後のセクションでは、このフックを使 ⽤してコミットメッセージが要求された様式に沿っているか検査するデモンストレーショ ンを⾏います。 コミットプロセスが全て完了した後に、post-commitフックが実⾏されます。パラメータ は必要無く、git log -1 HEADを実⾏することで直前のコミットを簡単に取り出すことがで きます。⼀般的にこのスクリプトは何かしらの通知といった⽬的に使⽤されます。 コミットワークフロークライアントサイドスクリプトはあらゆるワークフローに使⽤す ることができます。clone中にスクリプトが転送される事はありませんが、これらはしば しばサーバー側で決められたポリシーを強制する⽬的で使⽤されます。これらのスクリプ トは開発者を⽀援するために存在するのですから、いつでもオーバーライドされたり変更 されたりすることがありえるとしても開発者らによってセットアップされ、メンテナンス されてしかるべきです。 Eメールワークフローフック Eメールを使ったワークフロー⽤として、三種類のクライアントサイドフックを設定す ることができます。これらはすべて git am コマンドに対して起動されるものなので、ふだ んの作業でこのコマンドを使っていない場合は次のセクションを読み⾶ばしてもかまいま せん。git format-patch で作ったパッチを受け取ることがある場合は、ここで説明する内容 が有⽤になるかもしれません。 まず最初に実⾏されるフックは applypatch-msg です。これは引数をひとつだけ受け取り ます。コミットメッセージを含む⼀時ファイル名です。このスクリプトがゼロ以外の値で 終了した場合、Git はパッチの処理を強制終了させます。このフックを使うと、コミット メッセージの書式が正しいかどうかを確認したり、スクリプトで正しい書式に⼿直しした りすることができます。 git am でパッチを適⽤するときに⼆番⽬に実⾏されるフックは pre-applypatch です。こ れは引数を受け取らず、パッチが適⽤された後に実⾏されます。このフックを使うと、 パッチ適⽤後の状態をコミットする前に調べることができます。つまり、このスクリプト でテストを実⾏したり、その他の調査をしたりといったことができるということです。な にか抜けがあったりテストが失敗したりした場合はスクリプトをゼロ以外の値で終了させ ます。そうすれば、git am はパッチをコミットせずに強制終了します。 git am において最後に実⾏されるフックは post-applypatch です。これを使うと、グルー プのメンバーやそのパッチの作者に対して処理の完了を伝えることができます。このスク リプトでは、パッチの適⽤を中断させることはできません。 その他のクライアントフック pre-rebase フックは何かをリベースする前に実⾏され、ゼロ以外を返すとその処理を中 断させることができます。このフックを使うと、既にプッシュ済みのコミットのリベース を却下することができます。Gitに含まれているサンプルの pre-rebase フックがちょうど 218 Scott Chacon Pro Git 7.3節 Git フック この働きをします。ただしこのサンプルは、公開ブランチの名前が next であることを想 定したものです。実際に使っている安定版公開ブランチの名前に変更する必要があるで しょう。 git checkout が正常に終了すると、post-checkout フックが実⾏されます。これを使うと、 作業ディレクトリを⾃分のプロジェクトの環境にあわせて設定することができます。たと えば、バージョン管理対象外の巨⼤なバイナリファイルや⾃動⽣成ドキュメントなどを作 業ディレクトリに取り込むといった処理です。 最後に説明する post-merge フックは、merge コマンドが正常に終了したときに実⾏され ます。これを使うと、Git では追跡できないパーミッション情報などを作業ツリーに復元 することができます。作業ツリーに変更が加わったときに取り込みたい Git の管理対象外 のファイルの存在確認などにも使えます。 7.3.3 サーバーサイドフック クライアントサイドフックの他に、いくつかのサーバーサイドフックを使うこともでき ます。これは、システム管理者がプロジェクトのポリシーを強制させるために使うもので す。これらのスクリプトは、サーバへのプッシュの前後に実⾏されます。pre フックをゼ ロ以外の値で終了させると、プッシュを却下してエラーメッセージをクライアントに返す ことができます。つまり、プッシュに関するポリシーをここで設定することができるとい うことです。 pre-receive および post-receive クライアントからのプッシュを処理するときに最初に実⾏されるスクリプトが prereceive です。このスクリプトは、プッシュされた参照のリストを標準⼊⼒から受け取り ます。ゼロ以外の値で終了させると、これらはすべて却下されます。このフックを使う と、更新内容がすべてfast-forwardであることをチェックしたり、プッシュしてきたユー ザーがそれらのファイルに対する適切なアクセス権を持っているかを調べたりといったこ とができます。 post-receive フックは処理が終了した後で実⾏されるもので、他のサービスの更新や ユーザーへの通知などに使えます。pre-receive フックと同様、データを標準⼊⼒から受け 取ります。サンプルのスクリプトには、メーリングリストへの投稿や継続的インテグレー ションサーバーへの通知、チケット追跡システムの更新などの処理が含まれています。コ ミットメッセージを解析して、チケットのオープン・修正・クローズなどの必要性を調べ ることだってできます。このスクリプトではプッシュの処理を中断させることはできませ んが、クライアント側ではこのスクリプトが終了するまで接続を切断することができませ ん。このスクリプトで時間のかかる処理をさせるときには⼗分注意しましょう。 update update スクリプトは pre-receive スクリプトと似ていますが、プッシュしてきた⼈が更 新しようとしているブランチごとに実⾏されるという点が異なります。複数のブランチへ のプッシュがあったときに pre-receive が実⾏されるのは⼀度だけですが、update はブラ ンチ単位でそれぞれ⼀度ずつ実⾏されます。このスクリプトは、標準⼊⼒を読み込むの ではなく三つの引数を受け取ります。参照 (ブランチ) の名前、プッシュ前を指す参照の SHA-1、そしてプッシュしようとしている参照の SHA-1 です。update スクリプトをゼ 219 第7章 Git のカスタマイズ Scott Chacon Pro Git ロ以外で終了させると、その参照のみが却下されます。それ以外の参照はそのまま更新を 続⾏します。 7.4 Git ポリシーの実施例 このセクションでは、これまでに学んだ内容を使って実際に Git のワークフローを確⽴ してみます。コミットメッセージの書式をチェックし、プッシュは fast-forward 限定に し、そしてプロジェクト内の各サブディレクトリに対して特定のユーザーだけが変更を加 えられるようにするというものです。開発者に対して「なぜプッシュが却下されたのか」 を伝えるためのクライアントスクリプト、そして実際にそのポリシーを実施するための サーバースクリプトを作成します。 スクリプトは Ruby を使って書きます。その理由のひとつは私が Ruby を好きなこと、 そしてもうひとつの理由はその他のスクリプト⾔語の疑似コードとしてもそれっぽく⾒え るであろうということです。Ruby 使いじゃなくても、きっとコードの⼤まかな流れは追 えるはずです。しかし、Ruby 以外の⾔語であってもきちんと動作します。Git に同梱さ れているサンプルスクリプトはすべて Perl あるいは Bash で書かれているので、それら の⾔語のサンプルも⼤量に⾒ることができます。 7.4.1 サーバーサイドフック サーバーサイドの作業は、すべて hooks ディレクトリの update ファイルにまとめま す。update ファイルはプッシュされるブランチごとに実⾏されるもので、プッシュされ る参照と操作前のブランチのリビジョン、そしてプッシュされる新しいリビジョンを受 け取ります。また、SSH 経由でのプッシュの場合は、プッシュしたユーザーを知ること もできます。全員に共通のユーザー (『git』 など) を使って公開鍵認証をさせている場合 は、公開鍵の情報に基づいて実際のユーザーを判断して環境変数を設定するというラッ パーが必要です。ここでは、接続しているユーザー名が環境変数 $USER に格納されている ものとします。スクリプトは、まずこれらの情報を取得するところから始まります。 #!/usr/bin/env ruby refname = ARGV[0] oldrev = ARGV[1] newrev = ARGV[2] user = ENV['USER'] puts "Enforcing Policies... \n(#{refname}) (#{oldrev[0,6]}) (#{newrev[0,6]})" 特定のコミットメッセージ書式の強制 まずは、コミットメッセージを特定の書式に従わせることに挑戦してみましょう。ここ では、コミットメッセージには必ず 『ref: 1234』 形式の⽂字列を含むこと、というルー ルにします。個々のコミットをチケットシステムとリンクさせたいという意図です。やら なければならないことは、プッシュされてきた各コミットのコミットメッセージにその⽂ 220 Scott Chacon Pro Git 7.4節 Git ポリシーの実施例 字列があるかどうかを調べ、もしなければゼロ以外の値で終了してプッシュを却下するこ とです。 プッシュされたすべてのコミットの SHA-1 値を取得するには、$newrev と $oldrev の内 容を git rev-list という低レベル Git コマンドに渡します。これは基本的には git log コマ ンドのようなものですが、デフォルトでは SHA-1 値だけを表⽰してそれ以外の情報は出 ⼒しません。ふたつのコミットの間のすべてのコミットの SHA を得るには、次のような コマンドを実⾏します。 $ git rev-list 538c33..d14fc7 d14fc7c847ab946ec39590d87783c69b031bdfb7 9f585da4401b0a3999e84113824d15245c13f0be 234071a1be950e2a8d078e6141f5cd20c1e61ad3 dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a 17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475 この出⼒を受け取ってループさせて各コミットの SHA を取得し、個々のメッセージを 取り出し、正規表現でそのメッセージを調べることができます。 さて、これらのコミットからコミットメッセージを取り出す⽅法を⾒つけなければなり ません。⽣のコミットデータを取得するには、別の低レベルコマンド git cat-file を使い ます。低レベルコマンドについては第9章 で詳しく説明しますが、とりあえずはこのコマ ンドがどんな結果を返すのだけを⽰します。 $ git cat-file commit ca82a6 tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 author Scott Chacon <[email protected]> 1205815931 -0700 committer Scott Chacon <[email protected]> 1240030591 -0700 changed the version number SHA-1 値がわかっているときにコミットからコミットメッセージを得るシンプルな⽅ 法は、空⾏を探してそれ以降をすべて取得するというものです。これには、Unix システ ムの sed コマンドが使えます。 $ git cat-file commit ca82a6 | sed '1,/^$/d' changed the version number この呪⽂を使ってコミットメッセージを取得し、もし条件にマッチしないものがあれば 終了させればよいのです。スクリプトを抜けてプッシュを却下するには、ゼロ以外の値で 終了させます。以上を踏まえると、このメソッドは次のようになります。 221 第7章 Git のカスタマイズ Scott Chacon Pro Git $regex = /\[ref: (\d+)\]/ # enforced custom commit message format def check_message_format missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("\n") missed_revs.each do |rev| message = `git cat-file commit #{rev} | sed '1,/^$/d'` if !$regex.match(message) puts "[POLICY] Your message is not formatted correctly" exit 1 end end end check_message_format これを update スクリプトに追加すると、ルールを守らないコミットメッセージが含まれ るコミットのプッシュを却下するようになります。 ユーザーベースのアクセス制御 アクセス制御リスト (ACL) を使って、ユーザーごとにプロジェクトのどの部分を変更 できるのかを指定できるようにしてみましょう。全体にアクセスできるユーザーもいれ ば、特定のサブディレクトリやファイルだけにしか変更をプッシュできないユーザーもい る、といった仕組みです。これを実施するには、ルールを書いたファイル acl をサーバー 上のベア Git リポジトリに置きます。update フックにこのファイルを読ませ、プッシュさ れたコミットにどのファイルが含まれているのかを調べ、そしてプッシュしたユーザーが それらのファイルを変更する権限があるのかどうかを判断します。 まずは ACL を作るところから始めましょう。ここでは、CVS の ACL と似た書式を使 います。これは各項⽬を⼀⾏で表すもので、最初のフィールドは avail あるいは unavail、 そして次の⾏がそのルールを適⽤するユーザーの⼀覧 (カンマ区切り)、そして最後の フィールドがそのルールを適⽤するパス (ブランクは全体へのアクセスを意味します) で す。フィールドの区切りには、パイプ⽂字 (|) を使います。 ここでは、全体にアクセスする管理者と doc ディレクトリにアクセスするドキュメン ト担当者、そして lib と tests サブディレクトリだけにアクセスできる開発者を設定しま す。ACL ファイルは次のようになります。 avail|nickh,pjhyett,defunkt,tpw avail|usinclair,cdickens,ebronte|doc avail|schacon|lib avail|schacon|tests まずはこのデータを読み込んで、スクリプト内で使えるデータ構造にしてみましょう。 例をシンプルにするために、ここでは avail ディレクティブだけを使います。次のメソッ 222 Scott Chacon Pro Git 7.4節 Git ポリシーの実施例 ドは連想配列を返すものです。ユーザー名が配列のキー、そのユーザーが書き込み権を持 つパスの配列が対応する値となります。 def get_acl_access_data(acl_file) # read in ACL data acl_file = File.read(acl_file).split("\n").reject { |line| line == '' } access = {} acl_file.each do |line| avail, users, path = line.split('|') next unless avail == 'avail' users.split(',').each do |user| access[user] ||= [] access[user] << path end end access end 先ほどの ACL ファイルをこの get_acl_access_data メソッドに渡すと、このようなデー タ構造を返します。 {"defunkt"=>[nil], "tpw"=>[nil], "nickh"=>[nil], "pjhyett"=>[nil], "schacon"=>["lib", "tests"], "cdickens"=>["doc"], "usinclair"=>["doc"], "ebronte"=>["doc"]} これで権限がわかったので、あとはプッシュされた各コミットがどのパスを変更しよう としているのかを調べれば、そのユーザーがプッシュすることができるのかどうかを判断 できます。 あるコミットでどのファイルが変更されるのかを知るのはとても簡単で、git log コマ ンドに --name-only オプションを指定するだけです (第2章 で簡単に説明しました)。 $ git log -1 --name-only --pretty=format:'' 9f585d README lib/test.rb 223 第7章 Git のカスタマイズ Scott Chacon Pro Git get_acl_access_data メソッドが返す ACL のデータとこのファイルリストを付き合わせれ ば、そのユーザーがコミットをプッシュする権限があるかどうかを判断できます。 # only allows certain users to modify certain subdirectories in a project def check_directory_perms access = get_acl_access_data('acl') # see if anyone is trying to push something they can't new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n") new_commits.each do |rev| files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n") files_modified.each do |path| next if path.size == 0 has_file_access = false access[$user].each do |access_path| if !access_path || # user has access to everything (path.index(access_path) == 0) # access to this path has_file_access = true end end if !has_file_access puts "[POLICY] You do not have access to push to #{path}" exit 1 end end end end check_directory_perms それほど難しい処理ではありません。まず最初に git rev-list でコミットの⼀覧を取得 し、それぞれに対してどのファイルが変更されるのかを調べ、ユーザーがそのファイルを 変更する権限があることを確かめています。Ruby を知らない⼈にはわかりにくいところ があるとすれば path.index(access_path) == 0 でしょうか。これは、パスが access_path で始 まるときに真となります。つまり、access_path がパスの⼀部に含まれるのではなく、パス がそれで始まっているということを確認しています。 これで、まずい形式のコミットメッセージや権利のないファイルの変更を含むコミット はプッシュできなくなりました。 Fast-Forward なプッシュへの限定 最後は、fast-forward なプッシュに限るという仕組みです。 receive.denyDeletes および receive.denyNonFastForwards という設定項⽬で設定できます。また、フックを⽤いてこの制限 を課すこともできますし、特定のユーザーにだけこの制約を加えたいなどといった変更に も対応できます。 224 Scott Chacon Pro Git 7.4節 Git ポリシーの実施例 これを調べるには、旧リビジョンからたどれるすべてのコミットについて、新リビジョ ンから到達できないものがないかどうかを探します。もしひとつもなければ、それは fast-forward なプッシュです。ひとつでも⾒つかれば、却下することになります。 # enforces fast-forward only pushes def check_fast_forward missed_refs = `git rev-list #{$newrev}..#{$oldrev}` missed_ref_count = missed_refs.split("\n").size if missed_ref_count > 0 puts "[POLICY] Cannot push a non fast-forward reference" exit 1 end end check_fast_forward これですべてがととのいました。これまでのコードを書き込んだファイルに対して chmod u+x .git/hooks/update を実⾏し、fast-forward ではない参照をプッシュしてみましょ う。すると、こんなメッセージが表⽰されるでしょう。 $ git push -f origin master Counting objects: 5, done. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 323 bytes, done. Total 3 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. Enforcing Policies... (refs/heads/master) (8338c5) (c5b616) [POLICY] Cannot push a non fast-forward reference error: hooks/update exited with error code 1 error: hook declined to update refs/heads/master To git@gitserver:project.git ! [remote rejected] master -> master (hook declined) error: failed to push some refs to 'git@gitserver:project.git' この中には、いくつか興味深い点があります。まず、フックの実⾏が始まったときの次 の表⽰に注⽬しましょう。 Enforcing Policies... (refs/heads/master) (8338c5) (c5b616) 225 第7章 Git のカスタマイズ Scott Chacon Pro Git これは、スクリプトの先頭で標準出⼒に表⽰した内容でした。ここで重要なのは「スク リプトから標準出⼒に送った内容は、すべてクライアントにも送られる」ということで す。 次に注⽬するのは、エラーメッセージです。 [POLICY] Cannot push a non fast-forward reference error: hooks/update exited with error code 1 error: hook declined to update refs/heads/master 最初の⾏はスクリプトから出⼒したもので、その他の 2 ⾏は Git が出⼒したものです。 この 2 ⾏では、スクリプトがゼロ以外の値で終了したためにプッシュが却下されたとい うことを説明しています。最後に、次の部分に注⽬します。 To git@gitserver:project.git ! [remote rejected] master -> master (hook declined) error: failed to push some refs to 'git@gitserver:project.git' フックで却下したすべての参照について、remote rejected メッセージが表⽰されま す。これを⾒れば、フック内での処理のせいで却下されたのだということがわかります。 さらに、もしコミットメッセージに適切な ref が含まれていなければ、それを⽰す次の ようなエラーメッセージが表⽰されるでしょう。 [POLICY] Your message is not formatted correctly また、変更権限のないファイルを変更してそれを含むコミットをプッシュしようとした ときも、同様にエラーが表⽰されます。たとえば、ドキュメント担当者が lib ディレクト リ内の何かを変更しようとした場合のメッセージは次のようになります。 [POLICY] You do not have access to push to lib/test.rb 以上です。この update スクリプトが動いてさえいれば、もう⼆度とリポジトリが汚され ることはありません。コミットメッセージは決まりどおりのきちんとしたものになるし、 ユーザーに変なところをさわられる⼼配もなくなります。 7.4.2 クライアントサイドフック この⽅式の弱点は、プッシュが却下されたときにユーザーが泣き寝⼊りせざるを得なく なるということです。⼿間暇かけて仕上げた作業が最後の最後で却下されるというのは、 ⾮常にストレスがたまるし不可解です。プッシュするためには歴史を修正しなければなら ないのですが、気弱な⼈にとってそれはかなりつらいことです。 226 Scott Chacon Pro Git 7.4節 Git ポリシーの実施例 このジレンマに対する答えとして、サーバーが却下するであろう作業をするときにそれ をユーザーに伝えるためのクライアントサイドフックを⽤意します。そうすれば、何か問 題があるときにそれをコミットする前に知ることができるので、取り返しのつかなくなる 前に問題を修正することができます。プロジェクトをクローンしてもフックはコピーされ ないので、別の何らかの⽅法で各ユーザーにスクリプトを配布しなければなりません。各 ユーザーはそれを .git/hooks にコピーし、実⾏可能にします。フックスクリプト⾃体をプ ロジェクトに含めたり別のプロジェクトにしたりすることはできますが、各⾃の環境でそ れをフックとして⾃動的に設定することはできないのです。 はじめに、コミットを書き込む直前にコミットメッセージをチェックしなければなり ません。そして、サーバーに却下されないようにコミットメッセージの書式を調べるの です。そのためには commit-msg フックを使います。最初の引数で渡されたファイルからコ ミットメッセージを読み込んでパターンと⽐較し、もしマッチしなければ Git の処理を中 断させます。 #!/usr/bin/env ruby message_file = ARGV[0] message = File.read(message_file) $regex = /\[ref: (\d+)\]/ if !$regex.match(message) puts "[POLICY] Your message is not formatted correctly" exit 1 end このスクリプトを適切な場所 (.git/hooks/commit-msg) に置いて実⾏可能にしておくと、 不適切なメッセージを書いてコミットしようとしたときに次のような結果となります。 $ git commit -am 'test' [POLICY] Your message is not formatted correctly このとき、実際にはコミットされません。もしメッセージが適切な書式になっていれ ば、Git はコミットを許可します。 $ git commit -am 'test [ref: 132]' [master e05c914] test [ref: 132] 1 files changed, 1 insertions(+), 0 deletions(-) 次に、ACL で決められた範囲以外のファイルを変更していないことを確認しましょ う。先ほど使った ACL ファイルのコピーがプロジェクトの .git ディレクトリにあれば、 次のような pre-commit スクリプトでチェックすることができます。 227 第7章 Git のカスタマイズ Scott Chacon Pro Git #!/usr/bin/env ruby $user = ENV['USER'] # [ insert acl_access_data method from above ] # only allows certain users to modify certain subdirectories in a project def check_directory_perms access = get_acl_access_data('.git/acl') files_modified = `git diff-index --cached --name-only HEAD`.split("\n") files_modified.each do |path| next if path.size == 0 has_file_access = false access[$user].each do |access_path| if !access_path || (path.index(access_path) == 0) has_file_access = true end if !has_file_access puts "[POLICY] You do not have access to push to #{path}" exit 1 end end end check_directory_perms ⼤まかにはサーバーサイドのスクリプトと同じですが、重要な違いがふたつあります。 まず、ACL ファイルの場所が違います。このスクリプトは作業ディレクトリから実⾏す るものであり、Git ディレクトリから実⾏するものではないからです。ACL ファイルの場 所を、先ほどの access = get_acl_access_data('acl') から次のように変更しなければなりません。 access = get_acl_access_data('.git/acl') もうひとつの違いは、変更されたファイルの⼀覧を取得する⽅法です。サーバーサイド のメソッドではコミットログを調べていました。しかしこの時点ではまだコミットが記録 228 Scott Chacon Pro Git 7.4節 Git ポリシーの実施例 されていないので、ファイルの⼀覧はステージング・エリアから取得しなければなりませ ん。つまり、先ほどの files_modified = `git log -1 --name-only --pretty=format:'' #{ref}` は次のようになります。 files_modified = `git diff-index --cached --name-only HEAD` しかし、違うのはこの⼆点だけ。それ以外はまったく同じように動作します。ただ、こ のスクリプトは、ローカルで実⾏しているユーザーとリモートマシンにプッシュするとき のユーザーが同じであることを前提にしています。もし異なる場合は、変数 $user を⼿動 で設定しなければなりません。 最後に残ったのは fast-forward でないプッシュを⽌めることですが、これは多少特殊 です。fast-forward でない参照を取得するには、すでにプッシュした過去のコミットにリ ベースするか、別のローカルブランチにリモートブランチと同じところまでプッシュしな ければなりません。 サーバーサイドでは fast-forward ではないプッシュをできないようにしているので、 それ以外にあり得るのは、すでにプッシュ済みのコミットをリベースしようとするときく らいです。 それをチェックする pre-rebase スクリプトの例を⽰します。これは書き換えようとし ているコミットの⼀覧を取得し、それがリモート参照の中に存在するかどうかを調べま す。リモート参照から到達可能なコミットがひとつでもあれば、リベースを中断します。 #!/usr/bin/env ruby base_branch = ARGV[0] if ARGV[1] topic_branch = ARGV[1] else topic_branch = "HEAD" end target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n") remote_refs = `git branch -r`.split("\n").map { |r| r.strip } target_shas.each do |sha| remote_refs.each do |remote_ref| shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}` if shas_pushed.split("\n").include?(sha) puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}" 229 第7章 Git のカスタマイズ Scott Chacon Pro Git exit 1 end end end このスクリプトでは、第6章 の「リビジョンの選択」ではカバーしていない構⽂を使っ ています。既にプッシュ済みのコミットの⼀覧を得るために、次のコマンドを実⾏しま す。 git rev-list ^#{sha}^@ refs/remotes/#{remote_ref} SHAˆ@ 構⽂は、そのコミットのすべての親を解決します。リモートの最後のコミットか ら到達可能で、これからプッシュしようとするコミットの親のいずれかからアクセスでき ないコミットを探します。 この⽅式の弱点は⾮常に時間がかかることで、多くの場合このチェックは不要です。-f つきで強制的にプッシュしようとしない限り、サーバーが警告を出してプッシュできない からです。しかし練習⽤の課題としてはおもしろいもので、あとでリベースを取り消して やりなおすはめになることを理屈上は防げるようになります。 7.5 まとめ Git クライアントとサーバーをカスタマイズして⾃分たちのプロジェクトやワークフ ローにあてはめるための主要な⽅法を説明しました。あらゆる設定項⽬やファイルベース の属性、そしてイベントフックについて学び、特定のポリシーを実現するサーバーを構築 するサンプルを⽰しました。これで、あなたが思い描くであろうほぼすべてのワークフ ローにあわせて Git を調整できるようになったはずです。 230 第8章 Gitとその他のシステムの連携 世の中はそんなにうまくいくものではありません。あなたが関わることになったプロ ジェクトで使うバージョン管理システムを、すぐさまGitに切り替えられることはほとん どないでしょう。また、関わっているプロジェクトが他のVCSを使っていることも時々あ るでしょうし、多くの場合 Subversion が使われているのではないかと思います。この章 の前半では、まず Subversion と Git を繋ぐ双⽅向ゲートウェイである git svn について 説明します。 どこかの時点で、プロジェクトで Git を使うようにしたくなることもあるでしょう。 この章の後半では、プロジェクトのVCSを Git へ移⾏する⽅法について説明します。 Subversion と Perforce からの移⾏について説明したあと、特殊なケースにおいてスク リプトを使ったインポートの⽅法を説明します。 8.1 Git と Subversion 現在のところ、オープンソースや企業のプロジェクトの⼤多数が、ソースコードの管理 に Subversion を利⽤しています。Subversion は最も⼈気のあるオープンソースのVCS で、10年近く前から使われています。Subversion 以前は CVS がソースコード管理に広 く⽤いられていたのですが、多くの点で両者はよく似ています。 Git の素晴しい機能のひとつに、Git と Subversion を双⽅向にブリッジする git svn が あります。このツールを使うと、Subversion のクライアントとして Git を使うことがで きます。つまり、ローカルの作業では Git の機能を⼗分に活⽤することができて、あたか も Subversion を使っているかのように Subversion サーバーに変更をコミットすること ができます。共同作業をしている⼈達が古き良き⽅法を使っているのと 同時に、ローカ ルでのブランチ作成やマージ、ステージング・エリア、リベース、チェリーピックなどの Git の機能を使うことができるということです。共同の作業環境に Git を忍び込ませてお いて、仲間の開発者たちが Git より効率良く作業できるように⼿助けをしつつ、Git の全 ⾯的な採⽤のための根回しをしてゆく、というのが賢いやり⽅です。Subversion ブリッ ジは、分散VCS の素晴しい世界へのゲートウェイ・ドラッグといえるでしょう。 8.1.1 git svn Git と Subversion の橋渡しをするコマンド群のベースとなるコマンドが git svn です。 すべてはここから始めることができます。この後に続くコマンドはかなりたくさんあるの で、いくつかのワークフローを通して⼀般的なものから⾝につけていきましょう。 231 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git 注意すべきことは、git svn を使っているときは Subversion を相⼿にしているのだとい うことです。これは、Git ほど洗練されてはいません。ローカルでのブランチ作成やマー ジは簡単にできますが、作業内容をリベースするなどして歴史をできるだけ⼀直線に保つ ようにし、Git リモートリポジトリを相⼿にするときのように考えるのは避けましょう。 歴史を書き換えてもう⼀度プッシュしようなどとしてはいけません。また、他の開発者 との共同作業のために複数の Git リポジトリに並⾏してプッシュするのもいけません。 Subversion が扱えるのは⼀本の直線上の歴史だけで、ちょっとしたことですぐに混乱し てしまいます。チームのメンバーの中に SVN を使う⼈と Git を使う⼈がいる場合は、全 員が SVN サーバーを使って共同作業するようにしましょう。そうすれば、少しは⽣きや すくなります。 8.1.2 準備 この機能を説明するには、書き込みアクセス権を持つ標準的な SVN リポジトリが必 要です。もしこのサンプルをコピーして試したいのなら、私のテスト⽤リポジトリの書 き込み可能なコピーを作らなければなりません。これを簡単に⾏うには、svnsync という ツールを使います。最近のバージョンの Subversion、少なくとも 1.4 以降に付属してい るツールです。テスト⽤として、新しい Subversion リポジトリを Google code 上に作 りました。これは protobuf プロジェクトの⼀部で、protobuf は構造化されたデータを符号 化してネットワーク上で転送するためのツールです。 まずはじめに、新しいローカル Subversion リポジトリを作ります。 $ mkdir /tmp/test-svn $ svnadmin create /tmp/test-svn そして、すべてのユーザーが revprop を変更できるようにします。簡単な⽅法は、常 に 0 で終了する pre-revprop-change スクリプトを追加することです。 $ cat /tmp/test-svn/hooks/pre-revprop-change #!/bin/sh exit 0; $ chmod +x /tmp/test-svn/hooks/pre-revprop-change これで、ローカルマシンにこのプロジェクトを同期できるようになりました。同期元と 同期先のリポジトリを指定して svnsync init を実⾏します。 $ svnsync init file:///tmp/test-svn http://progit-example.googlecode.com/svn/ このコマンドは、同期を実⾏するためのプロパティを設定します。次に、このコマンド でコードをコピーします。 232 Scott Chacon Pro Git 8.1節 Git と Subversion $ svnsync sync file:///tmp/test-svn Committed revision 1. Copied properties for revision 1. Committed revision 2. Copied properties for revision 2. Committed revision 3. ... この操作は数分で終わりますが、もし元のリポジトリのコピー先がローカルではなく別 のリモートリポジトリだった場合、この処理には約⼀時間かかります。総コミット数はた かだか 100 にも満たないにもかかわらず。Subversion では、リビジョンごとにクローン を作ってコピー先のリポジトリに投⼊していかなければなりません。これはばかばかしい ほど⾮効率的ですが、簡単に済ませるにはこの⽅法しかないのです。 8.1.3 はじめましょう 書き込み可能な Subversion リポジトリが⼿に⼊ったので、⼀般的なワークフローに 沿って進めましょう。まずは git svn clone コマンドを実⾏します。このコマンドは、 Subversion リポジトリ全体をローカルの Git リポジトリにインポートします。どこかに ホストされている実際の Subversion リポジトリから取り込む場合は file:///tmp/test-svn の部分を Subversion リポジトリの URL に変更しましょう。 $ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags Initialized empty Git repository in /Users/schacon/projects/testsvnsync/svn/.git/ r1 = b4e387bc68740b5af56c2a5faf4003ae42bd135c (trunk) A m4/acx_pthread.m4 A m4/stl_hash.m4 ... r75 = d1957f3b307922124eec6314e15bcda59e3d9610 (trunk) Found possible branch point: file:///tmp/test-svn/trunk => \ file:///tmp/test-svn /branches/my-calc-branch, 75 Found branch parent: (my-calc-branch) d1957f3b307922124eec6314e15bcda59e3d9610 Following parent with do_switch Successfully followed parent r76 = 8624824ecc0badd73f40ea2f01fce51894189b01 (my-calc-branch) Checked out HEAD: file:///tmp/test-svn/branches/my-calc-branch r76 これは、指定した URL に対して git svn init に続けて git svn fetch を実⾏するのと同じ 意味です。しばらく時間がかかります。test プロジェクトには 75 のコミットしかなくて コードベースもそれほど⼤きくないので、数分しかかかりません。しかし、Git は各バー ジョンをそれぞれチェックアウトしては個別にコミットしています。もし数百数千のコ 233 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git ミットがあるプロジェクトで試すと、終わるまでには数時間から下⼿をすると数⽇かかっ てしまうかもしれません。 -T trunk -b branches -t tags の部分は、この Subversion リポジトリが標準的なブランチ とタグの規約に従っていることを表しています。trunk、branches、tags にもし別の名前 をつけているのなら、この部分を変更します。この規約は⼀般に使われているものなの で、単に -s とだけ指定することもできます。これは、先の 3 つのオプションを指定した のと同じ標準のレイアウトを表します。つまり、次のようにしても同じ意味になるという ことです。 $ git svn clone file:///tmp/test-svn -s これで、ブランチやタグも取り込んだ Git リポジトリができあがりました。 $ git branch -a * master my-calc-branch tags/2.0.2 tags/release-2.0.1 tags/release-2.0.2 tags/release-2.0.2rc1 trunk このツールがリモート参照を取り込むときの名前空間が通常と異なることに注意しま しょう。Git リポジトリのクローンを作成した場合は、リモートサーバー上のすべてのブ ランチが origin/[branch] のような形式で取り込まれます。つまりリモートの名前で名前空 間が作られます。しかし、git svn はリモートが複数あることを想定しておらず、すべて のリモートサーバーを名前空間なしに保存します。Git のコマンド show-ref を使うと、す べての参照名を完全な形式で⾒ることができます。 $ git show-ref 1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/heads/master aee1ecc26318164f355a883f5d99cff0c852d3c4 refs/remotes/my-calc-branch 03d09b0e2aad427e34a6d50ff147128e76c0e0f5 refs/remotes/tags/2.0.2 50d02cc0adc9da4319eeba0900430ba219b9c376 refs/remotes/tags/release-2.0.1 4caaa711a50c77879a91b8b90380060f672745cb refs/remotes/tags/release-2.0.2 1c4cb508144c513ff1214c3488abe66dcb92916f refs/remotes/tags/release-2.0.2rc1 1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/remotes/trunk 通常の Git リポジトリは、このようになります。 234 Scott Chacon Pro Git 8.1節 Git と Subversion $ git show-ref 83e38c7a0af325a9722f2fdc56b10188806d83a1 refs/heads/master 3e15e38c198baac84223acfc6224bb8b99ff2281 refs/remotes/gitserver/master 0a30dd3b0c795b80212ae723640d4e5d48cabdff refs/remotes/origin/master 25812380387fdd55f916652be4881c6f11600d6f refs/remotes/origin/testing 2 つのリモートサーバーがあり、⼀⽅の gitserver には master ブランチが、そしてもう ⼀⽅の origin には master と testing の 2 つのブランチがあります。 サンプルのリモート参照が git svn でどのように取り込まれたかに注⽬しましょう。 タグはリモートブランチとして取り込まれており、Git のタグにはなっていません。 Subversion から取り込んだ内容は、まるで tags という名前のリモートからブランチを取 り込んだように⾒えます。 8.1.4 Subversion へのコミットの書き戻し 作業リポジトリを⼿に⼊れたあなたはプロジェクト上で何らかの作業を終え、コミット を上流に書き戻すことになりました。Git を SVN クライアントとして使います。どれか ひとつのファイルを変更してコミットした時点では、Git上でローカルに存在するそのコ ミットはSubversionサーバー上には存在しません。 $ git commit -am 'Adding git-svn instructions to the README' [master 97031e5] Adding git-svn instructions to the README 1 files changed, 1 insertions(+), 1 deletions(-) 次に、これをプッシュして上流を変更しなければなりません。この変更が Subversion に対してどのように作⽤するのかに注意しましょう。オフラインで⾏った複数のコミッ トを、すべて⼀度に Subversion サーバーにプッシュすることができます。Subversion サーバーにプッシュするには git svn dcommit コマンドを使います。 $ git svn dcommit Committing to file:///tmp/test-svn/trunk ... M README.txt Committed r79 M README.txt r79 = 938b1a547c2cc92033b74d32030e86468294a5c8 (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk このコマンドは、Subversionサーバーからのコード上で⾏われたすべてのコミットに 対して個別に Subversion 上にコミットし、ローカルの Git のコミットを書き換えて⼀意 な識別⼦を含むようにします。ここで重要なのは、書き換えによってすべてのローカルコ 235 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git ミットの SHA-1 チェックサムが変化するということです。この理由もあって、Git ベー スのリモートリポジトリにあるプロジェクトと Subversion サーバーを同時に使うことは おすすめできません。直近のコミットを調べれば、新たに git-svn-id が追記されたことが わかります。 $ git log -1 commit 938b1a547c2cc92033b74d32030e86468294a5c8 Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029> Date: Sat May 2 22:06:44 2009 +0000 Adding git-svn instructions to the README git-svn-id: file:///tmp/test-svn/trunk@79 4c93b258-373f-11de-be05-5f7a86268029 元のコミットの SHA チェックサムが 97031e5 で始まっていたのに対して今は 938b1a5 に 変わっていることに注⽬しましょう。Git と Subversion の両⽅のサーバーにプッシュし たい場合は、まず Subversion サーバーにプッシュ (dcommit) してから Git のほうにプッ シュしなければなりません。dcommit でコミットデータが書き換わるからです。 8.1.5 新しい変更の取り込み 複数の開発者と作業をしていると、遅かれ早かれ、誰かがプッシュしたあとに他の⼈が プッシュしようとして衝突を起こすということが発⽣します。他の⼈の作業をマージする まで、その変更は却下されます。git svn では、このようになります。 $ git svn dcommit Committing to file:///tmp/test-svn/trunk ... Merge conflict during commit: Your file or directory 'README.txt' is probably \ out-of-date: resource out of date; try updating at /Users/schacon/libexec/git-\ core/git-svn line 482 この状態を解決するには git svn rebase を実⾏します。これは、サーバー上の変更のう ちまだ取り込んでいない変更をすべて取り込んでから、⾃分の作業をリベースします。 $ git svn rebase M README.txt r80 = ff829ab914e8775c7c025d741beb3d523ee30bc4 (trunk) First, rewinding head to replay your work on top of it... Applying: first user change これで⼿元の作業が Subversion サーバー上の最新状態の上でなされたことになったの で、無事に dcommit することができます。 236 Scott Chacon Pro Git 8.1節 Git と Subversion $ git svn dcommit Committing to file:///tmp/test-svn/trunk ... M README.txt Committed r81 M README.txt r81 = 456cbe6337abe49154db70106d1836bc1332deed (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk ここで注意すべき点は、Git の場合は上流での変更をすべてマージしてからでなけれ ばプッシュできないけれど、git svn の場合は衝突さえしなければマージしなくてもプッ シュできるということです。だれかがあるファイルを変更した後で⾃分が別のファイルを 変更してプッシュしても、dcommit は正しく動作します。 $ git svn dcommit Committing to file:///tmp/test-svn/trunk ... M configure.ac Committed r84 M autogen.sh r83 = 8aa54a74d452f82eee10076ab2584c1fc424853b (trunk) M configure.ac r84 = cdbac939211ccb18aa744e581e46563af5d962d0 (trunk) W: d2f23b80f67aaaa1f6f5aaef48fce3263ac71a92 and refs/remotes/trunk differ, \ using rebase: :100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 \ 015e4c98c482f0fa71e4d5434338014530b37fa6 M autogen.sh First, rewinding head to replay your work on top of it... Nothing to do. これは忘れずに覚えておきましょう。というのも、プッシュした後の結果はどの開発者 の作業環境にも存在しない状態になっているからです。たまたま衝突しなかっただけで 互換性のない変更をプッシュしてしまったときに、その問題を⾒つけるのが難しくなりま す。これが、Git サーバーを使う場合と異なる点です。Git の場合はクライアントの状態 をチェックしてからでないと変更を公開できませんが、SVN の場合はコミットの直前と コミット後の状態が同等であるかどうかすら確かめられないのです。 もし⾃分のコミット準備がまだできていなくても、Subversion から変更を取り込むと きにもこのコマンドを使わなければなりません。git svn fetch でも新しいデータを取得す ることはできますが、git svn rebase はデータを取得するだけでなくローカルのコミット の更新も⾏います。 237 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git $ git svn rebase M generate_descriptor_proto.sh r82 = bd16df9173e424c6f52c337ab6efa7f7643282f1 (trunk) First, rewinding head to replay your work on top of it... Fast-forwarded master to refs/remotes/trunk. git svn rebase をときどき実⾏しておけば、⼿元のコードを常に最新の状態に保ってお けます。しかし、このコマンドを実⾏するときには作業ディレクトリがクリーンな状態 であることを確認しておく必要があります。⼿元で変更をしている場合は、stash で作業 を退避させるか⼀時的にコミットしてからでないと git svn rebase を実⾏してはいけませ ん。さもないと、もしリベースの結果としてマージが衝突すればコマンドの実⾏が⽌まっ てしまいます。 8.1.6 Git でのブランチに関する問題 Git のワークフローに慣れてくると、トピックブランチを作ってそこで作業を⾏い、そ れをマージすることもあるでしょう。git svn を使って Subversion サーバーにプッシュ する場合は、それらのブランチをまとめてプッシュするのではなく⼀つのブランチ上に リベースしてからプッシュしたくなるかもしれません。リベースしたほうがよい理由は、 Subversion はリニアに歴史を管理していて Git のようなマージができないからです。git svn がスナップショットを Subversion のコミットに変換するときには、最初の親だけに 続けます。 歴史が次のような状態になっているものとしましょう。experiment ブランチを作ってそ こで 2 回のコミットを済ませ、それを master にマージしたところです。ここで dcommit す ると、出⼒はこのようになります。 $ git svn dcommit Committing to file:///tmp/test-svn/trunk ... M CHANGES.txt Committed r85 M CHANGES.txt r85 = 4bfebeec434d156c36f2bcd18f4e3d97dc3269a2 (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk COPYING.txt: locally modified INSTALL.txt: locally modified M COPYING.txt M INSTALL.txt Committed r86 M INSTALL.txt M COPYING.txt r86 = 2647f6b86ccfcaad4ec58c520e369ec81f7c283c (trunk) No changes between current HEAD and refs/remotes/trunk 238 Scott Chacon Pro Git 8.1節 Git と Subversion Resetting to the latest refs/remotes/trunk 歴史をマージしたブランチで dcommit を実⾏してもうまく動作します。ただし、Git プ ロジェクト上での歴史を⾒ると、experiment ブランチ上でのコミットは書き換えられてい ません。そこでのすべての変更は、SVN 上での単⼀のマージコミットとなっています。 他の⼈がその作業をクローンしたときには、すべての作業をひとまとめにしたマージコ ミットしか⾒ることができません。そのコミットがどこから来たのか、そしていつコミッ トされたのかを知ることができないのです。 8.1.7 Subversion のブランチ Subversion のブランチは Git のブランチとは異なります。可能ならば、Subversion の ブランチは使わないようにするのがベストでしょう。しかし、Subversion のブランチの 作成やコミットも、git svn を使ってすることができます。 新しい SVN ブランチの作成 Subversion に新たなブランチを作るには git svn branch [branchname] を実⾏します。 $ git svn branch opera Copying file:///tmp/test-svn/trunk at r87 to file:///tmp/test-svn/branches/opera... Found possible branch point: file:///tmp/test-svn/trunk => \ file:///tmp/test-svn/branches/opera, 87 Found branch parent: (opera) 1f6bfe471083cbca06ac8d4176f7ad4de0d62e5f Following parent with do_switch Successfully followed parent r89 = 9b6fe0b90c5c9adf9165f700897518dbc54a7cbf (opera) これは Subversion の svn copy trunk branches/opera コマンドと同じ意味で、Subversion サーバー上で実⾏されます。ここで注意すべき点は、このコマンドを実⾏しても新しいブ ランチに⼊ったことにはならないということです。この後コミットをすると、そのコミッ トはサーバーの trunk に対して⾏われます。opera ではありません。 8.1.8 アクティブなブランチの切り替え Git が dcommit の⾏き先のブランチを決めるときには、あなたの⼿元の歴史上にある Subversion ブランチのいずれかのヒントを使います。⼿元にはひとつしかないはずで、 それは現在のブランチの歴史上の直近のコミットにある git-svn-id です。 複数のブランチを同時に操作するときは、ローカルブランチを dcommit でその Subversion ブランチにコミットするのかを設定することができます。そのためには、Subversion のブランチをインポートしてローカルブランチを作ります。opera ブランチを個別に 操作したい場合は、このようなコマンドを実⾏します。 239 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git $ git branch opera remotes/opera これで、opera ブランチを trunk (⼿元の master ブランチ) にマージするときに通常の git merge が使えるようになりました。しかし、そのときには適切なコミットメッセージを (-m で) 指定しなければなりません。さもないと、有⽤な情報ではなく単なる 『Merge branch opera』 というメッセージになってしまいます。 git merge を使ってこの操作を⾏ったとしても、そしてそれが Subversion でのマージよ りもずっと簡単だったとしても (Git は⾃動的に適切なマージベースを検出してくれるか らね)、これは通常の Git のマージコミットとは違うということを覚えておきましょう。 このデータを Subversion に書き戻すことになりますが Subversion では複数の親を持つ コミットは処理できません。そのため、プッシュした後は、別のブランチ上で⾏ったす べての操作をひとまとめにした単⼀のコミットに⾒えてしまいます。あるブランチを別 のブランチにマージしたら、元のブランチに戻って作業を続けるのは困難です。Git なら 簡単なのですが。dcommit コマンドを実⾏すると、どのブランチからマージしたのかとい う情報はすべて消えてしまいます。そのため、それ以降のマージ元の算出は間違ったも のとなります。dcommit は、git merge の結果をまるで git merge --squash を実⾏したの と同じ状態にしてしまうのです。残念ながら、これを回避するよい⽅法はありません。 Subversion 側にこの情報を保持する⽅法がないからです。Subversion をサーバーに使 う以上は、常にこの制約に縛られることになります。問題を回避するには、trunk にマー ジしたらローカルブランチ (この場合は opera) を削除しなければなりません。 8.1.9 Subversion コマンド git svn ツールセットには、Git への移⾏をしやすくするための多くのコマンドが⽤意さ れています。Subversion で使い慣れていたのと同等の機能を提供するコマンド群です。 その中からいくつかを紹介します。 SVN 形式のログ Subversion に慣れているので SVN が出⼒する形式で歴史を⾒たい、という場合は git svn log を実⾏しましょう。すると、コミットの歴史が SVN 形式で表⽰されます。 $ git svn log -----------------------------------------------------------------------r87 | schacon | 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009) | 2 lines autogen change -----------------------------------------------------------------------r86 | schacon | 2009-05-02 16:00:21 -0700 (Sat, 02 May 2009) | 2 lines Merge branch 'experiment' 240 Scott Chacon Pro Git 8.1節 Git と Subversion -----------------------------------------------------------------------r85 | schacon | 2009-05-02 16:00:09 -0700 (Sat, 02 May 2009) | 2 lines updated the changelog git svn log に関して知っておくべき重要なことがふたつあります。まず。このコマンド はオフラインで動作します。実際の svn log コマンドのように Subversion サーバーにデー タを問い合わせたりしません。次に、すでに Subversion サーバーにコミット済みのコ ミットしか表⽰されません。つまり、ローカルの Git へのコミットのうちまだ dcommit していないものは表⽰されないし、その間に他の⼈が Subversion サーバーにコミットし た内容も表⽰されません。最後に Subversion サーバーの状態を調べたときのログが表⽰ されると考えればよいでしょう。 SVN アノテーション git svn log コマンドが svn log コマンドをオフラインでシミュレートしているのと同様 に、svn annotate と同様のことを git svn blame [FILE] で実現できます。出⼒は、このよう になります。 $ git svn blame README.txt 2 temporal Protocol Buffers - Google's data interchange format 2 temporal Copyright 2008 Google Inc. 2 temporal http://code.google.com/apis/protocolbuffers/ 2 temporal 22 temporal C++ Installation - Unix 22 temporal ======================= 2 temporal 79 schacon Committing in git-svn. 78 schacon 2 temporal To build and install the C++ Protocol Buffer runtime and the Protocol 2 temporal Buffer compiler (protoc) execute the following: 2 temporal 先ほどと同様、このコマンドも Git にローカルにコミットした内容や他から Subversion にプッシュされていたコミットは表⽰できません。 SVN サーバ情報 svn info と同様のサーバー情報を取得するには git svn info を実⾏します。 $ git svn info Path: . URL: https://schacon-test.googlecode.com/svn/trunk 241 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git Repository Root: https://schacon-test.googlecode.com/svn Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029 Revision: 87 Node Kind: directory Schedule: normal Last Changed Author: schacon Last Changed Rev: 87 Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009) blame や log と同様にこれもオフラインで動作し、最後に Subversion サーバーと通信し た時点での情報しか表⽰されません。 Subversion が無視するものを無視する どこかに svn:ignore プロパティが設定されている Subversion リポジトリをクローンし た場合は、対応する .gitignore ファイルを⽤意したくなることでしょう。コミットすべき ではないファイルを誤ってコミットしてしまうことを防ぐためにです。git svn には、この 問題に対応するためのコマンドが⼆つ⽤意されています。まず最初が git svn create-ignore で、これは、対応する .gitignore ファイルを⾃動⽣成して次のコミットに含めます。 もうひとつは git svn show-ignore で、これは .gitignore に書き込む内容を標準出⼒に送 ります。この出⼒を、プロジェクトの exclude ファイルにリダイレクトしましょう。 $ git svn show-ignore > .git/info/exclude これで、プロジェクトに .gitignore ファイルを散らかさなくてもよくなります。Subversion 使いのチームの中で Git を使うのが⾃分だけだという場合、他のメンバーにとっ ては .gitignore ファイルは⽬障りでしょう。そのような場合はこの⽅法が使えます。 8.1.10 Git-Svn のまとめ git svn ツール群は、Subversion サーバーに⾏き詰まっている場合や使っている開発環 境が Subversion サーバー前提になっている場合などに便利です。Git のできそこないだ と感じるかもしれません。また、他のメンバーとの間で混乱が起こるかもしれません。ト ラブルを避けるために、次のガイドラインに従いましょう。 • Git の歴史をリニアに保ち続け、git merge によるマージコミットを含めないように する。本流以外のブランチでの作業を書き戻すときは、マージではなくリベースす ること。 • Git サーバーを別途⽤意したりしないこと、新しい開発者がクローンするときのス ピードをあげるためにサーバーを⽤意することはあるでしょうが、そこに git-svn-id エントリを持たないコミットをプッシュしてはいけません。pre-receive フックを追 加してコミットメッセージをチェックし、git-svn-id がなければプッシュを拒否する ようにしてもよいでしょう。 242 Scott Chacon Pro Git 8.2節 Git への移⾏ これらのガイドラインを守れば、Subversion サーバーでの作業にも耐えられることで しょう。しかし、もし本物の Git サーバーに移⾏できるのなら、そうしたほうがチームに とってずっと利益になります。 8.2 Git への移行 別の VCS で管理している既存のコードベースを Git で管理しようと思ったら、何らか の⽅法でそのプロジェクトを移⾏しなければなりません。この節では、⼀般的なシステム 上の Git に含まれているインポートツールについて説明します。そして、インポートツー ルを⾃作する⽅法も扱います。 8.2.1 インポート ここでは、業務のソースコード管理に使われる2⼤ツールである Subversion と Perforce からデータをインポートする⽅法を説明します。現在 Git への移⾏を考えている⼈ たちの多くがこれらを使っていると聞いています。そのため、これらからのインポート⽤ に、Git には⾼品質のツールが付属しています。 8.2.2 Subversion 先ほどの節で git svn の使い⽅を読んでいれば、話は簡単です。まず git svn clone でリ ポジトリを作り、そして Subversion サーバーを使うのをやめ、新しい Git サーバーに プッシュし、あとはそれを使い始めればいいのです。これまでの歴史が欲しいのなら、そ れも Subversion サーバーからプルすることができます (多少時間がかかります)。 しかし、インポートは完全ではありません。また時間もかかるので、正しくやるのが いいでしょう。まず最初に問題になるのが作者 (author) の情報です。Subversion ではコ ミットした⼈すべてがシステム上にユーザーを持っており、それがコミット情報として記 録されます。たとえば先ほどの節のサンプルで⾔うと schacon がそれで、blame の出⼒や git svn log の出⼒に含まれています。これをうまく Git の作者データとしてマップするに は、Subversion のユーザーと Git の作者のマッピングが必要です。users.txt という名前 のファイルを作り、このような書式でマッピングを記述します。 schacon = Scott Chacon <[email protected]> selse = Someo Nelse <[email protected]> SVN で使っている作者の⼀覧を取得するには、このようにします。 $ svn log ^/ --xml | grep -P "^<author" | sort -u | \ perl -pe 's/<author>(.*?)<\/author>/$1 = /' > users.txt これは、まずログを XML フォーマットで出⼒します。その中から作者を捜して重複 を省き、XML を除去します (ちょっと⾒ればわかりますが、これは grep や sort、そして 243 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git perl といったコマンドが使える環境でないと動きません)。この出⼒を users.txt にリダイ レクトし、そこに Git のユーザーデータを書き⾜していきます。 このファイルを git svn に渡せば、作者のデータをより正確にマッピングできるように なります。また、Subversion が通常インポートするメタデータを含めないよう git svn に 指⽰することもできます。そのためには --no-metadata を clone コマンドあるいは init コマ ンドに渡します。そうすると、 import コマンドは次のようになります。 $ git svn clone http://my-project.googlecode.com/svn/ \ --authors-file=users.txt --no-metadata -s my_project これで、Subversion をちょっとマシにインポートした my_project ディレクトリができ あがりました。コミットがこんなふうに記録されるのではなく、 commit 37efa680e8473b615de980fa935944215428a35a Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029> Date: Sun May 3 00:12:22 2009 +0000 fixed install - go to trunk git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11debe05-5f7a86268029 次のように記録されています。 commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2 Author: Scott Chacon <[email protected]> Date: Sun May 3 00:12:22 2009 +0000 fixed install - go to trunk Author フィールドの⾒た⽬がずっとよくなっただけではなく、git-svn-id もなくなって います。 インポートした後に、ちょっとした後始末が必要です。たとえば、git svn が準備した 変な参照などです。まずはタグを移動して、奇妙なリモートブランチではなくちゃんとし たタグとして扱えるようにします。そして、残りのブランチを移動してローカルで扱える ようにします。 タグを Git のタグとして扱うには、次のコマンドを実⾏します。 $ git for- each- ref refs/ remotes/ tags | cut - d / - f 4- | grep - v @ | while read tagname; do git tag "$tagname" "tags/$tagname"; git branch -r -d "tags/ $tagname"; done 244 Scott Chacon Pro Git 8.2節 Git への移⾏ これは、リモートブランチのうち tag/ で始まる名前のものを、実際の (軽量な) タグに 変えます。 次に、refs/remotes 以下にあるそれ以外の参照をローカルブランチに移動します。 $ git for- each- ref refs/ remotes | cut - d / - f 3- | grep - v @ | while read branchname; do git branch "$branchname" "refs/remotes/$branchname"; git branch r -d "$branchname"; done これで、今まであった古いブランチはすべて Git のブランチとなり、古いタグもすべて Git のタグになりました。最後に残る作業は、新しい Git サーバーをリモートに追加して プッシュすることです。⾃分のサーバーをリモートとして追加するには以下のようにしま す。 $ git remote add origin git@my-git-server:myrepository.git すべてのブランチやタグを⼀緒にプッシュするには、このようにします。 $ git push origin --all $ git push origin --tags これで、ブランチやタグも含めたすべてを、新しい Git サーバーにきれいにインポート できました。 8.2.3 Perforce 次のインポート元としてとりあげるのは Perforce です。Perforce からのインポート ツールも Git に同梱されています。ただし、使⽤しているGitのバージョンが1.7.11より古 い場合は同梱されておらず、Gitソースコードの contrib から取り出す必要があります。 ソースコードは git.kernel.org からダウンロードできます。 $ git clone git://git.kernel.org/pub/scm/git/git.git $ cd git/contrib/fast-import この fast-import ディレクトリにある実⾏可能な Python スクリプト git-p4 が、それで す。このツールを使うには、Python と p4 ツールがマシンにインストールされていなけれ ばなりません。たとえば、Jam プロジェクトを Perforce Public Depot からインポート します。クライアントをセットアップするには、環境変数 P4PORT をエクスポートして Perforce depot の場所を指すようにしなければなりません。 245 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git $ export P4PORT=public.perforce.com:1666 git-p4 clone コマンドを実⾏して Jam プロジェクトを Perforce サーバーからインポー トし、depot とプロジェクトそしてプロジェクトの取り込み先のパスを指定します。 $ git-p4 clone //public/jam/src@all /opt/p4import Importing from //public/jam/src@all into /opt/p4import Reinitialized existing Git repository in /opt/p4import/.git/ Import destination: refs/remotes/p4/master Importing revision 4409 (100%) /opt/p4import ディレクトリに移動して git log を実⾏すると、インポートされた内容を ⾒ることができます。 $ git log -2 commit 1fd4ec126171790efd2db83548b85b1bbbc07dc2 Author: Perforce staff <[email protected]> Date: Thu Aug 19 10:18:45 2004 -0800 Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into the main part of the document. Built new tar/zip balls. Only 16 months later. [git-p4: depot-paths = "//public/jam/src/": change = 4409] commit ca8870db541a23ed867f38847eda65bf4363371d Author: Richard Geiger <[email protected]> Date: Tue Apr 22 20:51:34 2003 -0800 Update derived jamgram.c [git-p4: depot-paths = "//public/jam/src/": change = 3108] git-p4 という識別⼦が各コミットに含まれることがわかるでしょう。この識別⼦はその ままにしておいてもかまいません。後で万⼀ Perforce のチェンジ番号を参照しなければ ならなくなったときのために使えます。しかし、もし削除したいのならここで消しておき ましょう。新しいリポジトリ上で何か作業を始める前のこの段階で。git filter-branch を 使えば、この識別⼦を⼀括削除することができます。 246 Scott Chacon Pro Git 8.2節 Git への移⾏ $ git filter-branch --msg-filter ' sed -e "/^\[git-p4:/d" ' Rewrite 1fd4ec126171790efd2db83548b85b1bbbc07dc2 (123/123) Ref 'refs/heads/master' was rewritten git log を実⾏すれば各コミットの SHA-1 チェックサムがすべて変わったことがわかり ます。そして git-p4 ⽂字列はコミットメッセージから消えました。 $ git log -2 commit 10a16d60cffca14d454a15c6164378f4082bc5b0 Author: Perforce staff <[email protected]> Date: Thu Aug 19 10:18:45 2004 -0800 Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into the main part of the document. Built new tar/zip balls. Only 16 months later. commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2 Author: Richard Geiger <[email protected]> Date: Tue Apr 22 20:51:34 2003 -0800 Update derived jamgram.c これで、インポートした内容を新しい Git サーバーにプッシュする準備がととのいまし た。 8.2.4 カスタムインポーター Subversion や Perforce 以外のシステムを使っている場合は、それ⽤のインポートツー ルを探さなければなりません。CVS、Clear Case、Visual Source Safe、あるいはアーカ イブのディレクトリなどのためのツールはオンラインで公開されています。これらのツー ルがうまく動かなかったり⼿元で使っているバージョン管理ツールがもっとマイナーなも のだったり、あるいはインポート処理で特殊な操作をしたりしたい場合は git fast-import を使います。このコマンドはシンプルな指⽰を標準⼊⼒から受け取って、特定の Git デー タを書き出します。⽣の Git コマンドを使ったり⽣のオブジェクトを書きだそうとしたり する (詳細は第9章 を参照ください) よりもずっと簡単に Git オブジェクトを作ることが できます。この⽅法を使えばインポートスクリプトを⾃作することができます。必要な情 報を元のシステムから読み込み、単純な指⽰を標準出⼒に出せばよいのです。そして、こ のスクリプトの出⼒をパイプで git fast-import に送ります。 ⼿軽に試してみるために、シンプルなインポーターを書いてみましょう。currentで 作業をしており、プロジェクトのバックアップはディレクトリまるごとのコピーで⾏っ 247 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git ているものとします。バックアップディレクトリの名前は、タイムスタンプをもとに back_YYYY_MM_DD としています。これらを Git にインポートしてみましょう。ディレクトリ の構造は、このようになっています。 $ ls /opt/import_from back_2009_01_02 back_2009_01_04 back_2009_01_14 back_2009_02_03 current Git のディレクトリにインポートするにはまず、これらのデータをどのように Git に格 納するかをレビューしなければなりません。Git は基本的にはコミットオブジェクトのリ ンクリストであり、コミットオブジェクトがコンテンツのスナップショットを指していま す。fast-import に指⽰しなければならないのは、コンテンツのスナップショットが何でど のコミットデータがそれを指しているのかということと、コミットデータを取り込む順番 だけです。ここでは、スナップショットをひとつずつたどって各ディレクトリの中⾝をさ すコミットオブジェクトを作り、それらを⽇付順にリンクさせるものとします。 第7章 の「Git ポリシーの実施例」同様、ここでも Ruby を使って書きます。ふだんか ら使いなれており、きっと他の⽅にも読みやすいであろうからです。このサンプルをあ なたの使いなれた⾔語で書き換えるのも簡単でしょう。単に適切な情報を標準出⼒に送 るだけなのだから。また、Windows を使っている場合は、⾏末にキャリッジリターンを 含めないように注意が必要です。git fast-import が想定している⾏末は LF だけであり、 Windows で使われている CRLF は想定していません。 まず最初に対象ディレクトリに移動し、コミットとしてインポートするスナップショッ トとしてサブディレクトリを識別します。基本的なメインループは、このようになりま す。 last_mark = nil # 各ディレクトリをループ Dir.chdir(ARGV[0]) do Dir.glob("*").each do |dir| next if File.file?(dir) # 対象ディレクトリに移動 Dir.chdir(dir) do last_mark = print_export(dir, last_mark) end end end 各ディレクトリ内で実⾏している print_export は、前のスナップショットの内容とマー 248 Scott Chacon Pro Git 8.2節 Git への移⾏ クを受け取ってこのディレクトリの内容とマークを返します。このようにして、それぞれ を適切にリンクさせます。「マーク」とは fast-import ⽤語で、コミットに対する識別⼦ を意味します。コミットを作成するときにマークをつけ、それを使って他のコミットとリ ンクさせます。つまり、print_export メソッドで最初にやることは、ディレクトリ名から マークを⽣成することです。 mark = convert_dir_to_mark(dir) これを⾏うには、まずディレクトリの配列を作り、そのインデックスの値をマークとし て使います。マークは整数値でなければならないからです。メソッドの中⾝はこのように なります。 $marks = [] def convert_dir_to_mark(dir) if !$marks.include?(dir) $marks << dir end ($marks.index(dir) + 1).to_s end これで各コミットを整数値で表せるようになりました。次に必要なのは、コミットのメ タデータ⽤の⽇付です。⽇付はディレクトリ名で表されているので、ここから取得しま す。print_export ファイルで次にすることは、これです。 date = convert_dir_to_date(dir) convert_dir_to_date の定義は次のようになります。 def convert_dir_to_date(dir) if dir == 'current' return Time.now().to_i else dir = dir.gsub('back_', '') (year, month, day) = dir.split('_') return Time.local(year, month, day).to_i end end これは、各ディレクトリの⽇付に対応する整数値を返します。コミットのメタ情報とし て必要な最後の情報はコミッターのデータで、これはグローバル変数にハードコードしま す。 249 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git $author = 'Scott Chacon <[email protected]>' これで、コミットのデータをインポーターに流せるようになりました。最初の情報で⽰ しているのは、今定義しているのがコミットオブジェクトであることとどのブランチにい るのかを表す宣⾔です。その後に先ほど⽣成したマークが続き、さらにコミッターの情報 とコミットメッセージが続いた後にひとつ前のコミットが (もし存在すれば) 続きます。 コードはこのようになります。 # インポート情報の表示 puts 'commit refs/heads/master' puts 'mark :' + mark puts "committer #{$author} #{date} -0700" export_data('imported from ' + dir) puts 'from :' + last_mark if last_mark タイムゾーン (-0700) をハードコードしているのは、そのほうがお⼿軽だったからで す。別のシステムからインポートする場合は、タイムゾーンを適切に指定しなければなり ません。コミットメッセージは、次のような特殊な書式にする必要があります。 data (size)\n(contents) まず最初に「data」という単語、そして読み込むデータのサイズ、改⾏、最後にデータ がきます。同じ書式は後でファイルのコンテンツを指定するときにも使うので、ヘルパー メソッド export_data を作ります。 def export_data(string) print "data #{string.size}\n#{string}" end 残っているのは、各スナップショットが持つファイルのコンテンツを指定することで す。今回の場合はどれも⼀つのディレクトリにまとまっているので簡単です。deleteall コ マンドを表⽰し、それに続けてディレクトリ内の各ファイルの中⾝を表⽰すればよいので す。そうすれば、Git が各スナップショットを適切に記録します。 puts 'deleteall' Dir.glob("**/*").each do |file| next if !File.file?(file) inline_data(file) end 250 Scott Chacon Pro Git 8.2節 Git への移⾏ 注意: 多くのシステムではリビジョンを「あるコミットと別のコミットの差分」と考え ているので、fast-importでもその形式でコマンドを受け取ることができます。つまりコ ミットを指定するときに、追加/削除/変更されたファイルと新しいコンテンツの中⾝で 指定できるということです。各スナップショットの差分を算出してそのデータだけを渡 すこともできますが、処理が複雑になります。すべてのデータを渡して、Git に差分を算 出させたほうがよいでしょう。もし差分を渡すほうが⼿元のデータに適しているような ら、fast-import のマニュアルで詳細な⽅法を調べましょう。 新しいファイルの内容、あるいは変更されたファイルと変更後の内容を表す書式は次の ようになります。 M 644 inline path/to/file data (size) (file contents) この 644 はモード (実⾏可能ファイルがある場合は、そのファイルについては 755 を 指定する必要があります) を表し、inline とはファイルの内容をこの次の⾏に続けて指定 するという意味です。inline_data メソッドは、このようになります。 def inline_data(file, code = 'M', mode = '644') content = File.read(file) puts "#{code} #{mode} inline #{file}" export_data(content) end 先ほど定義した export_data メソッドを再利⽤することができます。この書式はコミッ トメッセージの書式と同じだからです。 最後に必要となるのは、現在のマークを返して次の処理に渡せるようにすることです。 return mark 注意: Windows 上で動かす場合はさらにもう⼀⼿間必要です。先述したように、 Windows の改⾏⽂字は CRLF ですが git fast-import は LF にしか対応していません。こ の問題に対応して git fast-import をうまく動作させるには、CRLF ではなく LF を使うよ う ruby に指⽰しなければなりません。 $stdout.binmode これで終わりです。このスクリプトを実⾏すれば、次のような結果が得られます。 251 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git $ ruby import.rb /opt/import_from commit refs/heads/master mark :1 committer Scott Chacon <[email protected]> 1230883200 -0700 data 29 imported from back_2009_01_02deleteall M 644 inline file.rb data 12 version two commit refs/heads/master mark :2 committer Scott Chacon <[email protected]> 1231056000 -0700 data 29 imported from back_2009_01_04from :1 deleteall M 644 inline file.rb data 14 version three M 644 inline new.rb data 16 new version one (...) インポーターを動かすには、インポート先の Git レポジトリにおいて、インポーターの 出⼒をパイプで git fast-import に渡す必要があります。インポート先に新しいディレクト リを作成したら、以下のように git init を実⾏し、そしてスクリプトを実⾏してみましょ う。 $ git init Initialized empty Git repository in /opt/import_to/.git/ $ ruby import.rb /opt/import_from | git fast-import git-fast-import statistics: --------------------------------------------------------------------Alloc'd objects: Total objects: 18 ( 1 duplicates ) blobs : 7 ( 1 duplicates 0 deltas) trees : 6 ( 0 duplicates 1 deltas) commits: 5 ( 0 duplicates 0 deltas) tags 0 ( 0 duplicates 0 deltas) 1 ( 1 loads ) 5 unique ) : Total branches: 252 5000 marks: 1024 ( atoms: 3 Scott Chacon Pro Git 8.2節 Git への移⾏ Memory total: 2255 KiB pools: 2098 KiB objects: 156 KiB --------------------------------------------------------------------pack_report: getpagesize() = 4096 pack_report: core.packedGitWindowSize = 33554432 pack_report: core.packedGitLimit = 268435456 pack_report: pack_used_ctr = 9 pack_report: pack_mmap_calls = 5 pack_report: pack_open_windows = 1 / 1 pack_report: pack_mapped = 1356 / 1356 --------------------------------------------------------------------- ご覧のとおり、処理が正常に完了すると、処理内容に関する統計情報が表⽰されます。 この場合は、全部で 18 のオブジェクトからなる 5 つのコミットが 1 つのブランチにイン ポートされたことがわかります。では、git log で新しい歴史を確認しましょう。 $ git log -2 commit 10bfe7d22ce15ee25b60a824c8982157ca593d41 Author: Scott Chacon <[email protected]> Date: Sun May 3 12:57:39 2009 -0700 imported from current commit 7e519590de754d079dd73b44d695a42c9d2df452 Author: Scott Chacon <[email protected]> Date: Tue Feb 3 01:00:00 2009 -0700 imported from back_2009_02_03 きれいな Git リポジトリができていますね。ここで重要なのは、この時点ではまだ何も チェックアウトされていないということです。作業ディレクトリには何もファイルがあり ません。ファイルを取得するには、ブランチをリセットして master の現在の状態にしなけ ればなりません。 $ ls $ git reset --hard master HEAD is now at 10bfe7d imported from current $ ls file.rb lib fast-import ツールにはさらに多くの機能があります。さまざまなモードを処理したりバ 253 第8章 Gitとその他のシステムの連携 Scott Chacon Pro Git イナリデータを扱ったり、複数のブランチやそのマージ、タグ、進捗状況表⽰などです。 より複雑なシナリオのサンプルは Git のソースコードの contrib/fast-import ディレクトリ にあります。先ほど取り上げた git-p4 スクリプトがよい例となるでしょう。 8.3 まとめ Git を Subversion と組み合わせて使う⽅法を説明しました。また、既存のリポジトリ のほぼすべてを、データを失うことなく新たな Git リポジトリにインポートできるように なりました。次章では、Git の内部に踏み込みます。必要とあらばバイト単位での操作も できることでしょう。 254 第9章 Gitの内側 あなたは前の章を⾶ばしてこの章に来たのでしょうか、あるいは、この本の他の部分を 読んだ後で来たのでしょうか。いずれにせよ、この章ではGit の内部動作と実装を辿って いくことになります。内部動作と実装を学ぶことは、Git がどうしてこんなに便利で有効 なのかを根本的に理解するのに重要です。しかし初⼼者にとっては不必要に複雑で混乱を 招いてしまうという⼈もいました。そのため、遅かれ早かれ学習の仕⽅に合わせて読める ように、この話題を最後の章に配置しました。いつ読むかって? それは読者の判断にお 任せします。 もう既にあなたはこの章を読んでいますので、早速、開始しましょう。まず、基本的に Git は連想記憶ファイル・システム(content-addressable filesystem)であり、その上 にVCS ユーザー・インターフェイスが記述されているのです。これが意味することを、 もう少し⾒て⾏きましょう。 初期のGit(主として1.5以前)は、洗練されたVCS というよりもむしろファイル・シ ステムであることを(Gitの特徴として)強調しており、それ故に、ユーザー・インター フェイスは今よりも複雑なものでした。ここ数年の間に、世の中のどのシステムにも劣ら ないほどGit のユーザー・インターフェイスはシンプルで扱いやすいものに改良されまし た。しかし、複雑で学習するのが難しいという初期のGit に対する型にはまったイメージ はまだ残っています。 連想記憶ファイル・システム層は驚くほど素晴らしいので、この章の最初にそれをカ バーすることにします。その次に転送メカニズムと、今後あなたが⾏う必要があるかもし れないリポジトリの保守作業について学習することにします。 9.1 配管(Plumbing)と磁器(Porcelain) 本書は、checkout や branch、remote などの約30のコマンドを⽤いて、Git の使い⽅を説 明しています。ですが、Git は元々、完全にユーザフレンドリーなバージョン管理システ ムというよりもむしろ、バージョン管理システムのためのツール類でした。そのため、 下位レベルの仕事を⾏うためのコマンドが沢⼭あり、UNIXの形式(またはスクリプト から呼ばれる形式)と密に関わりながら設計されました。これらのコマンドは、通常は 『配管(plumbing)』 コマンドと呼ばれ、よりユーザフレンドリーなコマンドは 『磁器 (porcelain)』 コマンドと呼ばれます。 本書のはじめの8つの章は、ほぼ例外なく磁器コマンドを取り扱いますが、本章では下 位レベルの配管コマンドを専ら使⽤することになります。なぜなら、それらのコマンド 255 第9章 Gitの内側 Scott Chacon Pro Git は、Gitの内部動作にアクセスして、Gitの内部で、何を、どのように、どうして⾏うのか を確かめるのに役に⽴つからです。それらのコマンドは、コマンドラインから実⾏するの に使⽤されるのではなく、むしろ新規のツールとカスタムスクリプトのための構成要素 (building blocks)として使⽤されます。 新規の、または既存のディレクトリで git init を実⾏すると、Git は .git というディ レクトリを作ります。Git が保管して操作するほとんどすべてのものがそこに格納されま す。もしもレポジトリをバックアップするかクローンを作りたいなら、この1つのディレ クトリをどこかにコピーすることで、必要とするほとんどすべてのことが満たされます。 この章では全体を通して、.git ディレクトリの中を基本的に取り扱います。その中は以下 のようになっています。 $ ls HEAD branches/ config description hooks/ index info/ objects/ refs/ これは git init を実⾏した直後のデフォルトのレポジトリです。それ以外の場合は、他 にも幾つかのファイルがそこに⾒つかるかもしれません。branches ディレクトリは、新し いバージョンのGitでは使⽤されません。description ファイルは、GitWeb プログラムの みで使⽤します。そのため、それらについての配慮は不要です。config ファイルには、あ なたのプロジェクト固有の設定オプションが含まれます。info ディレクトリは、追跡され ている .gitignore ファイルには記述したくない無視パターンを書くための、グローバルレ ベルの除外設定ファイルを保持します。hooks ディレクトリには、あなたのクライアント サイド、または、サーバサイドのフックスクリプトが含まれます。それについての詳細は 7章に記述されています。 残りの4つ(HEAD ファイルと index ファイル、また、objects ディレクトリと refs ディ レクトリ)は重要なエントリです。これらは、Git の中核(コア)の部分に相当しま す。objects ディレクトリはあなたのデータベースのすべてのコンテンツを保管しま す。refs ディレクトリは、そのデータ(ブランチ)内のコミットオブジェクトを指すポイ ンターを保管します。HEAD ファイルは、現在チェックアウトしているブランチを指しま す。index ファイルは、Git がステージングエリアの情報の保管する場所を⽰します。これ から各セクションで、Git がどのような仕組みで動くのかを詳細に⾒ていきます。 9.2 Gitオブジェクト Git は連想記憶ファイル・システムです。素晴らしい。…で、それはどういう意味なの でしょう?それは、Git のコアの部分が単純なキーバリューから成り⽴つデータストアで ある、という意味です。hash-object という配管コマンドを使⽤することで、それを実際に 256 Scott Chacon Pro Git 9.2節 Gitオブジェクト お⾒せすることができます。そのコマンドはあるデータを取り出して、それを .git ディ レクトリに格納し、そのデータが格納された場所を⽰すキーを返します。まずは、初期化 された新しいGit レポジトリには objects ディレクトリが存在しないことを確認します。 $ mkdir test $ cd test $ git init Initialized empty Git repository in /tmp/test/.git/ $ find .git/objects .git/objects .git/objects/info .git/objects/pack $ find .git/objects -type f $ Git は objects ディレクトリを初期化して、その中に pack と info というサブディレクト リを作ります。しかし、ファイルはひとつも作られません。今から Git データベースに幾 つかのテキストを格納してみます。 $ echo 'test content' | git hash-object -w --stdin d670460b4b4aece5915caf5c68d12f560a9fe3e4 -w オプションは hash-object に、オブジェクトを格納するように伝えます。-w オプショ ンを付けない場合、コマンドはただオブジェクトのキーが何かを伝えます。--stdin オプ ションは、標準⼊⼒からコンテンツを読み込むようにコマンドに伝えます。これを指定 しない場合、hash-object はファイルパスを探そうとします。コマンドを実⾏すると、40 ⽂字から成るチェックサムのハッシュ値が出⼒されます。これは、SHA-1ハッシュです。 (後ほど知ることになりますが、これは格納するコンテンツにヘッダーを加えたデータに 対するチェックサムです)これでGitがデータをどのようにして格納するかを知ることが できました。 $ find .git/objects -type f .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 ひとつのファイルが objectsディレクトリの中にあります。このようして Git は、最初 にコンテンツを格納します。ひとつの部分のコンテンツにつき 1ファイルで、コンテンツ とそのヘッダーに対する SHA-1のチェックサムを⽤いたファイル名で格納します。サブ ディレクトリは、SHA-1ハッシュのはじめの2⽂字で名付けられ、残りの38⽂字でファイ ル名が決まります。 cat-file コマンドを使って、コンテンツを Git の外に引き出すことができます。これは Git オブジェクトを調べることにおいて、cat-file は万能ナイフ(Swiss army knife)の 257 第9章 Gitの内側 Scott Chacon Pro Git ような便利なコマンドです。-p オプションを付けると、cat-file コマンドはコンテンツの タイプをわかりやすく表⽰してくれます。 $ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4 test content これであなたは Git にコンテンツを追加し、それを再び外に引き出すことができるよう になりました。複数のファイルがあるコンテンツに対してもこれと同様のことを⾏うこと ができます。例えば、あるファイルに対して幾つかの簡単なバージョン管理⾏うことがで きます。まず、新規にファイルを作成し、あなたのデータベースにそのコンテンツを保存 します。 $ echo 'version 1' > test.txt $ git hash-object -w test.txt 83baae61804e65cc73a7201a7252750c76066a30 それから、幾つか新しいコンテンツをそのファイルに書き込んで、再び保存します。 $ echo 'version 2' > test.txt $ git hash-object -w test.txt 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a データベースには、そこに格納した最初のコンテンツのバージョンに加えて、そのファ イルの新しいバージョンが⼆つ追加されています。 $ find .git/objects -type f .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 これで、そのファイルを最初のバージョンに復帰(revert)することができます。 $ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt $ cat test.txt version 1 あるいは、⼆つ⽬のバージョンに。 258 Scott Chacon Pro Git 9.2節 Gitオブジェクト $ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt $ cat test.txt version 2 しかし、それぞれのファイルのバージョンの SHA-1キーを覚えることは実⽤的ではあ りません。加えて、あなたはコンテンツのみを格納していてファイル名はシステム内に格 納していません。このオブジェクトタイプはブロブ(blob)と呼ばれます。cat-file -t コ マンドに SHA-1キーを渡すことで、あなたは Git 内にあるあらゆるオブジェクトのタイ プを問い合わせることができます。 $ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob 9.2.1 ツリーオブジェクト 次のタイプはツリーオブジェクトです。これは、ファイル名の格納の問題を解決して、 さらに、あるグループに属するファイル群を⼀緒に格納します。Git がコンテンツを格納 する⽅法は、UNIXのファイルシステムに似ていますが少し簡略されています。すべての コンテンツはツリーとブロブのオブジェクトとして格納されます。ツリーは UNIXのディ レクトリエントリーに対応しており、ブロブは幾分かは iノード またはファイルコンテン ツに対応しています。1つのツリーオブジェクトは1つ以上のツリーエントリーを含んで いて、またそれらのツリーは、それに関連するモード、タイプ、そしてファイル名と⼀緒 に、ブロブまたはサブツリーへの SHA-1ポインターを含んでいます。例えば、最も単純 なプロジェクトの最新のツリーはこのように⾒えるかもしれません。 $ git cat-file -p master^{tree} 100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README 100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile 040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib masterˆ{tree} のシンタックスは、master ブランチ上での最後のコミットによってポイン トされたツリーオブジェクトを⽰します。lib サブディレクトリがブロブではなく、別の ツリーへのポインタであることに注意してください。 $ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb 概念的に、Git が格納するデータは図9-1のようなものです。 独⾃のツリーを作ることも可能です。Git は通常、ステージングエリアもしくはイン デックスの状態を取得することによってツリーを作成し、 そこからツリーオブジェクト 259 第9章 Gitの内側 Scott Chacon Pro Git 図 9.1: Gitデータモデルの簡略版 を書き込みます。そのため、ツリーオブジェクトを作るには、まず幾つかのファイルを ステージングしてインデックスをセットアップしなければなりません。 test.txt ファイル の最初のバージョンである単⼀エントリーのインデックスを作るには、update-index とい う配管コマンドを使います。 前バージョンの test.txt ファイルを新しいステージングエ リアに⼈為的に追加するにはこのコマンドを使います。 ファイルはまだステージングエ リアには存在しない(未だステージングエリアをセットアップさえしていない)ので、-add オプションを付けなければなりません。 また、追加しようとしているファイルはディ レクトリには無くデータベースにあるので、--cacheinfoオプションを付ける必要がありま す。 その次に、モードと SHA-1、そしてファイル名を指定します。 $ git update-index --add --cacheinfo 100644 \ 83baae61804e65cc73a7201a7252750c76066a30 test.txt この例では、100644 のモードを指定しています。これは、それが通常のファイルであ ることを意味します。他には、実⾏可能ファイルであることを意味する 100755 や、シン ボリックリンクであることを⽰す 120000 のオプションがあります。このモードは通常の UNIX モードから取り⼊れた概念ですが融通性はもっと劣ります。これら三つのモード は、(他のモードはディレクトリとサブモジュールに使⽤されますが)Git のファイル (ブロブ)に対してのみ有効です。 これであなたは write-tree コマンドを使って、ステージングエリアをツリーオブジェク トに書き出すことができます。-w オプションは⼀切必要とされません。write-tree コマン ドを呼ぶことで、ツリーがまだ存在しない場合に、⾃動的にインデックスの状態からツ リーオブジェクトを作ります。 $ git write-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 $ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579 100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt また、これがツリーオブジェクトであることを検証することができます。 260 Scott Chacon Pro Git 9.2節 Gitオブジェクト $ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree これから、⼆つ⽬のバージョンの test.txt に新しいファイルを加えて新しくツリーを作 ります。 $ echo 'new file' > new.txt $ git update-index test.txt $ git update-index --add new.txt これでステージングエリアには、new.txt という新しいファイルに加えて、新しいバー ジョンの test.txt を持つようになります。(ステージングエリアまたはインデックスの状 態を記録している)そのツリーを書き出してみると、以下のように⾒えます。 $ git write-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 $ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt このツリーは両⽅のファイルエントリを持っていて、さらに、test.txt の SHA-1ハッ シュは最初の⽂字(1f7a7a)から 『バージョン2』 の SHA-1ハッシュとなっていることに 注意してください。ちょっと試しに、最初のツリーをサブディレクトリとしてこの中の1 つに追加してみましょう。read-tree を呼ぶことで、ステージングエリアの中にツリーを読 み込むことができます。このケースでは、--prefix オプションを付けて read-tree コマンド 使⽤することで、ステージングエリアの中に既存のツリーを、サブツリーとして読み込む ことができます。 $ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579 $ git write-tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614 $ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614 040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt 先ほど書き込んだ新しいツリーから作業ディレクトリを作っていれば、⼆つのファイル が作業ディレクトリのトップレベルに⾒つかり、また、最初のバージョンの test.txt ファ イルが含まれている bak という名前のサブディレクトリが⾒つかります。これらの構造の 261 第9章 Gitの内側 Scott Chacon Pro Git ために Git がデータをどのように含めているかは、図9-2のようにイメージすることがで きます。 図 9.2: 現在のGitデータのコンテンツ構造 9.2.2 コミットオブジェクト 追跡(track)したいと思うプロジェクトの異なるスナップショットを特定するための ツリーが三つありますが、前の問題が残っています。スナップショットを呼び戻すために は3つすべての SHA-1 の値を覚えなければならない、という問題です。さらに、あなた はそれらのスナップショットがいつ、どのような理由で、誰が保存したのかについての情 報を⼀切持っておりません。これはコミットオブジェクトがあなたのために保持する基本 的な情報です。 コミットオブジェクトを作成するには、単⼀ツリーの SHA-1 と、もしそれに直に先⾏ して作成されたコミットオブジェクトがあれば、それらを指定して commit-tree を呼びま す。あなたが書き込んだ最初のツリーから始めましょう。 $ echo 'first commit' | git commit-tree d8329f fdf4fc3344e67ab068f836878b6c4951e3b15f3d これで cat-file コマンドを呼んで新しいコミットオブジェクトを⾒ることができます。 $ git cat-file -p fdf4fc3 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 author Scott Chacon <[email protected]> 1243040974 -0700 committer Scott Chacon <[email protected]> 1243040974 -0700 first commit コミットオブジェクトの形式はシンプルです。それはプロジェクトのその時点のスナッ プショットに対して、トップレベルのツリーを指定します。その時点のスナップショッ トには、現在のタイムスタンプと共に user.name と user.email の設定から引き出された作 262 Scott Chacon Pro Git 9.2節 Gitオブジェクト 者(author)/コミッター(committer)の情報、ブランクライン、そしてコミットメッ セージが含まれます。 次に、あなたは⼆つのコミットオブジェクトを書き込みます。各コミットオブジェクト はその直前に来たコミットを参照しています。 $ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3 cac0cab538b970a37ea1e769cbbde608743bc96d $ echo 'third commit' | git commit-tree 3c4e9c -p cac0cab 1a410efbd13591db07496601ebc7a059dd55cfe9 三つのコミットオブジェクトは、それぞれ、あなたが作成した三つのスナップショット のツリーのひとつを指し⽰しています。⾯⽩いことに、あなたは本物のGitヒストリーを 持っており、git log コマンドによってログをみることができます。もしも最後のコミッ トの SHA-1ハッシュを指定して実⾏すると、 $ git log --stat 1a410e commit 1a410efbd13591db07496601ebc7a059dd55cfe9 Author: Scott Chacon <[email protected]> Date: Fri May 22 18:15:24 2009 -0700 third commit bak/test.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) commit cac0cab538b970a37ea1e769cbbde608743bc96d Author: Scott Chacon <[email protected]> Date: Fri May 22 18:14:29 2009 -0700 second commit new.txt | test.txt | 1 + 2 +- 2 files changed, 2 insertions(+), 1 deletions(-) commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d Author: Scott Chacon <[email protected]> Date: Fri May 22 18:09:34 2009 -0700 first commit test.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) 263 第9章 Gitの内側 Scott Chacon Pro Git 驚くべきことです。あなたは Git ヒストリーを形成するために、フロントエンドにある 何かを利⽤することせずに、ただ下位レベルのオペレーションを⾏っただけなのです。こ れは git add コマンドと git commit コマンドを実⾏するときに Git が⾏う本質的なことな のです。それは変更されたファイルに対応して、ブロブを格納し、インデックスを更新 し、ツリーを書き出します。そして、トップレベルのツリーとそれらの直前に来たコミッ トを参照するコミットオブジェクトを書きます。これらの三つの主要な Git オブジェクト - ブロブとツリーとコミットは、.git/object ディレクトリに分割されたファイルとして最 初に格納されます。こちらは、例のディレクトリに今あるすべてのオブジェクトであり、 それらが何を格納しているのかコメントされています。 $ find .git/objects -type f .git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2 .git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2 .git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1 .git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content' .git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1 .git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt .git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1 もしすべての内部のポインタを辿ってゆけば、図9-3のようなオブジェクトグラフを得 られます。 図 9.3: Gitレポジトリ内のすべてのオブジェクト 9.2.3 オブジェクトストレージ ヘッダはコンテンツと⼀緒に格納されることを、以前に述べました。少し時間を割い て、Git がどのようにしてオブジェクトを格納するのかを⾒ていきましょう。あなたはブ ロブオブジェクトがどのように格納されるのかを⾒ることになるでしょう。このケースで 264 Scott Chacon Pro Git 9.2節 Gitオブジェクト は 『what is up, doc?』 という⽂字列が Rubyスクリプト⾔語の中で対話的に格納されま す。irb コマンドを使って対話的な Rubyモードを開始します。 $ irb >> content = "what is up, doc?" => "what is up, doc?" Git はオブジェクトタイプで開始するヘッダを構成します。このケースではブロブのタ イプです。そして、コンテンツのサイズに従ってスペースを追加して、最後にヌルバイト を追加します。 >> header = "blob #{content.length}\0" => "blob 16\000" Git はヘッダとオリジナルのコンテンツとを結合して、その新しいコンテンツの SHA-1 チェックサムを計算します。Rubyスクリプト内に書かれた⽂字列のSHA-1のハッシュ値 は、require を使⽤して SHA1ダイジェストライブラリをインクルードし、⽂字列を引数 にして Digest::SHA1.hexdigest() 関数を呼ぶことで求めることができます。 >> store = header + content => "blob 16\000what is up, doc?" >> require 'digest/sha1' => true >> sha1 = Digest::SHA1.hexdigest(store) => "bd9dbf5aae1a3862dd1526723246b20206e5fc37" Gitは zlib を⽤いて新しいコンテンツを圧縮します。Rubyにある zlibライブラリをイン クルードして使⽤します。まず、require を使⽤して zlib ライブラリをインクルードし、 コンテンツに対して Zlib::Deflate.deflate() を実⾏します。 >> require 'zlib' => true >> zlib_content = Zlib::Deflate.deflate(store) => "x\234K\312\311OR04c(\317H,Q\310,V(-\320QH\311O\266\a\000_\034\a\235" 最後に、zlibで圧縮された(zlib-deflated)コンテンツをディスク上のオブジェクトに 書き込みます。オブジェクトの書き込み先のパスを決定します(SHA-1ハッシュ値の最 初の2⽂字はサブディレクトリの名前で、残りの38⽂字はそのディレクトリ内のファイル 名になります)。Rubyでは、FileUtils.mkdir_p() 関数を使⽤して(存在しない場合に)サ ブディレクトリを作成することができます。そして、File.open() によってファイルを開い 265 第9章 Gitの内側 Scott Chacon Pro Git て、前に zlib で圧縮された(zlib-compressed)コンテンツをファイルに書き出します。 ファイルへの書き出しは、開いたファイルのハンドルに対して write() を呼ぶことで⾏い ます。 >> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38] => ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37" >> require 'fileutils' => true >> FileUtils.mkdir_p(File.dirname(path)) => ".git/objects/bd" >> File.open(path, 'w') { |f| f.write zlib_content } => 32 これで終わりです。あなたは妥当な Git ブロブオブジェクトを作りました。ただタイプ が異なるだけで、Git オブジェクトはすべて同じ⽅法で格納されます。ブロブの⽂字列で はない場合には、ヘッダはコミットまたはツリーから始まります。また、ブロブのコンテ ンツはほぼ何にでもなれるのに対して、コミットとツリーのコンテンツはかなり特定的に 形式付けられています。 9.3 Gitの参照 すべての履歴をひと通り⾒るには git log 1a410e のように実⾏します。しかしそれでも 履歴を辿りながらそれらすべてのオブジェクトを⾒つけるためには、1a410e が最後のコ ミットであることを覚えていなければなりません。SHA-1ハッシュ値を格納できるファイ ルが必要です。ファイル名はシンプルなもので、未加⼯(raw)の SHA-1ハッシュ値では なくポインタを使⽤することができます。 Git では、これらは 『参照(references)』 ないしは 『refs』 と呼ばれます。SHA-1 のハッシュ値を含んでいるファイルは .git/refs ディレクトリ内に⾒つけることができま す。現在のプロジェクトでは、このディレクトリに何もファイルはありませんが、シンプ ルな構成を持っています。 $ find .git/refs .git/refs .git/refs/heads .git/refs/tags $ find .git/refs -type f $ 最後のコミットはどこにあるのかを覚えるのに役⽴つような参照を新しく作るには、こ れと同じぐらいシンプルなことを技術的にすることができます。 266 Scott Chacon Pro Git 9.3節 Gitの参照 $ echo "1a410efbd13591db07496601ebc7a059dd55cfe9" > .git/refs/heads/master これであなたは、Git コマンドにある SHA-1のハッシュ値ではなく、たった今作成した ヘッダの参照を使⽤することができます。 $ git log --pretty=oneline master 1a410efbd13591db07496601ebc7a059dd55cfe9 third commit cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit 参照ファイルに対して直接、変更を⾏うことは推奨されません。Git はそれを⾏うため のより安全なコマンドを提供しています。もし参照を更新したければ update-ref というコ マンドを呼びます。 $ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9 Git にとって基本的にブランチとは何なのかをこれは⽰しているのです。すなわちそ れはシンプルなポインタ、もしくは作業ライン(line of work)のヘッドへの参照なので す。⼆回⽬のコミット時にバックアップのブランチを作るには、次のようにします。 $ git update-ref refs/heads/test cac0ca これでブランチはそのコミットから下の作業のみを含むことになります。 $ git log --pretty=oneline test cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit いま、Git のデータベースは概念的には図9-4のように⾒えます。 git branch (ブランチ名) のようにコマンドを実⾏すると基本的に Git は update-ref コマン ドを実⾏します。そして、あなたが作りたいと思っている新しい参照は何であれ、いま⾃ 分が作業しているブランチ上のブランチの最後のコミットの SHA-1ハッシュを追加しま す。 9.3.1 HEADブランチ では、git branch (ブランチ名) を実⾏したときに、どこから Git は最後のコミットの SHA1ハッシュを知ることができるでしょうか? 答えは、HEADファイルです。HEADファイ 267 第9章 Gitの内側 Scott Chacon Pro Git 図 9.4: ブランチのヘッドへの参照を含むGitディレクトリオブジェクト ルは、あなたが現在作業中のブランチに対するシンボリック参照(symbolic reference) です。通常の参照と区別する意図でシンボリック参照と呼びますが、それは、⼀般的に SHA-1ハッシュ値を持たずに他の参照へのポインタを持ちます。通常は以下のファイルが ⾒えるでしょう。 $ cat .git/HEAD ref: refs/heads/master git checkout test を実⾏すると、Git はこのようにファイルを更新します。 $ cat .git/HEAD ref: refs/heads/test git commit を実⾏すると、コミットオブジェクトが作られます。HEADにある参照先の SHA-1ハッシュ値が何であれ、そのコミットオブジェクトの親が参照先に指定されます。 このファイルを直に編集することもできますが、symbolic-ref と呼ばれる、それを安全 に⾏うためのコマンドが存在します。このコマンドを使ってHEADの値を読み取ることが できます。 $ git symbolic-ref HEAD refs/heads/master HEADの値を設定することもできます。 $ git symbolic-ref HEAD refs/heads/test $ cat .git/HEAD ref: refs/heads/test refs の形式以外では、シンボリック参照を設定することはできません。 268 Scott Chacon Pro Git 9.3節 Gitの参照 $ git symbolic-ref HEAD test fatal: Refusing to point HEAD outside of refs/ 9.3.2 タグ これまで Git の主要な三つのオブジェクトを⾒てきましたが、タグという四つ⽬のオブ ジェクトがあります。タグオブジェクトはコミットオブジェクトにとても似ています。そ れには、タガー(tagger)、⽇付、メッセージ、そしてポインタが含まれます。主な違い は、タグオブジェクトはツリーではなくコミットを指し⽰すことです。タグオブジェクト はブランチの参照に似ていますが、決して変動しません。そのため常に同じコミットを⽰ しますが、より親しみのある名前が与えられます。 2章で述べましたが、タグには⼆つのタイプがあります。軽量 (lightweight) 版と注釈 付き (annotated) 版です。あなたは、次のように実⾏して軽量 (lightweight) 版のタグを 作ることができます。 $ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d これが軽量版のタグのすべてです。つまり決して変動しないブランチなのです。⼀⽅、 注釈付き版のタグはもっと複雑です。注釈付き版のタグを作ろうとすると、Git はタグオ ブジェクトを作り、そして、コミットに対する直接的な参照ではなく、そのタグをポイン トする参照を書き込みます。注釈付き版のタグを作ることで、これを⾒ることができま す。(注釈付き版のタグを作るには -a オプションを指定して実⾏します) $ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag' これで、作られたオブジェクトの SHA-1ハッシュ値を⾒ることができます。 $ cat .git/refs/tags/v1.1 9585191f37f7b0fb9444f35a9bf50de191beadc2 ここで、そのSHA-1ハッシュ値に対して cat-file コマンドを実⾏します。 $ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2 object 1a410efbd13591db07496601ebc7a059dd55cfe9 type commit tag v1.1 tagger Scott Chacon <[email protected]> Sat May 23 16:48:58 2009 -0700 269 第9章 Gitの内側 Scott Chacon Pro Git test tag オブジェクトエントリはあなたがタグ付けしたコミットの SHA-1 ハッシュ値をポイン トすることに注意してください。またそれがコミットをポイントする必要がないことに注 意してください。あらゆる Git オブジェクトに対してタグ付けをすることができます。 例えば、Git のソースコードの保守では GPG 公開鍵をブロブオブジェクトとして追加し て、それからタグ付けをします。Git ソースコードレポジトリで、以下のように実⾏する ことで公開鍵を閲覧することができます。 $ git cat-file blob junio-gpg-pub Linuxカーネルのリポジトリは、さらに、⾮コミットポインティング(non-commitpointing)タグオブジェクトを持っています。このタグオブジェクトは、最初のタグが作 られるとソースコードのインポートの最初のツリーをポイントします。 9.3.3 リモート これから⾒ていく三つ⽬の参照のタイプはリモート参照です。リモートを追加してそれ にプッシュを実⾏すると、Git は追加したリモートにあなたが最後にプッシュした値をを 格納します。そのリモートは refs/remotes ディレクトリにある各ブランチを参照します。 例えば、origin と呼ばれるリモートを追加して、それを master ブランチにプッシュするこ とができます。 $ git remote add origin [email protected]:schacon/simplegit-progit.git $ git push origin master Counting objects: 11, done. Compressing objects: 100% (5/5), done. Writing objects: 100% (7/7), 716 bytes, done. Total 7 (delta 2), reused 4 (delta 1) To [email protected]:schacon/simplegit-progit.git a11bef0..ca82a6d master -> master そして、origin リモートに対してどの master ブランチが最後にサーバと通信したのか を、refs/remotes/origin/master ファイルをチェックすることで知ることができます。 $ cat .git/refs/remotes/origin/master ca82a6dff817ec66f44342007202690a93763949 リモート参照は主にそれらがチェックアウトされ得ないという点において、ブランチ (refs/heads への参照)とは異なります。Git はそれらをブックマークとして、それらの 270 Scott Chacon Pro Git 9.4節 パックファイル ブランチがかつてサーバー上に存在していた場所の最後に知られている状態に移し変えま す。 9.4 パックファイル Git レポジトリ test のオブジェクトデータベースに戻りましょう。この時点で、あなた は11個のオブジェクトを持っています。4つのブロブ、3つのツリー、3つのコミット、そ して1つのタグです。 $ find .git/objects -type f .git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2 .git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2 .git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1 .git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag .git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content' .git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1 .git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt .git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1 Git は zlib を使⽤してこれらのファイルのコンテンツを圧縮するため、多くを格納して いません。これらすべてのファイルを集めても 925バイトにしかならないのです。Git の 興味深い機能を実際に⾒るために、幾つか⼤きなコンテンツをレポジトリに追加してみま しょう。前に作業したGritライブラリから repo.rb ファイルを追加します。これは約 12K バイトのソースコードファイルです。 $ curl https://raw.github.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb $ git add repo.rb $ git commit -m 'added repo.rb' [master 484a592] added repo.rb 3 files changed, 459 insertions(+), 2 deletions(-) delete mode 100644 bak/test.txt create mode 100644 repo.rb rewrite test.txt (100%) 結果のツリーを⾒ると、ブロブオブジェクトから取得した repo.rb ファイルの SHA-1 ハッシュ値を⾒ることができます。 271 第9章 Gitの内側 Scott Chacon Pro Git $ git cat-file -p master^{tree} 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e repo.rb 100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt それから、そのオブジェクトのディスク上のサイズがどのくらいか調べることもできま す。 $ du -b .git/objects/9b/c1dc421dcd51b4ac296e3e5b6e2a99cf44391e 4102 .git/objects/9b/c1dc421dcd51b4ac296e3e5b6e2a99cf44391e ここで、ファイルに少し変更を加えたらどうなるのか⾒てみましょう。 $ echo '# testing' >> repo.rb $ git commit -am 'modified repo a bit' [master ab1afef] modified repo a bit 1 files changed, 1 insertions(+), 0 deletions(-) このコミットによって作られたツリーをチェックすると、興味深いことがわかります。 $ git cat-file -p master^{tree} 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 05408d195263d853f09dca71d55116663690c27c repo.rb 100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt そのブロブは今では当初とは異なるブロブです。つまり、400⾏あるファイルの最後に 1⾏だけ追加しただけなのに、Git はその新しいコンテンツを完全に新しいオブジェクト として格納するのです。 $ du -b .git/objects/05/408d195263d853f09dca71d55116663690c27c 4109 .git/objects/05/408d195263d853f09dca71d55116663690c27c これだとディスク上にほとんど同⼀の 4Kバイトのオブジェクトを⼆つ持つことになり ます。もし Git がそれらのひとつは完全に格納するが⼆つ⽬のオブジェクトはもうひとつ との差分(delta)のみを格納するのだとしたら、どんなに素晴らしいことかと思いませ んか? それが可能になったのです。Git がディスク上にオブジェクトを格納する初期のフォー マットは、緩いオブジェクトフォーマット(loose object format)と呼ばれます。しかし 272 Scott Chacon Pro Git 9.4節 パックファイル Git はこれらのオブジェクトの中の幾つかをひとつのバイナリファイルに詰め込む(pack up)ことがあります。そのバイナリファイルは、空きスペースを保存してより効率的に するための、パックファイル(packfile)と呼ばれます。あまりにたくさんの緩いオブ ジェクトがそこら中にあるときや、git gc コマンドを⼿動で実⾏したとき、または、リ モートサーバにプッシュしたときに、Git はこれを実⾏します。何が起こるのかを知るに は、git gc コマンドを呼ぶことで、Git にオブジェクトを詰め込むように⼿動で問い合わ せることができます。 $ git gc Counting objects: 17, done. Delta compression using 2 threads. Compressing objects: 100% (13/13), done. Writing objects: 100% (17/17), done. Total 17 (delta 1), reused 10 (delta 0) オブジェクトディレクトリの中を⾒ると、⼤半のオブジェクトは消えて、新しいファイ ルのペアが現れていることがわかります。 $ find .git/objects -type f .git/objects/71/08f7ecb345ee9d0084193f147cdad4d2998293 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 .git/objects/info/packs .git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.idx .git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.pack 残りのオブジェクトは、どのコミットにもポイントされていないブロブです。このケー スでは、以前に作成した 『what is up, doc?』 の例と 『test content』 のブロブの例が それにあたります。それらに対していかなるコミットも加えられてないので、それらは遊 離(dangling)しているとみなされ新しいパックファイルに詰め込まれないのです。 他のファイルは新しいパックファイルとインデックスです。パックファイルは、ファイ ルシステムから取り除かれたすべてのオブジェクトのコンテンツを含んでいる単⼀のファ イルです。インデックスは、特定のオブジェクトを速く探し出せるようにパックファイル の中にあるオフセットを含むファイルです。素晴らしいことに、gc を実⾏する前のディ スク上のオブジェクトを集めると約 8Kバイトのサイズであったのに対して、新しいパッ クファイルは 4Kバイトになっています。オブジェクトをパックすることで、ディスクの 使⽤量が半分になったのです。 Git はどうやってこれを⾏うのでしょうか? Git はオブジェクトをパックするとき、似 たような名前とサイズのファイルを探し出し、ファイルのあるバージョンから次のバー ジョンまでの増分のみを格納します。パックファイルの中を⾒ることで、スペースを確保 するために Git が何を⾏ったのかを知ることができます。git verify-pack という配管コマ ンドを使⽤して、何が詰め込まれたのかを知ることができます。 273 第9章 Gitの内側 Scott Chacon Pro Git $ git verify-pack -v \ .git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.idx 0155eb4229851634a0f03eb265b69f5a2d56f341 tree 71 76 5400 05408d195263d853f09dca71d55116663690c27c blob 12908 3478 874 09f01cea547666f58d6a8d809583841a7c6f0130 tree 106 107 5086 1a410efbd13591db07496601ebc7a059dd55cfe9 commit 225 151 322 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob 10 19 5381 3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree 101 105 5211 484a59275031909e19aadb7c92262719cfcdf19a commit 226 153 169 83baae61804e65cc73a7201a7252750c76066a30 blob 10 19 5362 9585191f37f7b0fb9444f35a9bf50de191beadc2 tag 136 127 5476 9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e blob 7 18 5193 1 \ 05408d195263d853f09dca71d55116663690c27c ab1afef80fac8e34258ff41fc1b867c702daa24b commit 232 157 12 cac0cab538b970a37ea1e769cbbde608743bc96d commit 226 154 473 d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree 36 46 5316 e3f094f522629ae358806b17daf78246c27c007b blob 1486 734 4352 f8f51d7d8a1760462eca26eebafde32087499533 tree 106 107 749 fa49b077972391ad58037050f2a75f74e3671e92 blob 9 18 856 fdf4fc3344e67ab068f836878b6c4951e3b15f3d commit 177 122 627 chain length = 1: 1 object pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.pack: ok ここで、9bc1d というブロブを覚えてますでしょうか、これは repo.rb ファイルの最初の バージョンですが、このブロブは⼆つ⽬のバージョンである 05408 というブロブを参照し ています。出⼒にある三つ⽬のカラムはオブジェクトの実体のサイズを⽰しており、05408 の実体は 12Kバイトを要しているが、9bc1d の実体はたったの 7バイトしか要していない ことがわかります。さらに興味深いのは、最初のバージョンは増分として格納されている のに対して、⼆つ⽬のバージョンのファイルは完全な状態で格納されているということで す。これは直近のバージョンのファイルにより速くアクセスする必要があるであろうこと に因ります。 これに関する本当に素晴らしいことは、いつでも再パックが可能なことです。Git は時 折データベースを⾃動的に再パックして、常により多くのスペースを確保しようと努めま す。また、あなたはいつでも git gc を実⾏することによって⼿動で再パックをすることが できるのです。 9.5 参照仕様(Refspec) 本書の全体に渡って、リモートブランチからローカルの参照へのシンプルなマッピング を使⽤してきました。しかし、それらはもっと複雑なものです。以下のようにリモートを 追加したとしましょう。 274 Scott Chacon Pro Git 9.5節 参照仕様(Refspec) $ git remote add origin [email protected]:schacon/simplegit-progit.git .git/config ファイルにセクションを追加して、リモート(origin)の名前、リモートレ ポジトリのURL、そしてフェッチするための参照仕様(refspec)を指定します。 [remote "origin"] url = [email protected]:schacon/simplegit-progit.git fetch = +refs/heads/*:refs/remotes/origin/* 参照仕様はコロン(:)で分割した <src>:<dst> の形式で、オプションとして先頭に + を 付けます。<src> はリモート側への参照に対するパターンで、<dst> はそれらの参照がロー カル上で書かれる場所を⽰します。+ の記号は Git にそれが早送り(fast-forward)でな い場合でも参照を更新することを伝えます。 デフォルトのケースでは git remote add コマンドを実⾏することで⾃動的に書かれま す。このコマンドを実⾏すると、Git はサーバ上の refs/heads/ 以下にあるすべての参照を フェッチして、ローカル上の refs/remotes/origin/ にそれらを書きます。そのため、もしも サーバ上に master ブランチがあると、ローカルからそのブランチのログにアクセスするこ とができます。 $ git log origin/master $ git log remotes/origin/master $ git log refs/remotes/origin/master これらはすべて同じ意味を持ちます。なぜなら、Git はそれら各々を refs/remotes/ origin/master に拡張するからです。 その代わりに、Git に毎回 master ブランチのみを引き出して、リモートサーバ上のそれ 以外のすべてのブランチは引き出さないようにしたい場合は、フェッチラインを以下のよ うに変更します。 fetch = +refs/heads/master:refs/remotes/origin/master これはまさにリモートへの git fetch に対する参照仕様のデフォルトの振る舞いです。 もし何かを⼀度実⾏したければ、コマンドライン上の参照仕様を指定することもできま す。 リモート上の master ブランチをプルして、ローカル上の origin/mymaster に落とすに は、以下のように実⾏します。 $ git fetch origin master:refs/remotes/origin/mymaster 275 第9章 Gitの内側 Scott Chacon Pro Git 複数の参照仕様を指定することも可能です。コマンドライン上で、幾つかのブランチを このように引き落とす(pull down)ことができます。 $ git fetch origin master:refs/remotes/origin/mymaster \ topic:refs/remotes/origin/topic From [email protected]:schacon/simplegit ! [rejected] master -> origin/mymaster * [new branch] topic -> origin/topic (non fast forward) このケースでは、master ブランチのプルは早送りの参照ではなかったため拒否されま した。+ の記号を参照仕様の先頭に指定することで、それを上書きすることができます。 さらに設定ファイルの中のフェッチ設定に複数の参照仕様を指定することができます。 もし master と実験⽤のブランチを常にフェッチしたいならば、⼆⾏を追加します。 [remote "origin"] url = [email protected]:schacon/simplegit-progit.git fetch = +refs/heads/master:refs/remotes/origin/master fetch = +refs/heads/experiment:refs/remotes/origin/experiment ブロブの⼀部をパターンに使⽤することはできません。これは無効となります。 fetch = +refs/heads/qa*:refs/remotes/origin/qa* しかし、似たようなことを達成するのに名前空間を使⽤することができます。もし⼀連 のブランチをプッシュしてくれる QAチームがいて、master ブランチと QAチームのブラ ンチのみを取得したいならば、設定ファイルのセクションを以下のように使⽤することが できます。 [remote "origin"] url = [email protected]:schacon/simplegit-progit.git fetch = +refs/heads/master:refs/remotes/origin/master fetch = +refs/heads/qa/*:refs/remotes/origin/qa/* QAチームと開発チームがローカルのブランチにプッシュして、結合チームがリモート のブランチ上でプッシュして、共同で開発するような、複雑なワークフローのプロセスで あるならば、このように、名前空間によってそれらを簡単に分類することができます。 9.5.1 参照仕様へのプッシュ その⽅法で名前空間で分類された参照をフェッチできることは素晴らしいことです。し かし、そもそもどうやって QAチームは、彼らのブランチを qa/ という名前空間の中で取 276 Scott Chacon Pro Git 9.6節 トランスファープロトコル 得できるのでしょうか? 参照仕様にプッシュすることによってそれが可能です。 QAチームが彼らの master ブランチをリモートサーバ上の qa/master にプッシュしたい場 合、以下のように実⾏します。 $ git push origin master:refs/heads/qa/master もし彼らが git push origin を実⾏する都度、Git に⾃動的にそれを⾏なってほしいなら ば、設定ファイルに push の値を追加することで⽬的が達成されます。 [remote "origin"] url = [email protected]:schacon/simplegit-progit.git fetch = +refs/heads/*:refs/remotes/origin/* push = refs/heads/master:refs/heads/qa/master 再度、これは git push origin の実⾏をローカルの master ブランチに、リモートの qa/ master ブランチに、デフォルトで引き起こします。 9.5.2 参照の削除 また、リモートサーバから以下のように実⾏することによって、参照仕様を参照を削除 する⽬的で使⽤することもできます。 $ git push origin :topic 参照仕様は <src>:<dst> という形式であり、<src> の部分を取り除くことは、要するに何 もないブランチをリモート上に作ることであり、それを削除することになるのです。 9.6 トランスファープロトコル Git は2つのレポジトリ間を⼆つの主要な⽅法によってデータを移⾏することができま す。ひとつは HTTPによって、もうひとつは、file:// や ssh://、また、git:// によるトラ ンスポートに使⽤される、いわゆるスマートプロトコルによって。このセクションでは、 これらの主要なプロトコルがどのように機能するのかを駆け⾜で⾒ていきます。 9.6.1 無口なプロトコル Git の over HTTPによる移⾏は、しばしば無⼝なプロトコル(dumb protocol)と⾔ われます。なぜなら、トランスポートプロセスの最中に、サーバ側に関する Git 固有の コードは何も必要としないからです。フェッチプロセスは、⼀連の GET リクエストであ り、クライアントはサーバ上の Gitレポジトリのレイアウトを推測することができます。 simplegit ライブラリに対する http-fetch のプロセスを追ってみましょう。 277 第9章 Gitの内側 Scott Chacon Pro Git $ git clone http://github.com/schacon/simplegit-progit.git 最初にこのコマンドが⾏うことは info/refs ファイルを引き出す(pull down)ことで す。このファイルは update-server-info コマンドによって書き込まれます。そのために、 HTTPトランスポートが適切に動作するための post-receive フックとして、そのコマンド を有効にする必要があります。 => GET info/refs ca82a6dff817ec66f44342007202690a93763949 refs/heads/master いまあなたはリモート参照と SHAのハッシュのリストを持っています。 次に、終了時 に何をチェックアウトするのかを知るために、HEAD参照が何かを探します。 => GET HEAD ref: refs/heads/master プロセスの完了時に、master ブランチをチェックアウトする必要があります。この時点 で、あなたは参照を辿るプロセス(the walking process)を開始する準備ができていま す。開始時点はあなたが info/refs ファイルの中に⾒た ca82a6 のコミットオブジェクトな ので、それをフェッチすることによって開始します。 => GET objects/ca/82a6dff817ec66f44342007202690a93763949 (179 bytes of binary data) オブジェクトバック(object back)を取得します。それは、サーバ上の緩い形式の オブジェクトで、静的な HTTP GETリクエストを超えてそれをフェッチします。zlibuncompress を使ってそれを解凍することができます。ヘッダを剥ぎ取り(strip off)そ れからコミットコンテンツを⾒てみます。 $ git cat-file -p ca82a6dff817ec66f44342007202690a93763949 tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 author Scott Chacon <[email protected]> 1205815931 -0700 committer Scott Chacon <[email protected]> 1240030591 -0700 changed the version number 278 Scott Chacon Pro Git 9.6節 トランスファープロトコル 次に、取り戻すためのオブジェクトがもう⼆つあります。それは、たった今取り戻した コミットがポイントするコンテンツのツリーである cfda3b と、親のコミットである 085bb3 です。 => GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 (179 bytes of data) それは次のコミットオブジェクトを与えます。ツリーオブジェクトをつかみます。 => GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf (404 - Not Found) おっと、どうやらそのツリーオブジェクトはサーバ上の緩い形式には存在しないようで す。そのため404のレスポンスを受け取っています。これには⼆つの理由があります。ひ とつは、オブジェクトは代替のレポジトリ内に存在し得るため、もうひとつは、このレポ ジトリ内のパックファイルの中に存在し得るため。Git はまずリストにあるあらゆる代替 の URLをチェックします。 => GET objects/info/http-alternates (empty file) 代替の URLのリストと⼀緒にこれが戻ってくるなら、Git はそこにある緩いファイル とパックファイルをチェックします。これは、ディスク上のオブジェクトを共有するため に互いにフォークし合っているプロジェクトにとって素晴らしい機構(mechanism)で す。しかし、このケースではリスト化された代替は存在しないため、オブジェクトはパッ クファイルの中にあるに違いありません。サーバー上の何のパックファイルが利⽤可能か を知るには、objects/info/packs のファイルを取得することが必要です。そのファイルには (さらに update-server-info によって⽣成された)それらの⼀覧が含まれています。 => GET objects/info/packs P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack サーバー上にはパックファイルがひとつしかないので、あなたのオブジェクトは明らか にそこにあります。しかし念の為にインデックスファイルをチェックしてみましょう。こ れが便利でもあるのは、もしサーバー上にパックファイルを複数持つ場合に、どのパック ファイルにあなたが必要とするオブジェクトが含まれているのかを知ることができるから です。 279 第9章 Gitの内側 Scott Chacon Pro Git => GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx (4k of binary data) パックファイルのインデックスを持っているので、あなたのオブジェクトがその中にあ るのかどうかを知ることができます。なぜならインデックスにはパックファイルの中にあ るオブジェクトの SHAハッシュとそれらのオブジェクトに対するオフセットがリストさ れているからです。あなたのオブジェクトはそこにあります。さあ、すべてのパックファ イルを取得してみましょう。 => GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack (13k of binary data) あなたはツリーオブジェクトを持っているのでコミットを辿ってみましょう。それらす べてはまた、あなたが丁度ダウンロードしたパックファイルの中にあります。そのため、 もはやサーバーに対していかなるリクエストも不要です。Git は master ブランチの作業⽤ コピーをチェックアウトします。そのブランチは最初にダウンロードした HEAD への参 照によってポイントされています。 このプロセスのすべての出⼒はこのように⾒えます。 $ git clone http://github.com/schacon/simplegit-progit.git Initialized empty Git repository in /private/tmp/simplegit-progit/.git/ got ca82a6dff817ec66f44342007202690a93763949 walk ca82a6dff817ec66f44342007202690a93763949 got 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 Getting alternates list for http://github.com/schacon/simplegit-progit.git Getting pack list for http://github.com/schacon/simplegit-progit.git Getting index for pack 816a9b2334da9953e530f27bcac22082a9f5b835 Getting pack 816a9b2334da9953e530f27bcac22082a9f5b835 which contains cfda3bf379e4f8dba8717dee55aab78aef7f4daf walk 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 walk a11bef06a3f659402fe7563abf99ad00de2209e6 9.6.2 スマートプロトコル HTTPメソッドはシンプルですが少し⾮効率です。スマートプロトコルを使⽤すること はデータ移⾏のより⼀般的な⼿段です。これらのプロトコルは Git をよく知っているリ モートエンド上にプロセスを持っています。そのリモートエンドは、ローカルのデータを 読んで、クライアントが何を持っているか、または、必要としているか、そして、それに 対するカスタムデータを⽣成するのか知ることができます。データを転送するためのプロ セスが2セットあります。データをアップロードするペア、それと、ダウンロードするペ アです。 280 Scott Chacon Pro Git 9.6節 トランスファープロトコル データのアップロード リモートプロセスにデータをアップロードするため、Git は send-pack と receive-pack の プロセスを使⽤します。send-pack プロセスはクライアント上で実⾏されリモートサイド上 の receive-pack プロセスに接続します。 例えば、あなたのプロジェクトで git push origin master を実⾏したとしましょう。そし て origin は SSHプロトコルを使⽤する URLとして定義されているとします。Git はあな たのサーバーへの SSHによる接続を開始する send-pack プロセスを実⾏します。リモート サーバ上で以下のようなSSHの呼び出しを介してコマンドを実⾏しようとします。 $ ssh -x [email protected] "git-receive-pack 'schacon/simplegit-progit.git'" 005bca82a6dff817ec66f4437202690a93763949 refs/heads/master report-status delete-refs 003e085bb3bcb608e1e84b2432f8ecbe6306e7e7 refs/heads/topic 0000 git-receive-pack コマンドは現在持っている各々の参照に対してひとつの⾏をすぐに返し ます。このケースでは、master ブランチとその SHAハッシュのみです。最初の⾏はサー バーの可能性(ここでは、report-status と delete-refs)のリストも持っています。 各⾏は 4バイトの 16進数で始まっており、その残りの⾏がどれくらいの⻑さなのかを ⽰しています。最初の⾏は 005b で始まっていますが、これは16進数では 91 であり、そ の⾏には 91バイトが残っていることを意味します。次の⾏は 003e で始まっていて、こ れは 62 です。そのため残りの 62バイトを読みます。次の⾏は 0000 であり、サーバー はその参照のリスト表⽰を終えたことを意味します。 サーバーの状態がわかったので、あなたの send-pack プロセスはサーバーが持ってい ないのは何のコミットかを決定します。このプッシュが更新する予定の各参照に対し て、send-pack プロセスは receive-pack プロセスにその情報を伝えます。例えば、もしも あなたが master ブランチを更新していて、さらに、experiment ブランチを追加していると き、send-pack のレスポンスは次のように⾒えるかもしれません。 0085ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 refs/ heads/master report-status 00670000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d refs/ heads/experiment 0000 すべてが '0' の SHA-1ハッシュ値は以前そこには何もなかったことを意味します。そ れはあなたが experiment の参照を追加しているためです。もしもあなたが参照を削除し ていたとすると、あなたは逆にすべての '0' が右側にあるのを⾒るでしょう。 Git はあなたが古い SHA1ハッシュで更新している各々の古い参照、新しい参照、そ して更新されている参照に対して⾏を送信します。最初の⾏はまたクライアントの性能 (capabilities)を持っています。次に、クライアントはサーバーが未だ持ったことのな いすべてのオブジェクトのパックファイルをアップロードします。最後に、サーバーは成 功(あるいは失敗)の表⽰を返します。 281 第9章 Gitの内側 Scott Chacon Pro Git 000Aunpack ok データのダウンロード データをダウンロードするときには、fetch-pack と upload-pack プロセスが伴います。ク ライアントは fetch-pack プロセスを開始します。何のデータが移送されてくるのかを取 り決める(negotiate)ため、それはリモートサイド上の upload-pack プロセスに接続しま す。 リモートリポジトリ上の upload-pack プロセスを開始する異なった⽅法があります。あ なたは receive-pack プロセスと同様に SSH経由で実⾏することができます。さらに、Git デーモンを介してプロセスを開始することもできます。そのデーモンは、デフォルトでは サーバ上の 9418ポートを使⽤します。fetch-pack プロセスはデータを送信します。その データは接続後のデーモンに対して、以下のように⾒えます。 003fgit-upload-pack schacon/simplegit-progit.git\0host=myserver.com\0 どれくらい多くのデータが続いているのかを⽰す 4バイトから始まります。それから、 ヌルバイトに続いて実⾏コマンド、そして最後のヌルバイトに続いてサーバーのホスト 名が来ます。Git デーモンはコマンドが実⾏でき、レポジトリが存在して、それがパブ リックのパーミッションを持っていることをチェックします。もしすべてが素晴らしいな ら、upload-pack プロセスを発⾏して、それに対するリクエストを渡します。 もし SSHを介してフェッチを⾏っているとき、fetch-pack は代わりにこのように実⾏し ます。 $ ssh -x [email protected] "git-upload-pack 'schacon/simplegit-progit.git'" いずれケースでも、fetch-pack の接続のあと、upload-pack はこのように送り返します。 0088ca82a6dff817ec66f44342007202690a93763949 HEAD\0multi_ack thin-pack \ side-band side-band-64k ofs-delta shallow no-progress include-tag 003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master 003e085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 refs/heads/topic 0000 これは receive-pack が返答する内容にとても似ていますが、性能は異なります。加え て、これがクローンの場合はクライアントが何をチェックアウトするのかを知るために HEAD への参照を送り返します。 この時点で、fetch-pack プロセスは何のオブジェクトがそれを持っているかを⾒ます。 そして 『want』 とそれが求める SHA1ハッシュを送ることによって、それが必要なオブ 282 Scott Chacon Pro Git 9.7節 メインテナンスとデータリカバリ ジェクトを返答します。『have』 とその SHA1ハッシュで既に持っているオブジェクト すべてを送ります。このリストの最後で、それが必要とするデータのパックファイルを送 信する upload-pack プロセスを開始するために 『done』 を書き込みます。 0054want ca82a6dff817ec66f44342007202690a93763949 ofs-delta 0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 0000 0009done これはトランスファープロトコルのとても基本的なケースです。より複雑なケースで は、クライアントは multi_ack または side-band の性能をサポートします。しかしこの例で はスマートプロトコルのプロセスによって使⽤される基本の部分を⽰します。 9.7 メインテナンスとデータリカバリ 時々、幾らかのお掃除をする必要があるかもしれません。つまり、レポジトリをよりコ ンパクトにすること、インポートしたリポジトリをクリーンアップすること、あるいは 失った作業をもとに戻すことです。このセクションではこれらのシナリオの幾つかをカ バーします。 9.7.1 メインテナンス Git は時々 『auto gc』 と呼ばれるコマンドを⾃動的に実⾏します。⼤抵の場合、この コマンドは何もしません。もし沢⼭の緩いオブジェクト(パックファイルの中にないオ ブジェクト)があったり、あまりに多くのパックファイルがあると、Git は完全な(fullfledged)git gc コマンドを開始します。gc はガベージコレクト(garbage collect)を意 味します。このコマンドは幾つものことを⾏います。まず、すべての緩いオブジェクトを 集めてそれらをパックファイルの中に⼊れます。複数のパックファイルをひとつの⼤きな パックファイルに統合します。どのコミットからも到達が不可能なオブジェクトや数ヶ⽉ の間何も更新がないオブジェクトを削除します。 次のように⼿動で auto gc を実⾏することができます。 $ git gc --auto 繰り返しますが、これは通常は何も⾏いません。約 7,000個もの緩いオブジェクトがあ るか、または50以上のパックファイルがないと、Gitは実際に gc コマンドを開始しませ ん。これらのリミットは設定ファイルの gc.auto と gc.autopacklimit によってそれぞれ変更 することができます。 他にも gc が⾏うこととしては、あなたが持つ参照を1つのファイルにまとめて⼊れるこ とが挙げられます。あなたのレポジトリには、次のようなブランチとタグが含まれている としましょう。 283 第9章 Gitの内側 Scott Chacon Pro Git $ find .git/refs -type f .git/refs/heads/experiment .git/refs/heads/master .git/refs/tags/v1.0 .git/refs/tags/v1.1 git gc を実⾏すると、refs ディレクトリにはこれらのファイルはもはや存在しなくなり ます。効率性のために Git はそれらを、以下のような .git/packed-refs という名前のファ イルに移します。 $ cat .git/packed-refs # pack-refs with: peeled cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0 9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1 ^1a410efbd13591db07496601ebc7a059dd55cfe9 もし参照を更新すると、Git はこのファイルを編集せず、その代わりに refs/heads に新 しいファイルを書き込みます。与えられた参照に対する適切な SHA1ハッシュを得るため に、Git は refs ディレクトリ内でその参照をチェックし、それから予備(fallback)とし て packed-refs ファイルをチェックします。ところがもし refs ディレクトリ内で参照が⾒ つけられない場合は、それはおそらく packed-refs ファイル内にあります。 ファイルの最後の⾏に注意してください。それは ˆ という⽂字で始まっています。これ はタグを意味し、そのすぐ上にあるのはアノテートタグ(annotated tag)であり、その ⾏はアノテートタグがポイントするコミットです。 9.7.2 データリカバリ Git を使っていく過程のある時点で、誤ってコミットを失ってしまうことがあるかもし れません。これが起こるのは⼀般的には、作業後のブランチを force-delete して、その後 結局そのブランチが必要になったとき、あるいはブランチを hard-reset したために、そこ から何か必要とするコミットが破棄されるときです。これが起きたとしたら、あなたはど うやってコミットを元に戻しますか? こちらの例では、あなたの test リポジトリ内の master ブランチを古いコミットに hard-reset して、それから失ったコミットを復元します。まず、ここであなたのレポジト リがどこにあるのか調べてみましょう。 $ git log --pretty=oneline ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit 484a59275031909e19aadb7c92262719cfcdf19a added repo.rb 284 Scott Chacon Pro Git 9.7節 メインテナンスとデータリカバリ 1a410efbd13591db07496601ebc7a059dd55cfe9 third commit cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit ここで、master ブランチを移動させて、中間のコミットに戻します。 $ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9 HEAD is now at 1a410ef third commit $ git log --pretty=oneline 1a410efbd13591db07496601ebc7a059dd55cfe9 third commit cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit あなたはトップにある⼆つのコミットを⼿際よく失いました。それらのコミットからは どのブランチからも到達され得ません。最後のコミットの SHA1ハッシュを⾒つけて、そ れにポイントするブランチを追加する必要があります。その最後のコミットの SHA1ハッ シュを⾒つけるコツは、記憶しておくことではないですよね? ⼤抵の場合、最も⼿っ取り早いのは、git reflog と呼ばれるツールを使う⽅法です。あ なたが作業をしているとき、変更する度に Git は HEAD が何であるかを黙って記録し ます。ブランチをコミットまたは変更する度に reflog は更新されます。reflog はまた git update-ref コマンドによっても更新されます。このチャプターの前の 『Gitの参照』 のセ クションでカバーしましたが、これは、ref ファイルに SHA1ハッシュ値を直に書くので はなくコマンドを使⽤する別の理由です。git reflog を実⾏することで⾃分がどこにいた のかをいつでも知ることができます。 $ git reflog 1a410ef HEAD@{0}: 1a410efbd13591db07496601ebc7a059dd55cfe9: updating HEAD ab1afef HEAD@{1}: ab1afef80fac8e34258ff41fc1b867c702daa24b: updating HEAD ここでチェックアウトした2つのコミットを⾒つけることができますが、ここに多くの 情報はありません。もっと有効な⽅法で同じ情報を⾒るためには、git log -g を実⾏する ことができます。これは reflog に対する通常のログ出⼒を提供してくれます。 $ git log -g commit 1a410efbd13591db07496601ebc7a059dd55cfe9 Reflog: HEAD@{0} (Scott Chacon <[email protected]>) Reflog message: updating HEAD Author: Scott Chacon <[email protected]> Date: Fri May 22 18:22:37 2009 -0700 285 第9章 Gitの内側 Scott Chacon Pro Git third commit commit ab1afef80fac8e34258ff41fc1b867c702daa24b Reflog: HEAD@{1} (Scott Chacon <[email protected]>) Reflog message: updating HEAD Author: Scott Chacon <[email protected]> Date: Fri May 22 18:15:24 2009 -0700 modified repo a bit ⼀番下にあるコミットがあなたが失ったコミットのようです。そのコミットの新し いブランチを作成することでそれを復元することができます。例えば、そのコミット (ab1afef)から recover-branch という名前でブランチを開始することができます。 $ git branch recover-branch ab1afef $ git log --pretty=oneline recover-branch ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit 484a59275031909e19aadb7c92262719cfcdf19a added repo.rb 1a410efbd13591db07496601ebc7a059dd55cfe9 third commit cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit 素晴らしい。master ブランチがかつて存在した場所に、最初の⼆つのコミットを再び 到達可能にして、あなたはいま recover-branch という名前のブランチを持っています。 次に、損失の原因は reflog の中にはないある理由によるものだったと想定しましょ う。recover-branch を取り除いて reflog を削除することによって、それをシミュレートす ることができます。最初の⼆つのコミットは今いかなるものからも到達不能な状態です。 $ git branch -D recover-branch $ rm -Rf .git/logs/ なぜなら reflog データは .git/logs/ ディレクトリに残っているため、あなたは効率的に reflog を持たない状態です。この時点でそのコミットをどうやって復元できるのでしょう か? ひとつの⽅法は git fsck ユティリティーを使⽤することです。それはあなたのデータ ベースの完全性(integrity)をチェックします。もし --full オプションを付けて実⾏する と、別のオブジェクトによってポイントされていないすべてのオブジェクトを表⽰しま す。 $ git fsck --full dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4 286 Scott Chacon Pro Git 9.7節 メインテナンスとデータリカバリ dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9 dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293 このケースでは、あなたは浮遊コミットの後に⾒失ったコミットを⾒つけることができ ます。その SHA1ハッシュにポイントするブランチを加えることによって、同様にそれを 復元することができます。 9.7.3 オブジェクトの除去 Git には素晴らしいものたくさんあります。しかし問題が⽣じる可能性がある機能がひ とつあります。git clone がすべてのファイルのすべてのバージョンを含んだプロジェクト の履歴全体をダウンロードしてしまうということです。すべてがソースコードならこれは 申し分のないことです。なぜなら Git はそのデータを効率良く圧縮することに⾼度に最適 化されているからです。しかし、もし誰かがある時点であなたのプロジェクトの履歴に1 つ⾮常に⼤きなファイルを加えると、すべてのクローンは以後ずっと、その⼤きなファイ ルのダウンロードを強いられることになります。たとえ、まさに次のコミットでそれをプ ロジェクトから取り除かれたとしても。なぜなら常にそこに存在して、履歴から到達可能 だからです。 Subversion または Perforce のレポジトリを Git に変換するときに、これは⼤きな問 題になり得ます。なぜなら、それらのシステムではすべての履歴をダウンロードする必要 がないため、⾮常に⼤きなファイルを追加してもほとんど悪影響がないからです。もし別 のシステムからインポートを⾏った場合、あるいはあなたのレポジトリがあるべき状態よ りもずっと⼤きくなっている場合、⼤きなオブジェクトを⾒つけて取り除く⽅法がありま す。 注意: このテクニックはあなたのコミット履歴を壊すことになります。⼤きなファイル への参照を取り除くために修正が必要な⼀番前のツリーからすべての下流のコミットオブ ジェクトに再書き込みをします。もしインポートした後そのコミット上での作業を誰かが 開始する前にすぐにこれを⾏った場合は問題ないです。その他の場合は、あなたの新しい コミット上に作業をリベースしなければならないことをすべての関係者(contributors) に知らせる必要があります。 実演するために、あなたの test リポジトリに⼤きなファイルを追加して、次のコミッ トでそれを取り除き、それを⾒つけて、そしてレポジトリからそれを永久に取り除きま す。まず、あなたの履歴に⼤きなオブジェクトを追加します。 $ curl http://kernel.org/pub/software/scm/git/git-1.6.3.1.tar.bz2 > git.tbz2 $ git add git.tbz2 $ git commit -am 'added git tarball' [master 6df7640] added git tarball 1 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 git.tbz2 おっと、誤ってプロジェクトに⾮常に⼤きなターボールを追加してしまいました。取り 287 第9章 Gitの内側 Scott Chacon Pro Git 除いたほうがいいでしょう。 $ git rm git.tbz2 rm 'git.tbz2' $ git commit -m 'oops - removed large tarball' [master da3f30d] oops - removed large tarball 1 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 git.tbz2 ここで、データベースに対して gc を実⾏して、どれくらい多くのスペースを使⽤して いるのかを⾒てみます。 $ git gc Counting objects: 21, done. Delta compression using 2 threads. Compressing objects: 100% (16/16), done. Writing objects: 100% (21/21), done. Total 21 (delta 3), reused 15 (delta 1) count-objects コマンドを実⾏してどれくらい多くのスペースを使⽤しているのかをすぐ に⾒ることができます。 $ git count-objects -v count: 4 size: 16 in-pack: 21 packs: 1 size-pack: 2016 prune-packable: 0 garbage: 0 size-pack エントリにはパックファイルのサイズがキロバイトで記されていて、2MB使 ⽤していることがわかります。最後のコミットの前は、2KB近くを使⽤していました。明 らかに前のコミットからファイルが取り除かれましたが、そのファイルは履歴からは取り 除かれませんでした。このレポジトリを誰かがクローンする都度、彼らはこの⼩さなプロ ジェクトを取得するだけに 2MBすべてをクローンする必要があるでしょう。なぜならあ なたは誤って⼤きなファイルを追加してしまったからです。それを取り除きましょう。 最初にあなたはそれを⾒つけなければなりません。このケースでは、あなたはそれが何 のファイルかを既に知っています。しかし、もし知らなかったとします。その場合どう やってあなたは多くのスペースを占めているファイルを⾒分けるのでしょうか? もし git gc を実⾏したとき、すべてのプロジェクトはパックファイルのなかにあります。⼤きな 288 Scott Chacon Pro Git 9.7節 メインテナンスとデータリカバリ オブジェクトは別の配管コマンドを実⾏することで⾒分けることができます。それは git verify-pack と呼ばれ、ファイルサイズを意味する三つ⽬の出⼒フィールドに対して並び替 えを⾏います。それを tail コマンドと通してパイプすることもできます。なぜなら最後 の幾つかの⼤きなファイルのみが関⼼の対象となるからです。 $ git verify-pack -v .git/objects/pack/pack-3f8c0...bb.idx | sort -k 3 -n | tail -3 e3f094f522629ae358806b17daf78246c27c007b blob 1486 734 4667 05408d195263d853f09dca71d55116663690c27c blob 12908 3478 1189 7a9eb2fba2b1811321254ac360970fc169ba2330 blob 2056716 2056872 5401 ⼤きなオブジェクトは⼀番下の 2MBのものです。それが何のファイルなのかを知るに は7章で少し使⽤した rev-list コマンドを使⽤します。--objects を rev-list に渡すと、す べてのコミットの SHA1ハッシュとブロブの SHA1ハッシュをそれらに関連するファイル パスと⼀緒にリストします。ブロブの名前を⾒つけるためにこれを使うことができます。 $ git rev-list --objects --all | grep 7a9eb2fb 7a9eb2fba2b1811321254ac360970fc169ba2330 git.tbz2 ここで、あなたは過去のすべてのツリーからこのファイルを取り除く必要があります。 このファイルを変更したのは何のコミットなのか知ることは簡単です。 $ git log --pretty=oneline --branches -- git.tbz2 da3f30d019005479c99eb4c3406225613985a1db oops - removed large tarball 6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 added git tarball Git レポジトリから完全にこのファイルを取り除くためには、6df76 から下流のすべての コミットを書き直さなければなりません。そのためには、6章で使⽤した filter-branch を 使⽤します。 $ git filter-branch --index-filter \ 'git rm --cached --ignore-unmatch git.tbz2' -- 6df7640^.. Rewrite 6df764092f3e7c8f5f94cbe08ee5cf42e92a0289 (1/2)rm 'git.tbz2' Rewrite da3f30d019005479c99eb4c3406225613985a1db (2/2) Ref 'refs/heads/master' was rewritten --index-filter オプションは、ディスク上のチェックアウトされたファイルを変更する コマンドを渡すのではなく、ステージングエリアまたはインデックスを毎度変更すること を除けば、6章で使⽤した --tree-filter オプションに似ています。特定のファイルに対し て rm file を実⾏するように取り除くよりもむしろ、git rm --cached を実⾏して取り除か なければなりません。つまりディスクではなくインデックスからそれを取り除くのです。 289 第9章 Gitの内側 Scott Chacon Pro Git このようにする理由はスピードです。Git はあなたの除去作業の前にディスク上の各リビ ジョンをチェックアウトする必要がないので、プロセスをもっともっと速くすることが できます。同様のタスクを --tree-filter を使⽤することで達成することができます。git rm に渡す --ignore-unmatch オプションは取り除こうとするパターンがそこにない場合にエ ラーを出⼒しないようにします。最後に、filter-branch に 6df7640 のコミットから後の履 歴のみを再書き込みするように伝えます。なぜならこれが問題が⽣じた場所であることを あなたは知っているからです。さもなければ、最初から開始することになり不必要に⻑く かかるでしょう。 履歴にはもはやそのファイルへの参照が含まれなくなります。しかしあなたの reflog と .git/refs/original の下で filter-branch を⾏ったときに Git が追加した新しいセットの refs には、参照はまだ含まれているので、それらを取り除いてそしてデータベースを再 パックしなければなりません。再パックの前にそれら古いコミットへのポインタを持つい かなるものを取り除く必要があります。 $ rm -Rf .git/refs/original $ rm -Rf .git/logs/ $ git gc Counting objects: 19, done. Delta compression using 2 threads. Compressing objects: 100% (14/14), done. Writing objects: 100% (19/19), done. Total 19 (delta 3), reused 16 (delta 1) どれくらいのスペースが節約されたかを⾒てみましょう。 $ git count-objects -v count: 8 size: 2040 in-pack: 19 packs: 1 size-pack: 7 prune-packable: 0 garbage: 0 パックされたレポジトリのサイズは 7KBに下がりました。当初の 2MBよりもずっとよ くなりました。サイズの値から⼤きなオブジェクトが未だ緩いオブジェクトの中にあるこ とがわかります。そのため、それは無くなったわけではないのです。ですが、それはプッ シュや後続するクローンで移送されることは決してありません。これは重要なことです。 本当にそれを望んでいたのなら、git prune --expire を実⾏することでオブジェクトを完全 に取り除くことができました。 290 Scott Chacon Pro Git 9.8節 要約 9.8 要約 Git がバックグラウンドで何を⾏うのかについて、また、ある程度までの Git の実装の ⽅法について、かなり良い理解が得られたことでしょう。この章では幾つかの配管コマン ドを取り扱いました。このコマンドは、本書の残りで学んだ磁器コマンドよりもシンプル でもっと下位レベルのコマンドです。下位レベルで Git がどのように機能するのかを理解 することは、なぜ⾏うのか、何を⾏うのかを理解して、さらに、あなた⾃⾝でツールを書 いて、あなた固有のワークフローが機能するようにスクリプト利⽤することをより容易に します。 連想記憶ファイル・システムとしての Git は単なるバージョン管理システム(VCS)以 上のものとして簡単に使⽤できる、とても強⼒なツールです。望むらくは、あなたが Git の内側で⾒つけた新しい知識を使うことです。その知識は、このテクノロジーを利⽤する あなた⾃⾝の素晴らしいアプリケーションを実装するための知識、また、より進歩した⽅ 法で Git を使うことをより快適に感じるための知識です。 291