ruby_git_hooks 0.0.40 → 0.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZWIzZWY4YWFhMDQ3OWYyMGJlOWQ1ZjExZTA2NjY0ZDBjMGFiOWVlZA==
5
+ data.tar.gz: !binary |-
6
+ ZGJiMTIwZDk4YmY5YWYwMTc3MGJiYTcyNWZlOTg5Y2VlOGM2MWIzNA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MTJlOTk4MDliOTA0ZWNkYjQ2Y2FkZmMwZGJlOWQwOTUyOWU3YmUzNGJiN2Fi
10
+ NTk1NTJjMDM4Nzk1YTRkNzhjNWM3ZWZmYjJlZTAyYjAxYjZjZGFjZDIyNzQy
11
+ N2M0NzVlZTY5YjZiMjcwMGQzMDBhNTUwNWNiYWNlYmM0NTU3OWE=
12
+ data.tar.gz: !binary |-
13
+ MjNlNzQzZDVlOWZjYTI0Y2NkMTVlYmMzZjczNjlhM2I5ZGE2NjBjZjVjMTQ3
14
+ NTVlNDRmMGFkYzRkODhjODBjYjk5MTJhYmQ2MjIwYTYyMzJjNzMzYWIxY2Ri
15
+ YWFmMjFhYmE3ODdmYjM5MmUzMDViNWIzYTE0N2MxMjc4YmM4ODk=
@@ -59,11 +59,14 @@ module RubyGitHooks
59
59
 
60
60
  # refs associated with each commit
61
61
  attr_accessor :commit_ref_map
62
+
63
+ # branches included in this push
64
+ attr_accessor :branches_changed
62
65
  end
63
66
 
64
67
  # Instances of Hook delegate these methods to the class methods.
65
68
  HOOK_INFO = [ :files_changed, :file_contents, :file_diffs, :ls_files,
66
- :commits, :commit_message, :commit_message_file, :commit_ref_map ]
69
+ :commits, :commit_message, :commit_message_file, :commit_ref_map, :branches_changed ]
67
70
  HOOK_INFO.each do |info_method|
68
71
  define_method(info_method) do |*args, &block|
69
72
  Hook.send(info_method, *args, &block)
@@ -84,32 +87,38 @@ module RubyGitHooks
84
87
  base, commit, ref = line.strip.split
85
88
  changes.push [base, commit, ref]
86
89
  end
87
- self.commit_ref_map = {} #
88
- # commit_ref_map is a list of which new commits are in this push, and which branches they are associated with
90
+
91
+ self.branches_changed = {} # {ref => [base, commit], ref2 => [base, commit]}
92
+
93
+ self.commit_ref_map = {} # commit_ref_map is a list of which new commits are in this push,
94
+ # and which branches they are associated with
89
95
  # as {commit1 => [ref1, ref2], commit2 => [ref1]}
90
- # For existing branches, this information is sent in directly "base commit ref"
91
- # BUT for branch new branches, the pre/post-receive hook gets "0 commit ref"
92
- # ref is of the form refs/heads/branch_name
93
-
94
- new_branches = changes.select{|base, _, _ | base =~ /\A0+\z/ }.collect{|_,_, ref| ref[/refs\/heads\/(\S+)/,1] }
95
-
96
- if !new_branches.empty?
97
- # For new branches, we will calculate which commits are new by specifically not including commits which are
98
- # present in any other branch (and therefore will have been processed with that branch)
99
- all_branches = Hook.shell!("git branch").split(/[* \n]+/).select{|b| !b.empty?} # remove spaces and the *
100
- # ref is like refs/heads/<branch_name>
101
- existing_branches = all_branches - new_branches
102
- exclude_branches = existing_branches.inject("") {|str, b| str + " ^" + b} # "^B1 ^B2"
96
+
97
+ # figure out which commits have already been processed (everything we have seen before)
98
+ exclude_refs = [] # we know we have seen under these refs already
99
+ # includes all branches not referenced in this push
100
+ # and all commits before the base of referenced branches
101
+ all_branches = Hook.shell!("git for-each-ref --format='%(refname)' refs/heads/").split
102
+ changes.each do |base, _ , ref|
103
+ # ref is of the form refs/heads/branch_name
104
+ all_branches.delete(ref) # we don't want to use the new ref for this branch
105
+ exclude_refs << "^#{base}" unless base =~ /\A0+\z/ # add the old ref for this branch to the exclude list
106
+ # (don't add if it's 0, this is a new branch with no old ref)
103
107
  end
104
108
 
109
+ # add the branches which aren't included in this push if any
110
+ all_branches.each { |ref| exclude_refs << "^#{ref}" }
105
111
 
106
112
  self.files_changed = []
107
113
  self.file_contents = {}
108
114
  self.file_diffs = {}
109
115
 
110
116
  changes.each do |base, commit, ref|
111
- new_branch = base =~ /\A0+\z/
112
- if new_branch
117
+ self.branches_changed[ref] = [base, commit]
118
+
119
+ # TODO : calculate file_diffs and file_contents PER COMMIT for pre and post receive hooks
120
+ # for now it just does the overall diffs
121
+ if base =~ /\A0+\z/
113
122
  # if base is 000 then this is a new branch and we have no easy way know what files were added
114
123
  # so for now just don't include files changed in a new branch
115
124
  # because really this should be done per commit or at least per branch anyway
@@ -134,19 +143,11 @@ module RubyGitHooks
134
143
  file_contents[file_changed] = ""
135
144
  end
136
145
  end
137
-
138
- # now calculate which commits are new
139
- if new_branch
140
- # new branch, but we don't want to include all commits from beginning of time
141
- # so exclude any commits that are on any other branches
142
- # e.g. git rev-list <commit for B3> ^master ^B2
143
- # NOTE: have to use commit, not ref, because if this is called in pre-receive the branch name of ref won't
144
- # actually have been set up yet!
145
- new_commits = Hook.shell!("git rev-list #{commit} #{exclude_branches}").split("\n")
146
- else
147
- # existing branch, base..commit is right
148
- new_commits = Hook.shell!("git rev-list #{base}..#{commit}").split("\n")
149
- end
146
+
147
+ # calculate which commits are new - exclude any commits that are on any other branches
148
+ # e.g. git rev-list <commit for B3> ^old_B3 ^master ^B2 --
149
+ # (the "--" at the end tells git these are refs NOT file references)
150
+ new_commits = Hook.shell!("git rev-list #{commit} #{exclude_refs.join(' ')} --").split("\n")
150
151
 
151
152
  new_commits.each do |one_commit|
152
153
  self.commit_ref_map[one_commit] ||= [];
@@ -109,4 +109,14 @@ module RubyGitHooks::GitOps
109
109
  Hook.shell!("cd #{repo_name} && git rev-list --all").split("\n")
110
110
  end
111
111
 
112
+ def git_merge(repo_name = "child_repo", branch = "B1", msg = "Merge branch #{branch}")
113
+ # better be sure there's not going to be a conflict
114
+ Hook.shell!("cd #{repo_name} && git merge #{branch} --no-ff -m '#{msg}'")
115
+ end
116
+
117
+ def git_ff_merge(repo_name = "child_repo", branch = "B1", msg = "Merge branch #{branch}")
118
+ # better be sure there's not going to be a conflict
119
+ Hook.shell!("cd #{repo_name} && git merge #{branch} --ff")
120
+ end
121
+
112
122
  end
@@ -64,9 +64,6 @@ class JiraCommentAddHook < RubyGitHooks::Hook
64
64
 
65
65
 
66
66
  def check
67
- if commits.empty?
68
- STDERR.puts "JiraCommentAddHook - need list of commits to process"
69
- end
70
67
  # called with a list of commits to check, as post-receive.
71
68
  # consider it a success for now only if all commit checks are successful
72
69
  # may cause us to redo some of the checks.
@@ -114,6 +111,24 @@ class JiraCommentAddHook < RubyGitHooks::Hook
114
111
  uri = "#{repo_remote_path}/commit/#{commit}"
115
112
  end
116
113
 
114
+ def get_change_list(commit)
115
+ # we want changes from the previous commit, if any
116
+ # ideally this list should be available from the ruby_git_hooks directly
117
+ # since they go through this same process.
118
+ # use --first-parent so it lists the correct files after a merge
119
+ current, base = Hook.shell!("git log #{commit} --first-parent -2 --pretty=%H").split
120
+ if !base
121
+ # This is the initial commit so all files were added, but have to add the A ourselves
122
+ files_with_status = Hook.shell!("git ls-tree --name-status -r #{commit}").split("\n")
123
+ # put the A at the front
124
+ files_with_status.map!{|filename| "A\t" + filename}
125
+ else
126
+
127
+ files_with_status = Hook.shell!("git diff --name-status #{base}..#{current}")
128
+ end
129
+ files_with_status
130
+ end
131
+
117
132
  def get_commit_branch(commit)
118
133
  # get the branch (list) for this commit
119
134
  # will usually be a single ref ([refs/heads/branch_name]). but could
@@ -138,21 +153,19 @@ class JiraCommentAddHook < RubyGitHooks::Hook
138
153
  # M test.txt
139
154
 
140
155
  github_link = build_commit_uri(commit) # have to do this separately
141
- branch = "Branch: #{get_commit_branch(commit)}"
142
- begin
143
- content = "Revision: %h committed by %cn%nCommit date: %cd%n#{branch}%n#{github_link}%n%n#{commit_message}%n{noformat}"
144
- text = Hook.shell!("git log #{commit} -1 --name-status --pretty='#{content}'")
145
- text += "{noformat}" # git log puts changes at the bottom, we need to close the noformat tag for Jira
146
- rescue
147
- text = "No commit details available for #{commit}\n#{commit_message}"
148
- end
149
- text
156
+ changes = get_change_list(commit)
157
+ revision_and_date = Hook.shell!("git log #{commit} -1 --pretty='Revision: %h committed by %cn%nCommit date: %cd'") rescue ""
158
+ branch = "Branch: #{get_commit_branch(commit)}\n"
159
+
160
+ text = "#{revision_and_date}#{branch}#{github_link}\n\n#{commit_message}\n{noformat}\n#{changes}{noformat}"
161
+
162
+
150
163
  end
151
164
 
152
165
  def check_one_commit(commit, commit_message)
153
166
  STDERR.puts "Checking #{commit[0..6]} #{commit_message.lines.first}"
154
167
 
155
- jira_tickets = commit_message.scan(JiraReferenceCheckHook::JIRA_TICKET_REGEXP).map(&:strip)
168
+ jira_tickets = commit_message.scan(JiraReferenceCheckHook::JIRA_TICKET_REGEXP).map(&:strip).uniq
156
169
  if jira_tickets.length == 0
157
170
  STDERR.puts ">>Commit message must refer to a jira ticket"
158
171
  add_error_to_report(commit, commit_message, "no_jira")
@@ -1,5 +1,5 @@
1
1
  # Copyright (C) 2013-2014 OL2, Inc. See LICENSE.txt for details.
2
2
 
3
3
  module RubyGitHooks
4
- VERSION = "0.0.40"
4
+ VERSION = "0.0.44"
5
5
  end
@@ -51,6 +51,7 @@ class TestHook < RubyGitHooks::Hook
51
51
  def check
52
52
  File.open("#{TEST_PATH}", "w") do |f|
53
53
  f.puts commit_ref_map.inspect
54
+ f.puts branches_changed.keys
54
55
  end
55
56
 
56
57
  puts "Test hook runs!"
@@ -216,6 +217,67 @@ HOOK
216
217
 
217
218
  end
218
219
 
220
+ def test_pre_receive_with_merge_commit
221
+ @hook_name ||= "pre-receive"
222
+
223
+ add_hook("parent_repo.git", @hook_name, TEST_HOOK_MULTI)
224
+
225
+ # set up master and 2 branches with commits
226
+ # make changes to different files so no merge conflicts
227
+ new_commit("child_repo", "file1.txt","Contents", "master commit")
228
+ git_create_and_checkout_branch("child_repo", "B1")
229
+ new_commit("child_repo", "file2.txt","Contents", "B1 commit")
230
+ git_create_and_checkout_branch("child_repo", "B2")
231
+ new_commit("child_repo", "file3.txt","Contents", "B2 commit")
232
+ git_checkout("child_repo", "master")
233
+ new_commit("child_repo", "file4.txt","Contents", "master commit")
234
+
235
+ git_push_all("child_repo")
236
+ before_commits = git_revlist_all("child_repo") # commits already in the repo
237
+
238
+ # now do a merge commit
239
+ git_checkout("child_repo", "master")
240
+ git_merge("child_repo", "B1")
241
+ git_push_all("child_repo")
242
+
243
+ # make sure none of the before_commits are in the output
244
+ contents = File.read(TEST_PATH)
245
+ before_commits.each do |c|
246
+ refute contents.include?(c), "#{c} shouldn't be processed!"
247
+ end
248
+ end
249
+
250
+ def test_pre_receive_ff_merge
251
+ @hook_name ||= "pre-receive"
252
+
253
+ add_hook("parent_repo.git", @hook_name, TEST_HOOK_MULTI)
254
+
255
+ git_create_and_checkout_branch("child_repo", "B1")
256
+ new_commit("child_repo", "file22.txt","Contents", "B1 commit")
257
+ new_commit("child_repo", "file23.txt","Contents", "B1 commit 2")
258
+ git_push_all("child_repo")
259
+ before_commits = git_revlist_all("child_repo") # commits already in the repo
260
+
261
+ # now a merge ff commit
262
+ # shouldn't be any new commits
263
+ git_checkout("child_repo", "master")
264
+ git_ff_merge("child_repo", "B1")
265
+ after_commits = git_revlist_all("child_repo") # commits already in the repo
266
+
267
+ git_push_all("child_repo")
268
+
269
+ assert_empty(before_commits-after_commits) # there are no new commits
270
+ contents = File.read(TEST_PATH)
271
+ before_commits.each do |c|
272
+ refute contents.include?(c), "#{c} shouldn't be processed!"
273
+ end
274
+
275
+ # should check that branches_changed is accurate
276
+ assert contents.include?("master")
277
+
278
+
279
+ end
280
+
219
281
 
220
282
  def test_pre_receive_with_delete
221
283
  @hook_name ||= "pre-receive"
@@ -1,3 +1,4 @@
1
+ # encoding: UTF-8
1
2
  # Copyright (C) 2013-2014 OL2, Inc. See LICENSE.txt for details.
2
3
 
3
4
  require "test_helper"
@@ -32,12 +33,27 @@ class JiraCommentAddHookTest < HookTestCase
32
33
  "username" => "user", "password" => "password"
33
34
  end
34
35
 
35
- def fake_hook_check(msg = "Commit message")
36
+ def fake_hook_check(msg = "Commit message", with_branch_merge = false)
36
37
  new_commit("child_repo", "file.txt","Contents", msg)
37
38
  stub(@hook).commit_message { msg }
38
39
  sha = last_commit_sha("child_repo")
39
- stub(@hook).commits{[sha]}
40
- stub(@hook).commit_ref_map{ {sha => ["refs/heads/master"]} } # it's always the master branch
40
+ hook_refs = {sha => ["refs/heads/master"]} # it's always the master branch
41
+ if with_branch_merge
42
+ git_create_and_checkout_branch("child_repo", "B1")
43
+ new_commit("child_repo", "file2.txt","Contents", "B1 commit\n(#{msg})")
44
+ sha2 = last_commit_sha("child_repo")
45
+
46
+ git_checkout("child_repo", "master")
47
+ git_merge("child_repo", "B1", "Merge Branch B1\n(#{msg})")
48
+ merge_sha = last_commit_sha("child_repo")
49
+
50
+ hook_refs[sha2] = ["refs/heads/master","refs/heads/B1"]
51
+ hook_refs[merge_sha] = ["refs/heads/master"]
52
+ end
53
+
54
+ stub(@hook).commit_ref_map{ hook_refs }
55
+ stub(@hook).commits{hook_refs.keys}
56
+
41
57
  Dir.chdir("child_repo") do
42
58
  @hook.check
43
59
  end
@@ -91,6 +107,22 @@ JSON
91
107
  # but at least we'll make sure it doesn't fail.
92
108
  end
93
109
 
110
+ def test_same_good_reference_twice
111
+ mock(RestClient).get("https://user:password@jira.example.com/rest/api/latest/issue/GOOD-234") { <<JSON }
112
+ { "fields": { "status": { "name": "Open" } } }
113
+ JSON
114
+
115
+ mock(RestClient).post.with_any_args {<<JSON } # more complicated to check the args, just be sure it's called.
116
+ { "fields": { "status": { "name": "Open" } } }
117
+ JSON
118
+ git_tag("child_repo", "0.1")
119
+ fake_hook_check("Message with GOOD-234 reference to Jira and another GOOD-234" )
120
+
121
+ # as long as the mocked RestClient calls happen, we succeeded
122
+ # would be better if we had a way to check if the tag is in the message
123
+ # but at least we'll make sure it doesn't fail.
124
+ end
125
+
94
126
  def test_good_reference_with_description
95
127
 
96
128
  mock(RestClient).get("https://user:password@jira.example.com/rest/api/latest/issue/GOOD-234") { <<JSON }
@@ -103,8 +135,32 @@ JSON
103
135
  # add a tag so describe works
104
136
 
105
137
  fake_hook_check("Message with GOOD-234 reference to Jira" )
138
+ end
139
+
140
+ def test_good_reference_with_long_message
106
141
 
142
+ mock(RestClient).get("https://user:password@jira.example.com/rest/api/latest/issue/GOOD-234") { <<JSON }
143
+ { "fields": { "status": { "name": "Open" } } }
144
+ JSON
145
+
146
+ mock(RestClient).post.with_any_args {<<JSON } # more complicated to check the args, just be sure it's called.
147
+ { "fields": { "status": { "name": "Open" } } }
148
+ JSON
107
149
 
150
+ fake_hook_check("Message with GOOD-234 reference to Jira\n\nWhat if it can't handle unicode like ©?\n(Good, it can!)" )
151
+ end
152
+
153
+ def test_good_ref_with_merge
154
+ stub(RestClient).get("https://user:password@jira.example.com/rest/api/latest/issue/GOOD-234") { <<JSON }
155
+ { "fields": { "status": { "name": "Open" } } }
156
+ JSON
157
+
158
+ stub(RestClient).post.with_any_args {<<JSON } # more complicated to check the args, just be sure it's called.
159
+ { "fields": { "status": { "name": "Open" } } }
160
+ JSON
161
+ # look at output to see what gets generated for message
162
+ puts "***** STARTING MERGE REF CHECK *****"
163
+ fake_hook_check("Message with GOOD-234 reference to Jira" , true)
108
164
  end
109
165
 
110
166
 
@@ -169,4 +225,5 @@ JSON
169
225
 
170
226
 
171
227
 
228
+
172
229
  end
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_git_hooks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.40
5
- prerelease:
4
+ version: 0.0.44
6
5
  platform: ruby
7
6
  authors:
8
7
  - Noah Gibbs
@@ -11,12 +10,11 @@ authors:
11
10
  autorequire:
12
11
  bindir: bin
13
12
  cert_chain: []
14
- date: 2014-04-14 00:00:00.000000000 Z
13
+ date: 2014-05-01 00:00:00.000000000 Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
16
  name: pony
18
17
  requirement: !ruby/object:Gem::Requirement
19
- none: false
20
18
  requirements:
21
19
  - - ! '>='
22
20
  - !ruby/object:Gem::Version
@@ -24,7 +22,6 @@ dependencies:
24
22
  type: :runtime
25
23
  prerelease: false
26
24
  version_requirements: !ruby/object:Gem::Requirement
27
- none: false
28
25
  requirements:
29
26
  - - ! '>='
30
27
  - !ruby/object:Gem::Version
@@ -32,7 +29,6 @@ dependencies:
32
29
  - !ruby/object:Gem::Dependency
33
30
  name: rest-client
34
31
  requirement: !ruby/object:Gem::Requirement
35
- none: false
36
32
  requirements:
37
33
  - - ! '>='
38
34
  - !ruby/object:Gem::Version
@@ -40,7 +36,6 @@ dependencies:
40
36
  type: :runtime
41
37
  prerelease: false
42
38
  version_requirements: !ruby/object:Gem::Requirement
43
- none: false
44
39
  requirements:
45
40
  - - ! '>='
46
41
  - !ruby/object:Gem::Version
@@ -48,7 +43,6 @@ dependencies:
48
43
  - !ruby/object:Gem::Dependency
49
44
  name: json
50
45
  requirement: !ruby/object:Gem::Requirement
51
- none: false
52
46
  requirements:
53
47
  - - ! '>='
54
48
  - !ruby/object:Gem::Version
@@ -56,7 +50,6 @@ dependencies:
56
50
  type: :runtime
57
51
  prerelease: false
58
52
  version_requirements: !ruby/object:Gem::Requirement
59
- none: false
60
53
  requirements:
61
54
  - - ! '>='
62
55
  - !ruby/object:Gem::Version
@@ -64,7 +57,6 @@ dependencies:
64
57
  - !ruby/object:Gem::Dependency
65
58
  name: bundler
66
59
  requirement: !ruby/object:Gem::Requirement
67
- none: false
68
60
  requirements:
69
61
  - - ~>
70
62
  - !ruby/object:Gem::Version
@@ -72,7 +64,6 @@ dependencies:
72
64
  type: :development
73
65
  prerelease: false
74
66
  version_requirements: !ruby/object:Gem::Requirement
75
- none: false
76
67
  requirements:
77
68
  - - ~>
78
69
  - !ruby/object:Gem::Version
@@ -80,7 +71,6 @@ dependencies:
80
71
  - !ruby/object:Gem::Dependency
81
72
  name: minitest
82
73
  requirement: !ruby/object:Gem::Requirement
83
- none: false
84
74
  requirements:
85
75
  - - ! '>='
86
76
  - !ruby/object:Gem::Version
@@ -88,7 +78,6 @@ dependencies:
88
78
  type: :development
89
79
  prerelease: false
90
80
  version_requirements: !ruby/object:Gem::Requirement
91
- none: false
92
81
  requirements:
93
82
  - - ! '>='
94
83
  - !ruby/object:Gem::Version
@@ -96,7 +85,6 @@ dependencies:
96
85
  - !ruby/object:Gem::Dependency
97
86
  name: rr
98
87
  requirement: !ruby/object:Gem::Requirement
99
- none: false
100
88
  requirements:
101
89
  - - ! '>='
102
90
  - !ruby/object:Gem::Version
@@ -104,7 +92,6 @@ dependencies:
104
92
  type: :development
105
93
  prerelease: false
106
94
  version_requirements: !ruby/object:Gem::Requirement
107
- none: false
108
95
  requirements:
109
96
  - - ! '>='
110
97
  - !ruby/object:Gem::Version
@@ -112,7 +99,6 @@ dependencies:
112
99
  - !ruby/object:Gem::Dependency
113
100
  name: rake
114
101
  requirement: !ruby/object:Gem::Requirement
115
- none: false
116
102
  requirements:
117
103
  - - ! '>='
118
104
  - !ruby/object:Gem::Version
@@ -120,7 +106,6 @@ dependencies:
120
106
  type: :development
121
107
  prerelease: false
122
108
  version_requirements: !ruby/object:Gem::Requirement
123
- none: false
124
109
  requirements:
125
110
  - - ! '>='
126
111
  - !ruby/object:Gem::Version
@@ -187,27 +172,26 @@ files:
187
172
  homepage: ''
188
173
  licenses:
189
174
  - MIT
175
+ metadata: {}
190
176
  post_install_message:
191
177
  rdoc_options: []
192
178
  require_paths:
193
179
  - lib
194
180
  required_ruby_version: !ruby/object:Gem::Requirement
195
- none: false
196
181
  requirements:
197
182
  - - ! '>='
198
183
  - !ruby/object:Gem::Version
199
184
  version: '0'
200
185
  required_rubygems_version: !ruby/object:Gem::Requirement
201
- none: false
202
186
  requirements:
203
187
  - - ! '>='
204
188
  - !ruby/object:Gem::Version
205
189
  version: '0'
206
190
  requirements: []
207
191
  rubyforge_project:
208
- rubygems_version: 1.8.23
192
+ rubygems_version: 2.2.2
209
193
  signing_key:
210
- specification_version: 3
194
+ specification_version: 4
211
195
  summary: DSL and manager for git hooks in Ruby.
212
196
  test_files:
213
197
  - test/basic_hook_test.rb