ruby_git_hooks 0.0.31

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,354 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ require "ruby_git_hooks"
4
+ require "ruby_git_hooks/jira_ref_check"
5
+
6
+ require "rest-client"
7
+ require "json"
8
+
9
+ # This hook adds Jira "commit" comments for your commits. It is
10
+ # called as a post-receive hook with a list of commits - ideally the
11
+ # ruby_git_hooks framework would allow us to get each commit message
12
+ # from them but for now we'll do it ourselves.
13
+
14
+ # The hook checks that commit message has one or more valid Jira
15
+ # ticket references. In general we can't always reject a commit. So
16
+ # we continue through the list of commits, check everything and report
17
+ # errors.
18
+
19
+
20
+ class JiraCommentAddHook < RubyGitHooks::Hook
21
+ Hook = RubyGitHooks::Hook
22
+
23
+ OPTIONS = [ "protocol", "host", "username", "password",
24
+ "api_path", "github", "issues",
25
+ "domain", "from", "subject", "via", "via_options", "intro", "conclusion",
26
+ "no_send", "check_status"]
27
+ VALID_ERROR_TYPES = [:no_jira, :invalid_jira]
28
+
29
+ attr_accessor :errors_to_report
30
+
31
+ def initialize(options = {})
32
+ bad_options = options.keys - OPTIONS
33
+ raise "JiraCommentAddHook created with unrecognized options: " +
34
+ "#{bad_options.inspect}!" if bad_options.size > 0
35
+
36
+ if !options.has_key?("username") || !options.has_key?("password")
37
+ raise "You must provide Jira server user name and password in options"
38
+ end
39
+
40
+ @options = options
41
+ @options["protocol"] ||= "https"
42
+ @options["host"] ||= "jira"
43
+ @options["api_path"] ||= "rest/api/latest/issue"
44
+ @options["github"] ||= "github.com"
45
+ @options["check_status"] = true if !@options.has_key? "check_status" # don't allow "closed" issues by default
46
+
47
+ # options for error emailing
48
+
49
+ @options["domain"] ||= "mydomain.com"
50
+ @options["from"] ||= "Jira Jailer <noreply@#{@options["domain"]}>"
51
+ @options["subject"] ||= "Use Jira Ticket Numbers, Please!"
52
+ @options["via"] ||= "no_send"
53
+ @options["via_options"] ||= {}
54
+
55
+
56
+ @errors_to_report = {} # listed in hash indexed by user
57
+ end
58
+
59
+ def build_uri(ticket, command=nil)
60
+ uri = "#{@options['protocol']}://#{@options['username']}:#{@options['password']}@#{@options['host']}/#{@options['api_path']}/#{ticket}"
61
+ uri = "#{uri}/#{command}" if command
62
+ return uri
63
+ end
64
+
65
+
66
+ def check
67
+ if commits.empty?
68
+ STDERR.puts "JiraCommentAddHook - need list of commits to process"
69
+ end
70
+ # called with a list of commits to check, as post-receive.
71
+ # consider it a success for now only if all commit checks are successful
72
+ # may cause us to redo some of the checks.
73
+ # but for now it's all or nothing.
74
+ success = true
75
+ commits.reverse_each do |commit|
76
+ commit_message = RubyGitHooks::Hook.shell!("git log #{commit} -1 --pretty=%B").rstrip
77
+ success = false unless check_one_commit(commit, commit_message )
78
+ end
79
+
80
+ # send email regarding failed commits
81
+ report_errors
82
+ return success
83
+ end
84
+
85
+ # Do not show password when converting to string
86
+ def to_s
87
+ "<JiraCommentAddHook:#{object_id} #{@options.merge("password" => :redacted)}>"
88
+ end
89
+
90
+
91
+ def repo_remote_path
92
+ remote_urls = RubyGitHooks::Hook.shell!("git remote -v").split
93
+ remote = remote_urls[1] # ["origin", "git@github.com:my_github_name/ruby_git_hooks.git", "fetch", ...]
94
+ return "" if !remote # No remote.
95
+
96
+ uri = URI.parse(remote) rescue nil
97
+ if uri
98
+ # "https://github.com/my_github_name/ruby_git_hooks.git "
99
+ uri.to_s.sub(/.git\z/, "")
100
+ else
101
+ # "git@github.com:my_github_name/ruby_git_hooks.git"
102
+ # ?? Can there be a "." in a repo name?
103
+ path = remote[/:([\w\/.-]*)/,1]
104
+ path.sub!(/.git\z/, "") if path
105
+ "#{@options['protocol']}://#{@options['github']}/#{path}"
106
+ end
107
+ # in either case return "https://github.com/my_github_name/ruby_git_hooks"
108
+
109
+ end
110
+
111
+ def build_commit_uri(commit)
112
+ # like https://github.com/my_github_name/ruby_git_hooks/commit/b067c718a74315224bf88a267a82ac85054cdf6e
113
+
114
+ uri = "#{repo_remote_path}/commit/#{commit}"
115
+ end
116
+
117
+ def get_change_list(commit)
118
+ # we want changes from the previous commit, if any
119
+ # ideally this list should be available from the ruby_git_hooks directly
120
+ # since they go through this same process.
121
+ current, base = Hook.shell!("git log #{commit} -2 --pretty=%H").split
122
+ if !base
123
+ # This is the initial commit so all files were added, but have to add the A ourselves
124
+ files_with_status = Hook.shell!("git ls-tree --name-status -r #{commit}").split("\n")
125
+ # put the A at the front
126
+ files_with_status.map!{|filename| "A\t" + filename}
127
+ else
128
+
129
+ files_with_status = Hook.shell!("git diff --name-status #{base}..#{current}")
130
+ end
131
+ files_with_status
132
+ end
133
+
134
+ def get_comment_content(commit, commit_message)
135
+ # Needs to look like the git equivalent of this
136
+ #/opt/svn/ops rev 37251 committed by john.doe (commit shah and committer)
137
+ #http://viewvc.example.com/viewvc/ops?rev=37251&view=rev (github link)
138
+ #BUG-3863 adding check to configs for testing (commit message and changes)
139
+ # U /trunk/puppet/dist/nagios/nrpe.cfg
140
+ # U /trunk/puppet/dist/nagios/ol_checks.cfg
141
+ # return as a string
142
+ # revision bac9b85f2 committed by Ruth Helfinstein
143
+ # Fri Jul 12 13:57:28 2013 -0700
144
+ # https://github.com/ruth-helfinstein/ruth-test/commit/bac9b85f2c98ccdba8d25f0b9a6e855cd2535901
145
+ # BUG-5366 commit message
146
+ #
147
+ # M test.txt
148
+
149
+ github_link = build_commit_uri(commit) # have to do this separately
150
+ changes = get_change_list(commit)
151
+
152
+ revision_and_date = Hook.shell!("git log #{commit} -1 --pretty='Revision: %h committed by %cn%nCommit date: %cd'") rescue ""
153
+
154
+ text = "#{revision_and_date}#{github_link}\n\n#{commit_message}\n{noformat}#{changes}{noformat}"
155
+ end
156
+
157
+ def check_one_commit(commit, commit_message)
158
+ STDERR.puts "Checking #{commit[0..6]} #{commit_message.lines.first}"
159
+
160
+ jira_tickets = commit_message.scan(JiraReferenceCheckHook::JIRA_TICKET_REGEXP).map(&:strip)
161
+ if jira_tickets.length == 0
162
+ STDERR.puts ">>Commit message must refer to a jira ticket"
163
+ add_error_to_report(commit, commit_message, "no_jira")
164
+ return false
165
+ end
166
+
167
+ # we know we have to add comments for at least one ticket
168
+ # so build up the options with more info about the commit.
169
+ # the comment will be the same in each ticket
170
+
171
+ comment_text = get_comment_content(commit, commit_message)
172
+
173
+ success = false
174
+ jira_tickets.each do |ticket|
175
+ valid_ticket = check_for_valid_ticket(ticket)
176
+ if valid_ticket
177
+ add_comment(ticket, comment_text)
178
+ success = true
179
+ end
180
+ end
181
+
182
+ unless success
183
+ STDERR.puts ">>Commit message must refer to a valid jira ticket"
184
+ add_error_to_report(commit, commit_message, "invalid_jira")
185
+ end
186
+
187
+ return success # did we find any valid tickets?
188
+ end
189
+
190
+
191
+
192
+ def add_comment(ticket, comment_text)
193
+ STDERR.puts "ADDING COMMENT for ticket #{ticket}"
194
+ uri = build_uri(ticket, "comment")
195
+ data = {"body" => comment_text}
196
+
197
+ STDERR.puts comment_text
198
+
199
+ if !@options["issues"] || @options["issues"].include?(ticket) # can limit to single issue until get the text right.
200
+ resp = RestClient.post(uri, data.to_json, :content_type => :json, :accept=>:json)
201
+ # hash = JSON.parse(resp)
202
+ # do we need to check anything about the response to see if it went ok?
203
+ # it will throw an error if ticket not found or something.
204
+ end
205
+ end
206
+
207
+ def check_for_valid_ticket(ticket)
208
+ begin
209
+
210
+ uri = build_uri(ticket)
211
+ resp = RestClient.get uri
212
+ hash = JSON.parse(resp)
213
+
214
+ if @options["check_status"]
215
+ # Grab the Jira bug status, or fall back to allowing
216
+ # if the format is unexpected.
217
+
218
+ status = hash["fields"]["status"]["name"] rescue "open"
219
+
220
+ if status.downcase == "closed"
221
+ STDERR.puts "Issue #{ticket} is closed, not allowing."
222
+ return false
223
+ end
224
+ end
225
+ # The bug (probably) isn't closed (or we aren't checking),so we're valid!
226
+ return true
227
+ rescue SocketError
228
+ STDERR.puts "SocketError finding '#{@options["host"]}': #{$!.inspect}"
229
+ STDERR.puts "Is '#{@options["host"]}' the right Jira hostname? "
230
+ STDERR.puts "I'm allowing this in case you're offline, but make sure"
231
+ STDERR.puts "your hostname is right, please!"
232
+ return true
233
+ rescue RestClient::Exception
234
+ if $!.http_code == 401
235
+ STDERR.puts "You're not authorized on this server!"
236
+ STDERR.puts "Please set your username and password correctly."
237
+ return false
238
+ elsif $!.http_code == 404
239
+ # Nope, not a valid issue. Keep trying
240
+ elsif $!.http_code == 407
241
+ STDERR.puts "We don't support proxies to Jira yet!"
242
+ STDERR.puts "I'll give you the benefit of the doubt."
243
+ return true
244
+ elsif $!.http_code >= 500
245
+ STDERR.puts "Jira got a server error."
246
+ STDERR.puts "I'll give you the benefit of the doubt."
247
+ return true
248
+ else
249
+ STDERR.puts "Unexpected HTTP Error: #{$!.http_code}!"
250
+ return false
251
+ end
252
+
253
+ rescue
254
+ STDERR.puts "Unexpected exception: #{$!.inspect}!"
255
+ return false
256
+ end
257
+ false # if we get to this point it's not a valid ticket
258
+ end
259
+
260
+ def commit_list
261
+ # return the list of commits to display. We don't want to show them all
262
+ # (it looks scary when there's a lot)
263
+ # when there's only one, just return the commit
264
+ # when more than one return first_commit..last_commit
265
+ # use the shortened SHAH1 for readability
266
+ return "" if !self.commits || self.commits.empty?
267
+
268
+ if self.commits.size == 1
269
+ "#{self.commits.first[0..6]}"
270
+ else
271
+ "#{self.commits.last[0..6]}..#{self.commits.first[0..6]}"
272
+ end
273
+ end
274
+
275
+
276
+ def add_error_to_report(commit, msg, error_type = "no_jira")
277
+ # remember this error so we can report it later with others by this author
278
+ # store the string we'd like to print out about this commit (commit link and msg)
279
+ # to make it easier to print later
280
+ # (could store commit and message separately and process later if necessary)
281
+ # format:
282
+ # {"email1@test.com"" => {"no_jira" => ["www.github.com/commit/1234 invalid commit message",
283
+ # "www.github.com/commit/6789 also invalid"]
284
+ # "invalid_jira" => ["www.github.com/commit/1212 ABC-123 invalid commit message"]}
285
+ # "email2@test.com" => {...} }
286
+
287
+
288
+ author_email = Hook.shell!("git log #{commit} -1 --pretty='%aN <%aE>'").chomp rescue "no email"
289
+
290
+ errors_to_report[author_email] ||= {"no_jira" => [], "invalid_jira" => []} # in case first error for this author
291
+ errors_to_report[author_email][error_type] << "#{build_commit_uri(commit[0..7])}\n#{msg}"
292
+ end
293
+
294
+ def report_errors
295
+ # report any errors we have reported
296
+ require "pony" unless @options["no_send"] || @options["via"] == "no_send" # wait until we need it
297
+ # NOTE: Pony breaks on Windows so don't use this option in Windows.
298
+ errors_to_report.each do |email, details|
299
+ desc = build_message(details["no_jira"], details["invalid_jira"])
300
+ STDERR.puts "Warnings for commit from Jira Add Comment Check:\n--"
301
+ STDERR.puts "#{desc}\n--"
302
+
303
+ unless @options["no_send"] || @options["via"] == "no_send"
304
+ STDERR.puts "Sending warning email to #{email}"
305
+ ret = Pony.mail :to => email,
306
+ :from => @options["from"],
307
+ :subject => @options["subject"],
308
+ :body => desc,
309
+ :via => @options["via"],
310
+ :via_options => @options["via_options"]
311
+ end
312
+ end
313
+ end
314
+
315
+ # Build the email message.
316
+ # use the remote repo path for the name of the repo
317
+ # since this is always run as post_receive, there should always be a remote path.
318
+
319
+ def build_message(no_jira = [], invalid_jira= [])
320
+ description = @options["intro"] || ""
321
+ description.concat <<DESCRIPTION
322
+ This notice is to remind you that you need to include valid Jira ticket
323
+ numbers in all of your Git commits!
324
+
325
+ We encountered the following problems in your recent commits.
326
+
327
+ DESCRIPTION
328
+ if no_jira.size > 0
329
+ description.concat <<DESCRIPTION
330
+ Commits with no reference to any jira tickets:
331
+
332
+ #{no_jira.join("\n--\n ")}
333
+ -----
334
+ DESCRIPTION
335
+ end
336
+
337
+ if invalid_jira.size > 0
338
+ description.concat <<DESCRIPTION
339
+ Commits which reference invalid Jira ticket numbers
340
+ that don't exist or have already been closed:
341
+
342
+ #{invalid_jira.join("\n--\n ")}
343
+ -----
344
+ DESCRIPTION
345
+ end
346
+
347
+ description.concat @options["conclusion"] if @options["conclusion"]
348
+
349
+ description
350
+ end
351
+ end
352
+
353
+
354
+
@@ -0,0 +1,32 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ require "ruby_git_hooks"
4
+
5
+ # This hook checks that the commit message has one or more correctly-formatted
6
+ # Jira ticket references.
7
+
8
+ class JiraReferenceCheckHook < RubyGitHooks::Hook
9
+ Hook = RubyGitHooks::Hook
10
+
11
+ JIRA_TICKET_REGEXP = /(?<=\W|^)[A-Z]{2,10}-\d{1,6}(?=\W|$)/
12
+
13
+ def initialize(options = {})
14
+ # not using options now, but leave this here for backwards compatibility
15
+ end
16
+
17
+ def check
18
+ if !commit_message || commit_message.length == 0
19
+ STDERR.puts "Commit message is missing or empty!"
20
+ return false
21
+ end
22
+
23
+ jira_tickets = commit_message.scan(JIRA_TICKET_REGEXP).map(&:strip)
24
+ if jira_tickets.length == 0
25
+ STDERR.puts "Commit message must refer to a jira ticket"
26
+ return false
27
+ end
28
+
29
+ return true
30
+ end
31
+
32
+ end
@@ -0,0 +1,32 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ require "ruby_git_hooks"
4
+
5
+ # This hook checks the size of each individual file against a
6
+ # configurable maximum size. Once a huge file is in your git history
7
+ # it can't be fully removed without rewriting history, so you're
8
+ # usually better off preventing them in the first place.
9
+
10
+ class MaxFileSizeHook < RubyGitHooks::Hook
11
+ DEFAULT_MAX_FILE_SIZE = 10*1024*1024;
12
+ VERBOSE = false
13
+
14
+ def initialize(max_size = DEFAULT_MAX_FILE_SIZE)
15
+ @max_file_size = max_size
16
+ end
17
+
18
+ def check
19
+ STDERR.puts "Checking, max file size: #{@max_file_size}" if VERBOSE
20
+ okay = true
21
+ file_contents.each do |name, file|
22
+ STDERR.puts "File length: #{file.length}" if VERBOSE
23
+ if file.length > @max_file_size
24
+ okay = false
25
+ STDERR.puts "File #{name} exceeds maximum allowed size!"
26
+ end
27
+ end
28
+
29
+ okay
30
+ end
31
+ end
32
+
@@ -0,0 +1,33 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ require "ruby_git_hooks"
4
+
5
+ # This hook checks that commit message contains only ASCII characters.
6
+
7
+ class NonAsciiCharactersCheckHook < RubyGitHooks::Hook
8
+ Hook = RubyGitHooks::Hook
9
+
10
+ def initialize(options = {})
11
+ end
12
+
13
+ def check
14
+ if !commit_message || commit_message.length == 0
15
+ STDERR.puts "Commit message is missing or empty!"
16
+ return false
17
+ end
18
+
19
+ # Brute force approach. I didn't find any clever way to check for non-ascii
20
+ # using string encoder tricks
21
+ count = 0
22
+ valid_control_chars = [13, 10, 9]
23
+ commit_message.each_byte do |b|
24
+ if b > 127 || (b < 32 && !valid_control_chars.include?(b))
25
+ count = count + 1
26
+ end
27
+ end
28
+ if count > 0
29
+ STDERR.puts "Commit message has #{count} non-ASCII characters"
30
+ end
31
+ return count == 0 ? true : false
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ require "ruby_git_hooks"
4
+
5
+ # This hook looks for source files that include ruby-debug and fails
6
+ # if any do. It exists primarily for testing. It's functional, but
7
+ # not necessarily useful.
8
+
9
+ class RubyDebugHook < RubyGitHooks::Hook
10
+ def check
11
+ bad_files = []
12
+
13
+ file_diffs.each do |file, diff|
14
+ if diff.include? "require 'ruby-debug'"
15
+ bad_files << file
16
+ end
17
+ end
18
+
19
+ return true if bad_files.empty?
20
+
21
+ puts "You left requires of ruby-debug in the following files:\n"
22
+ puts bad_files.join("\n")
23
+
24
+ false
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ module RubyGitHooks
4
+ VERSION = "0.0.31"
5
+ end
@@ -0,0 +1,27 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ require "ruby_git_hooks"
4
+
5
+ # This hook adds a watermark to the end of commit message so that we
6
+ # know the hooks have been run. It should be run as the last
7
+ # commit-msg hook so it changes the message immediately before the
8
+ # commit is accepted and other hooks can't remove the watermark
9
+ # afterward.
10
+
11
+ class AddWatermarkCommitHook < RubyGitHooks::Hook
12
+
13
+ def initialize(mark = "\u00a0")
14
+ @watermark = mark
15
+ end
16
+
17
+ def check
18
+ if !commit_message_file
19
+ STDERR.puts "Warning: Watermark hook must be run as commit-msg only"
20
+ return true # don't actually cause commit to fail
21
+ end
22
+
23
+ File.open(commit_message_file, 'a') {|f| f.write(@watermark)}
24
+ return true
25
+ end
26
+
27
+ end