-
1. Gitを始めるにあたって
- 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 スマートHTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 サードパーティのホスティングオプション
- 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 Resetの解明
- 7.8 高度なマージ
- 7.9 Rerere
- 7.10 Gitを使ったデバッグ
- 7.11 サブモジュール
- 7.12 バンドル
- 7.13 置換
- 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 Visual Studio CodeでのGit
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMineでのGit
- A1.5 Sublime TextでのGit
- A1.6 BashでのGit
- A1.7 ZshでのGit
- A1.8 PowerShellでのGit
- A1.9 まとめ
-
A2. 付録B: アプリケーションへのGitの組み込み
- A2.1 コマンドラインGit
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
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 Plumbingコマンド
8.4 Gitのカスタマイズ - Gitによる強制ポリシーの例
Gitによる強制ポリシーの例
このセクションでは、学習したことを利用して、カスタムコミットメッセージ形式をチェックし、特定のユーザーのみがプロジェクトの特定のサブディレクトリを変更できるようにするGitワークフローを確立します。開発者がプッシュが拒否されるかどうかを判断するのに役立つクライアントスクリプトと、実際にポリシーを強制するサーバスクリプトを作成します。
ここで示すスクリプトはRubyで書かれています。これは、私たちの知的な慣性のためもありますが、Rubyが読みやすい言語であるためでもあります(必ずしも書ける必要はありません)。しかし、どの言語でも機能します。Gitに配布されているすべてのサンプルフックスクリプトはPerlまたはBashで書かれているため、これらの言語のフックの多くの例をサンプルで確認することもできます。
サーバサイドフック
すべてのサーバサイドの作業は、hooks
ディレクトリ内のupdate
ファイルに入ります。update
フックは、プッシュされるブランチごとに1回実行され、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」のような文字列を含める必要があると仮定します。これは、各コミットをチケットシステムの作業項目にリンクさせたいからです。プッシュされる各コミットを調べ、その文字列がコミットメッセージに含まれているかを確認し、いずれかのコミットに文字列が含まれていない場合、プッシュを拒否するために0以外の値を返します。
プッシュされるすべてのコミットのSHA-1値のリストは、$newrev
と$oldrev
の値をgit rev-list
というGitのプラミングコマンドに渡すことで取得できます。これは基本的にgit log
コマンドですが、デフォルトではSHA-1値のみが出力され、他の情報は出力されません。したがって、あるコミットSHA-1と別のコミットSHA-1の間に導入されたすべてのコミットSHA-1のリストを取得するには、次のようなコマンドを実行します。
$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
その出力を取得し、それぞれのコミットSHA-1をループ処理し、そのメッセージを取得して、パターンを探す正規表現に対してそのメッセージをテストできます。
テストするために、これらの各コミットからコミットメッセージを取得する方法を見つける必要があります。生のコミットデータを取得するには、git cat-file
という別のプラミングコマンドを使用できます。これらのプラミングコマンドについては、Gitの内側で詳しく説明しますが、ここでは、そのコマンドが何を与えるかを示します。
$ 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
Change the version number
SHA-1値を持つコミットからコミットメッセージを取得する簡単な方法は、最初の空白行に行き、それ以降のすべてを取得することです。これはUnixシステムでsed
コマンドを使用して行えます。
$ git cat-file commit ca82a6 | sed '1,/^$/d'
Change 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リポジトリに存在するacl
という名前のファイルに書き込みます。update
フックはこれらのルールを調べ、プッシュされているすべてのコミットで導入されているファイルを確認し、プッシュを実行しているユーザーがそれらのすべてのファイルを更新するアクセス権を持っているかどうかを判断します。
まず、ACLを作成します。ここでは、CVS ACLメカニズムに非常によく似た形式を使用します。一連の行で構成され、最初のフィールドはavail
またはunavail
、次のフィールドはルールが適用されるユーザーのコンマ区切りリスト、最後のフィールドはルールが適用されるパス(空白はオープンアクセスを意味します)です。これらのすべてのフィールドはパイプ(|
)文字で区切られます。
この場合、管理者数名、`doc`ディレクトリへのアクセス権を持つドキュメント作成者数名、そして`lib`と`tests`ディレクトリにのみアクセス権を持つ開発者が1名いるとします。その場合、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
オプションを指定すると(Gitの基本で簡単に触れられています)、単一のコミットでどのファイルが変更されたかをかなり簡単に確認できます。
$ 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
でサーバーにプッシュされる新しいコミットのリストを取得します。次に、それらのコミットごとに、どのファイルが変更されたかを調べ、プッシュしているユーザーが変更されているすべてのパスにアクセスできることを確認します。
これで、ユーザーは形式が不適切なメッセージを含むコミットや、指定されたパス外の変更されたファイルを含むコミットをプッシュできなくなります。
テストしてみる
このすべてのコードを配置したファイルである.git/hooks/update
を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が伝えているものです。最後に、これがあります。
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
フックが拒否した各参照について、リモートリジェクトメッセージが表示され、フックの失敗が原因で拒否されたことが示されます。
さらに、アクセス権のないファイルを編集しようとし、それを含むコミットをプッシュしようとすると、同様のメッセージが表示されます。例えば、ドキュメントの作成者がlib
ディレクトリ内の何かを変更するコミットをプッシュしようとすると、次のようになります。
[POLICY] You do not have access to push to lib/test.rb
今後、そのupdate
スクリプトが存在し、実行可能である限り、リポジトリにパターンを含まないコミットメッセージは存在せず、ユーザーはサンドボックス化されます。
クライアントサイドフック
このアプローチの欠点は、ユーザーのコミットプッシュが拒否されたときに必然的に発生する不満です。苦労して作成した作業が土壇場で拒否されることは、非常にイライラし、混乱を招く可能性があります。さらに、修正するために履歴を編集しなければならず、これは常に気の弱い人向けではありません。
このジレンマに対する答えは、ユーザーがサーバーが拒否する可能性のあることを行っているときに通知するために実行できるクライアントサイドフックを提供することです。そうすることで、コミットする前、そして問題の解決がより困難になる前に、問題を修正できます。フックはプロジェクトのクローンと一緒に転送されないため、これらのスクリプトを別の方法で配布し、ユーザーにそれらを.git/hooks
ディレクトリにコピーさせて実行可能にする必要があります。これらのフックはプロジェクト内または別のプロジェクトで配布できますが、Gitはそれらを自動的に設定しません。
まず、コミットが記録される直前にコミットメッセージをチェックして、サーバーが不適切な形式のコミットメッセージのために変更を拒否しないようにします。これを行うには、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の範囲外のファイルを変更していないことを確認します。プロジェクトの.git
ディレクトリに以前使用したACLファイルのコピーが含まれている場合、次の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
これは、サーバー側の部分とほぼ同じスクリプトですが、2つの重要な違いがあります。まず、ACLファイルは別の場所にあります。このスクリプトは、.git
ディレクトリからではなく、作業ディレクトリから実行されるためです。ACLファイルへのパスを次のように変更する必要があります。
access = get_acl_access_data('acl')
これに
access = get_acl_access_data('.git/acl')
もう1つの重要な違いは、変更されたファイルのリストを取得する方法です。サーバー側のメソッドはコミットのログを調べるため、この時点ではまだコミットは記録されていません。代わりにステージングエリアからファイルリストを取得する必要があります。そのため、
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
を使用する必要があります。
files_modified = `git diff-index --cached --name-only HEAD`
しかし、それらが唯一の違いです。それ以外は、スクリプトは同じように機能します。注意点としては、リモートマシンにプッシュするのと同じユーザーとしてローカルで実行していることを想定していることです。もし異なる場合は、$user
変数を手動で設定する必要があります。
他にできることとして、ユーザーがノンファストフォワード参照をプッシュしないようにすることもできます。ノンファストフォワード参照を取得するには、すでにプッシュしたコミットより前のリベースを行うか、別のローカルブランチを同じリモートブランチにプッシュしようとする必要があります。
おそらく、サーバーはすでにreceive.denyDeletes
とreceive.denyNonFastForwards
でこのポリシーを強制するように設定されているので、誤って捕らえようとできる唯一のことは、すでにプッシュされたコミットのリベースです。
リベース前のスクリプトの例を次に示します。これは、書き換えようとしているすべてのコミットのリストを取得し、それらがリモート参照のいずれかに存在するかどうかをチェックします。リモート参照のいずれかから到達可能なものが見つかった場合、リベースを中止します。
#!/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のいずれかの親から到達できないコミットを探しています。つまり、それはファストフォワードです。
このアプローチの主な欠点は、非常に遅く、しばしば不要であることです。-f
で強制プッシュを試みなければ、サーバーは警告を発し、プッシュを受け入れません。ただし、これは興味深い演習であり、理論的には後で修正する必要があるかもしれないリベースを回避するのに役立ちます。