-
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 サードパーティのホスティングサービス
- 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ツール
-
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 プルーミングとポーセリン
- 10.2 Gitオブジェクト
- 10.3 Gitリファレンス
- 10.4 パックファイル
- 10.5 リフスペック
- 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 プルーミングコマンド
8.4 Gitのカスタマイズ - Gitで強制されるポリシーの例
Gitで強制されるポリシーの例
このセクションでは、学習した内容を活かして、カスタムコミットメッセージ形式をチェックし、特定のユーザーのみがプロジェクト内の特定のサブディレクトリを変更できるようにするGitワークフローを確立します。開発者がプッシュが拒否されるかどうかを知るのに役立つクライアントスクリプトと、実際にポリシーを強制するサーバスクリプトを作成します。
ここで示すスクリプトはRubyで書かれています。その理由は、私たちの知識慣性によるものもありますが、Rubyは書けなくても読みやすいからです。しかし、どの言語でも動作します — Gitに同梱されているフックスクリプトのサンプルはすべてPerlかBashで書かれているので、サンプルを見ればこれらの言語でのフックの例もたくさん見ることができます。
サーバサイドフック
すべてのサーバサイドの作業は、あなたのhooks
ディレクトリにあるupdate
ファイルに入ります。update
フックは、プッシュされるブランチごとに1回実行され、3つの引数を取ります
-
プッシュ先の参照名
-
そのブランチがあった古いリビジョン
-
プッシュされる新しいリビジョン
プッシュがSSH経由で実行されている場合、プッシュを行っているユーザーにもアクセスできます。公開鍵認証を介して全員が単一のユーザー(「git」など)で接続できるようにしている場合、公開鍵に基づいてどのユーザーが接続しているかを判断し、それに応じて環境変数を設定するシェルラッパーをそのユーザーに与える必要があるかもしれません。ここでは、接続しているユーザーが$USER
環境変数に入っていると仮定し、必要なすべての情報を収集することから更新スクリプトを開始します
#!/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のプルーミングコマンドに渡すことで取得できます。これは基本的に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)を使用するメカニズムを追加したいとします。一部のユーザーはフルアクセス権を持ち、他のユーザーは特定のサブディレクトリまたは特定のファイルへの変更のみをプッシュできます。これを強制するために、これらのルールをサーバ上のベアリポジトリにあるacl
というファイルに書き込みます。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
オプション(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
を使用して、サーバにプッシュされている新しいコミットのリストを取得します。その後、それらの各コミットについて、どのファイルが変更されているかを調べ、プッシュしているユーザーが変更されているすべてのパスへのアクセス権を持っていることを確認します。
これで、ユーザーは不正な形式のメッセージを含むコミットや、指定されたパス外の変更されたファイルを含むコミットをプッシュできなくなります。
試してみる
もし、このすべてのコードを配置したファイルである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')
もう一つの重要な違いは、変更されたファイルのリストを取得する方法です。サーバサイドの方法はコミットログを参照しますが、この時点ではコミットはまだ記録されていないため、代わりにステージングエリアからファイルリストを取得する必要があります。次のようにする代わりに
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
このように使用する必要があります
files_modified = `git diff-index --cached --name-only HEAD`
しかし、これらが唯一の2つの違いです — それ以外は、スクリプトは同じように動作します。1つの注意点としては、リモートマシンにプッシュするのと同じユーザーとしてローカルで実行していることを想定しています。もしそれが異なる場合は、$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
で強制プッシュしようとしない限り、サーバは警告を発し、プッシュを受け入れません。しかし、これは興味深い演習であり、理論的には後で修正しなければならないかもしれないリベースを回避するのに役立ちます。