ruby_git_hooks 0.0.31

Sign up to get free protection for your applications and to get access to all the features.
@@ -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