-
1. 使い始める
- 1.1 バージョン管理に関して
- 1.2 Git略史
- 1.3 Gitの基本
- 1.4 コマンドライン
- 1.5 Gitのインストール
- 1.6 最初のGitの構成
- 1.7 ヘルプを見る
- 1.8 まとめ
-
2. Git の基本
- 2.1 Git リポジトリの取得
- 2.2 変更内容のリポジトリへの記録
- 2.3 コミット履歴の閲覧
- 2.4 作業のやり直し
- 2.5 リモートでの作業
- 2.6 タグ
- 2.7 Git エイリアス
- 2.8 まとめ
-
3. Git のブランチ機能
- 3.1 ブランチとは
- 3.2 ブランチとマージの基本
- 3.3 ブランチの管理
- 3.4 ブランチでの作業の流れ
- 3.5 リモートブランチ
- 3.6 リベース
- 3.7 まとめ
-
4. Gitサーバー
- 4.1 プロトコル
- 4.2 サーバー用の Git の取得
- 4.3 SSH 公開鍵の作成
- 4.4 サーバーのセットアップ
- 4.5 Git デーモン
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 サードパーティによる Git ホスティング
- 4.10 まとめ
-
5. Git での分散作業
- 5.1 分散作業の流れ
- 5.2 プロジェクトへの貢献
- 5.3 プロジェクトの運営
- 5.4 まとめ
-
6. GitHub
- 6.1 アカウントの準備と設定
- 6.2 プロジェクトへの貢献
- 6.3 プロジェクトのメンテナンス
- 6.4 組織の管理
- 6.5 スクリプトによる GitHub の操作
- 6.6 まとめ
-
7. Git のさまざまなツール
- 7.1 リビジョンの選択
- 7.2 対話的なステージング
- 7.3 作業の隠しかたと消しかた
- 7.4 作業内容への署名
- 7.5 検索
- 7.6 歴史の書き換え
- 7.7 リセットコマンド詳説
- 7.8 高度なマージ手法
- 7.9 Rerere
- 7.10 Git によるデバッグ
- 7.11 サブモジュール
- 7.12 バンドルファイルの作成
- 7.13 Git オブジェクトの置き換え
- 7.14 認証情報の保存
- 7.15 まとめ
-
8. Git のカスタマイズ
- 8.1 Git の設定
- 8.2 Git の属性
- 8.3 Git フック
- 8.4 Git ポリシーの実施例
- 8.5 まとめ
-
9. Gitとその他のシステムの連携
- 9.1 Git をクライアントとして使用する
- 9.2 Git へ移行する
- 9.3 まとめ
-
10. Gitの内側
- 10.1 配管(Plumbing)と磁器(Porcelain)
- 10.2 Gitオブジェクト
- 10.3 Gitの参照
- 10.4 Packfile
- 10.5 Refspec
- 10.6 転送プロトコル
- 10.7 メンテナンスとデータリカバリ
- 10.8 環境変数
- 10.9 まとめ
-
A1. 付録 A: その他の環境でのGit
- A1.1 グラフィカルインタフェース
- A1.2 Visual StudioでGitを使う
- A1.3 EclipseでGitを使う
- A1.4 BashでGitを使う
- A1.5 ZshでGitを使う
- A1.6 PowershellでGitを使う
- A1.7 まとめ
-
A2. 付録 B: Gitをあなたのアプリケーションに組み込む
- A2.1 Gitのコマンドラインツールを使う方法
- A2.2 Libgit2を使う方法
- A2.3 JGit
-
A3. 付録 C: Gitのコマンド
- A3.1 セットアップと設定
- A3.2 プロジェクトの取得と作成
- A3.3 基本的なスナップショット
- A3.4 ブランチとマージ
- A3.5 プロジェクトの共有とアップデート
- A3.6 検査と比較
- A3.7 デバッグ
- A3.8 パッチの適用
- A3.9 メール
- A3.10 外部システム
- A3.11 システム管理
- A3.12 配管コマンド
8.4 Git のカスタマイズ - Git ポリシーの実施例
Git ポリシーの実施例
このセクションでは、これまでに学んだ内容を使って実際に Git のワークフローを確立してみます。 コミットメッセージの書式をチェックし、またプロジェクト内の特定のサブディレクトリを特定のユーザーだけが変更できるようにします。 以降では、開発者に対して「なぜプッシュが却下されたのか」を伝えるためのクライアントスクリプトと、ポリシーを強制するためのサーバースクリプトを作成していきます。
以降で示すスクリプトは Ruby で書かれています。理由としては、我々の知的習慣によるところもありますが、Ruby は(たとえ書けないとしても)読むのが簡単というのも理由のひとつです。 しかし、それ以外の言語であってもきちんと動作します。Git に同梱されているサンプルスクリプトはすべて Perl あるいは Bash で書かれています。サンプルスクリプトを見れば、それらの言語による大量のフックの例を見ることができます。
サーバーサイドフック
サーバーサイドで行う処理は、すべて hooks
ディレクトリの update
ファイルにまとめます。
update
ファイルはプッシュされるブランチごとに実行され、次の3つの引数を取ります。
-
プッシュされる参照の名前
-
操作前のブランチのリビジョン
-
プッシュされる新しいリビジョン
また、SSH 経由でのプッシュの場合は、プッシュしたユーザーを知ることもできます。
全員に共通のユーザー( “git” など)を使って公開鍵認証をしている場合は、公開鍵の情報に基づいて実際のユーザーを判断して環境変数を設定するというラッパーが必要です。
ここでは、接続しているユーザー名が環境変数 $USER
に格納されているものとします。 update
スクリプトは、まず必要な情報を取得するところから始まります。
#!/usr/bin/env ruby
$refname = ARGV[0]
$oldrev = ARGV[1]
$newrev = ARGV[2]
$user = ENV['USER']
puts "Enforcing Policies..."
puts "(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"
そう、グローバル変数を使ってますね。 が、責めないでください – 実例を示すには、こっちの方が簡単なんです。
特定のコミットメッセージ書式の強制
まずは、コミットメッセージを特定の書式に従わせることに挑戦してみましょう。 ここでは、コミットメッセージには必ず “ref: 1234” 形式の文字列を含むこと、というルールにします。個々のコミットをチケットシステムの作業項目とリンクさせたいという意図です。 やらなければならないことは、プッシュされてきた各コミットのコミットメッセージに上記の文字列があるか調べ、なければゼロ以外の値を返して終了し、プッシュを却下することです。
プッシュされたすべてのコミットの SHA-1 値を取得するには、$newrev
と $oldrev
の内容を git rev-list
という Git の配管(plumbing)コマンドに渡します。
これは基本的には git log
コマンドのようなものですが、デフォルトでは SHA-1 値だけを表示してそれ以外の情報は出力しません。
ふたつのコミットの間のすべてのコミットの SHA-1 を得るには、次のようなコマンドを実行します。
$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
この出力を受け取って、ループさせて各コミットの SHA-1 を取得し、個々のメッセージを取り出せば、正規表現でそのメッセージを調べることができます。
さて、これらのコミットからコミットメッセージを取り出す方法を見つけなければなりません。
生のコミットデータを取得するには、別の配管コマンド git cat-file
を使います。
配管コマンドについては [ch10-git-internals] で詳しく説明しますが、とりあえずはこのコマンドがどんな結果を返すのだけを示します。
$ git cat-file commit ca82a6
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700
changed the version number
SHA-1 値がわかっているときにコミットからコミットメッセージを得るシンプルな方法は、空行を探してそれ以降をすべて取得するというものです。
これには、Unix システムの sed
コマンドが使えます。
$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number
プッシュしようとしているコミットから、この呪文を使ってコミットメッセージを取得し、もし条件にマッチしないものがあれば終了させればよいのです。 スクリプトを抜けてプッシュを却下するには、ゼロ以外の値を返して終了します。 以上を踏まえると、このメソッドは次のようになります。
$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
ディレクティブだけを使います。
次のメソッドは連想配列を返すものです。配列のキーはユーザー名、キーに対応する値はそのユーザーが書き込み権限を持つパスの配列になります。
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
オプションを指定するだけです([ch02-git-basics] で簡単に説明しました)。
$ git log -1 --name-only --pretty=format:'' 9f585d
README
lib/test.rb
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.start_with? access_path) # 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
でサーバへプッシュされるコミットの一覧を取得します。
次に、それぞれのコミットでどのファイルが変更されるのかを調べ、プッシュしてきたユーザーにそのファイルを変更する権限があるか確かめています。
これで、まずい形式のコミットメッセージや、指定されたパス以外のファイルの変更を含むコミットはプッシュできなくなりました。
テストを実施する
これまでのコードを書き込んだファイルに対して chmod u+x .git/hooks/update
を実行します。その上で、メッセージが規定に沿っていないコミットをプッシュしてみましょう。すると、こんなメッセージが表示されるでしょう。
$ 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] Your message is not formatted correctly
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) (fb8c72) (c56860)
これは、スクリプトの先頭で標準出力に表示した内容でした。
ここで重要なのは「スクリプトから stdout
に送った内容は、すべてクライアントにも送られる」ということです。
次に注目するのは、エラーメッセージです。
[POLICY] Your message is not formatted correctly
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 メッセージが表示されます。これを見れば、フック内での処理のせいで却下されたのだということがわかります。
また、変更権限のないファイルを変更してそれを含むコミットをプッシュしようとしたときも、同様にエラーが表示されます。
たとえば、ドキュメント担当者が lib
ディレクトリ内の何かを変更しようとした場合のメッセージは次のようになります。
[POLICY] You do not have access to push to lib/test.rb
以降、この update
スクリプトが動いてさえいれば、指定したパターンを含まないコミットメッセージがリポジトリに登録されることは二度とありません。また、ユーザーに変なところをさわられる心配もなくなります。
クライアントサイドフック
この方式の弱点は、プッシュが却下されたときにユーザーが泣き寝入りせざるを得なくなるということです。 手間暇かけて仕上げた作業が最後の最後で却下されるというのは、非常にストレスがたまるし不可解です。さらに、プッシュするためには歴史を修正しなければならないのですが、気弱な人にとってそれはかなりつらいことです。
このジレンマに対する答えとして、サーバーが却下するであろう作業をするときに、それをユーザーに伝えるためのクライアントサイドフックを用意します。
そうすれば、何か問題があるときに、それをコミットする前に知ることができるので、取り返しのつかなくなる前に問題を修正できます。
なおプロジェクトをクローンしてもフックはコピーされないので、別の何らかの方法で各ユーザーにスクリプトを配布した上で、各ユーザーにそれを .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 file changed, 1 insertions(+), 0 deletions(-)
次に、ACL で決められた範囲以外のファイルを変更していないことを確認しましょう。
先ほど使った ACL ファイルのコピーがプロジェクトの .git
ディレクトリにあれば、次のような pre-commit
スクリプトでチェックできます。
#!/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')
もうひとつの違いは、変更されたファイルの一覧を取得する方法です。 サーバーサイドのメソッドではコミットログを調べていました。しかしこの時点ではまだコミットが記録されていないので、ファイルの一覧はステージング・エリアから取得しなければなりません。 つまり、先ほどの
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
は次のようになります。
files_modified = `git diff-index --cached --name-only HEAD`
しかし、違うのはこの二点だけで、それ以外はまったく同じように動作します。
ただしこのスクリプトは、ローカルで実行しているユーザーと、リモートマシンにプッシュするときのユーザーが同じであることを前提にしています。
もし異なる場合は、変数 $user
を手動で設定しなければなりません。
最後に残ったのは fast-forward でないプッシュを止めることです。 fast-forward でない参照を取得するには、すでにプッシュした過去のコミットにリベースするか、別のローカルブランチにリモートブランチと同じところまでプッシュしなければなりません。
サーバーサイドではすでに receive.denyDeletes
と receive.denyNonFastForwards
でこのポリシーを強制しているでしょうから、あり得るのは、すでにプッシュ済みのコミットをリベースしようとするときくらいです。
それをチェックする 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}"
exit 1
end
end
end
このスクリプトでは、 リビジョンの選択 ではカバーしていない構文を使っています。 既にプッシュ済みのコミットの一覧を得るために、次のコマンドを実行します。
`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
SHA^@
構文は、そのコミットのすべての親を解決します。
リモートの最後のコミットから到達可能で、これからプッシュしようとする SHA-1 の親のいずれかからもアクセスできないコミット(これによって fast-forward であることが分かります)を探します。
この方式の弱点は非常に時間がかかることで、多くの場合このチェックは不要です。-f
つきで強制的にプッシュしようとしない限り、サーバーが警告を出してプッシュできないからです。
しかし練習用の課題としてはおもしろいもので、あとでリベースを取り消してやりなおすはめになることを理屈上は防げるようになります。