git-commit-notifier 0.8.1 → 0.9.0

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.
data/.gitignore DELETED
@@ -1,21 +0,0 @@
1
- ## MAC OS
2
- .DS_Store
3
-
4
- ## TEXTMATE
5
- *.tmproj
6
- tmtags
7
-
8
- ## EMACS
9
- *~
10
- \#*
11
- .\#*
12
-
13
- ## VIM
14
- *.swp
15
-
16
- ## PROJECT::GENERAL
17
- coverage
18
- rdoc
19
- pkg
20
-
21
- ## PROJECT::SPECIFIC
data/lib/commit_hook.rb DELETED
@@ -1,129 +0,0 @@
1
- require 'rubygems'
2
- require 'yaml'
3
- require 'cgi'
4
- require 'net/smtp'
5
- require 'digest/sha1'
6
-
7
- require 'logger'
8
- require 'diff_to_html'
9
- require 'emailer'
10
- require 'git'
11
-
12
- class CommitHook
13
-
14
- class << self
15
- attr_reader :config
16
-
17
- def show_error(message)
18
- $stderr.puts "************** GIT NOTIFIER PROBLEM *******************"
19
- $stderr.puts "\n"
20
- $stderr.puts message
21
- $stderr.puts "\n"
22
- $stderr.puts "************** GIT NOTIFIER PROBLEM *******************"
23
- end
24
-
25
- def info(message)
26
- $stdout.puts message
27
- $stdout.flush
28
- end
29
-
30
- def logger
31
- @logger ||= Logger.new(config)
32
- end
33
-
34
- def run(config_name, rev1, rev2, ref_name)
35
- @config = File.exists?(config_name) ? YAML::load_file(config_name) : {}
36
-
37
- project_path = Dir.getwd
38
- recipient = config["mailinglist"] || Git.mailing_list_address
39
-
40
- if recipient.nil? || recipient.length == 0
41
- CommitHook.show_error(
42
- "Please add a recipient for the emails. Eg : \n" +
43
- " git config hooks.mailinglist developer@example.com"
44
- )
45
- return
46
- end
47
-
48
- logger.debug('----')
49
- logger.debug("pwd: #{Dir.pwd}")
50
- logger.debug("ref_name: #{ref_name}")
51
- logger.debug("rev1: #{rev1}")
52
- logger.debug("rev2: #{rev2}")
53
-
54
-
55
- info("Sending mail...")
56
-
57
- prefix = @config["emailprefix"] || Git.repo_name
58
- branch_name = (ref_name =~ /master$/i) ? "" : "/#{ref_name.split("/").last}"
59
-
60
- logger.debug("prefix: #{prefix}")
61
- logger.debug("branch: #{branch_name}")
62
-
63
- diff2html = DiffToHtml.new(Dir.pwd, config)
64
- diff2html.diff_between_revisions(rev1, rev2, prefix, ref_name)
65
-
66
- diffresult = diff2html.result
67
-
68
- if config["ignore_merge"]
69
- diffresult = diffresult.reject do |result|
70
- !result[:commit_info][:merge].nil?
71
- end
72
- end
73
-
74
- if config["group_email_by_push"]
75
- text, html = [], []
76
- diffresult.each_with_index do |result, i|
77
- text << result[:text_content]
78
- html << result[:html_content]
79
- end
80
- result = diffresult.first
81
- return if result.nil? || !result[:commit_info]
82
-
83
- emailer = Emailer.new(config,
84
- :project_path => project_path,
85
- :recipient => recipient,
86
- :from_address => config["from"] || result[:commit_info][:email],
87
- :from_alias => result[:commit_info][:author],
88
- :subject => "[#{prefix}#{branch_name}] #{diffresult.size > 1 ? "#{diffresult.size} commits: " : ''}#{result[:commit_info][:message]}",
89
- :text_message => text.join("------------------------------------------\n\n"),
90
- :html_message => html.join("<hr /><br />"),
91
- :old_rev => rev1,
92
- :new_rev => rev2,
93
- :ref_name => ref_name
94
- )
95
- emailer.send
96
- else
97
- diffresult.reverse.each_with_index do |result, i|
98
- next unless result[:commit_info]
99
- nr = number(diffresult.size, i)
100
-
101
- emailer = Emailer.new(config,
102
- :project_path => project_path,
103
- :recipient => recipient,
104
- :from_address => config["from"] || result[:commit_info][:email],
105
- :from_alias => result[:commit_info][:author],
106
- :subject => "[#{prefix}#{branch_name}]#{nr} #{result[:commit_info][:message]}",
107
- :text_message => result[:text_content],
108
- :html_message => result[:html_content],
109
- :old_rev => rev1,
110
- :new_rev => rev2,
111
- :ref_name => ref_name
112
- )
113
- emailer.send
114
- end
115
- end
116
- end
117
-
118
- def number(total_entries, i)
119
- return '' if total_entries <= 1
120
- digits = total_entries < 10 ? 1 : 3
121
- '[' + sprintf("%0#{digits}d", i + 1) + ']'
122
- end
123
- end
124
- end
125
-
126
- __END__
127
-
128
- vim: tabstop=2:expandtab:shiftwidth=2
129
-
data/lib/diff_to_html.rb DELETED
@@ -1,449 +0,0 @@
1
- require 'rubygems'
2
- require 'diff/lcs'
3
- require 'digest/sha1'
4
-
5
- require File.dirname(__FILE__) + '/result_processor'
6
-
7
- def escape_content(s)
8
- CGI.escapeHTML(s).gsub(" ", "&nbsp;")
9
- end
10
-
11
- class DiffToHtml
12
- INTEGRATION_MAP = {
13
- :mediawiki => { :search_for => /\[\[([^\[\]]+)\]\]/, :replace_with => '#{url}/\1' },
14
- :redmine => { :search_for => /\b(?:refs|fixes)\s*\#(\d+)/i, :replace_with => '#{url}/issues/show/\1' },
15
- :bugzilla => { :search_for => /\bBUG\s*(\d+)/i, :replace_with => '#{url}/show_bug.cgi?id=\1' },
16
- :fogbugz => { :search_for => /\bbugzid:\s*(\d+)/i, :replace_with => '#{url}\1' }
17
- }.freeze
18
- MAX_COMMITS_PER_ACTION = 10000
19
- HANDLED_COMMITS_FILE = 'previously.txt'.freeze
20
- NEW_HANDLED_COMMITS_FILE = 'previously_new.txt'.freeze
21
-
22
- attr_accessor :file_prefix, :current_file_name
23
- attr_reader :result, :branch
24
-
25
- def initialize(previous_dir = nil, config = nil)
26
- @previous_dir = previous_dir
27
- @config = config || {}
28
- @lines_added = 0
29
- @file_added = false
30
- @file_removed = false
31
- @binary = false
32
- end
33
-
34
- def range_info(range)
35
- matches = range.match(/^@@ \-(\S+) \+(\S+)/)
36
- return matches[1..2].map { |m| m.split(',')[0].to_i }
37
- end
38
-
39
- def line_class(line)
40
- if line[:op] == :removal
41
- return " class=\"r\""
42
- elsif line[:op] == :addition
43
- return " class=\"a\""
44
- else
45
- return ''
46
- end
47
- end
48
-
49
- def add_block_to_results(block, escape)
50
- return if block.empty?
51
- block.each do |line|
52
- add_line_to_result(line, escape)
53
- end
54
- end
55
-
56
- def lines_per_diff
57
- @config['lines_per_diff']
58
- end
59
-
60
- def add_separator
61
- return if lines_per_diff && @lines_added >= lines_per_diff
62
- @diff_result << '<tr class="sep"><td class="sep" colspan="3" title="Unchanged content skipped between diff. blocks">&hellip;</td></tr>'
63
- end
64
-
65
- def add_line_to_result(line, escape)
66
- @lines_added += 1
67
- if lines_per_diff
68
- if @lines_added == lines_per_diff
69
- @diff_result << '<tr><td colspan="3">Diff too large and stripped&hellip;</td></tr>'
70
- end
71
- if @lines_added >= lines_per_diff
72
- return
73
- end
74
- end
75
- klass = line_class(line)
76
- content = escape ? escape_content(line[:content]) : line[:content]
77
- padding = '&nbsp;' if klass != ''
78
- @diff_result << "<tr#{klass}>\n<td class=\"ln\">#{line[:removed]}</td>\n<td class=\"ln\">#{line[:added]}</td>\n<td>#{padding}#{content}</td></tr>"
79
- end
80
-
81
- def extract_block_content(block)
82
- block.collect { |b| b[:content] }.join("\n")
83
- end
84
-
85
- def lcs_diff(removals, additions)
86
- # arrays always have at least 1 element
87
- callback = DiffCallback.new
88
-
89
- s1 = extract_block_content(removals)
90
- s2 = extract_block_content(additions)
91
-
92
- s1 = tokenize_string(s1)
93
- s2 = tokenize_string(s2)
94
-
95
- Diff::LCS.traverse_balanced(s1, s2, callback)
96
-
97
- processor = ResultProcessor.new(callback.tags)
98
-
99
- diff_for_removals, diff_for_additions = processor.results
100
- result = []
101
-
102
- ln_start = removals[0][:removed]
103
- diff_for_removals.each_with_index do |line, i|
104
- result << { :removed => ln_start + i, :added => nil, :op => :removal, :content => line}
105
- end
106
-
107
- ln_start = additions[0][:added]
108
- diff_for_additions.each_with_index do |line, i|
109
- result << { :removed => nil, :added => ln_start + i, :op => :addition, :content => line}
110
- end
111
-
112
- result
113
- end
114
-
115
- def tokenize_string(str)
116
- # tokenize by non-word characters
117
- tokens = []
118
- token = ''
119
- str.scan(/./mu) do |ch|
120
- if ch =~ /[^\W_]/u
121
- token += ch
122
- else
123
- unless token.empty?
124
- tokens << token
125
- token = ''
126
- end
127
- tokens << ch
128
- end
129
- end
130
- tokens << token unless token.empty?
131
- tokens
132
- end
133
-
134
- def operation_description
135
- binary = @binary ? 'binary ' : ''
136
- if @file_removed
137
- op = "Deleted"
138
- elsif @file_added
139
- op = "Added"
140
- else
141
- op = "Changed"
142
- end
143
-
144
- file_name = @current_file_name
145
-
146
- if (@config["link_files"] && @config["link_files"] == "gitweb" && @config["gitweb"])
147
- file_name = "<a href='#{@config['gitweb']['path']}?p=#{@config['gitweb']['project']};f=#{file_name};hb=HEAD'>#{file_name}</a>"
148
- elsif (@config["link_files"] && @config["link_files"] == "gitorious" && @config["gitorious"])
149
- file_name = "<a href='#{@config['gitorious']['path']}/#{@config['gitorious']['project']}/#{@config['gitorious']['repository']}/blobs/#{branch}/#{file_name}'>#{file_name}</a>"
150
- elsif (@config["link_files"] && @config["link_files"] == "cgit" && @config["cgit"])
151
- file_name = "<a href='#{@config['cgit']['path']}/#{@config['cgit']['project']}/tree/#{file_name}'>#{file_name}</a>"
152
- end
153
-
154
- header = "#{op} #{binary}file #{file_name}"
155
- "<h2>#{header}</h2>\n"
156
- end
157
-
158
- def lines_are_sequential?(first, second)
159
- result = false
160
- [:added, :removed].each do |side|
161
- if !first[side].nil? && !second[side].nil?
162
- result = true if first[side] == (second[side] - 1)
163
- end
164
- end
165
- result
166
- end
167
-
168
- def add_changes_to_result
169
- return if @current_file_name.nil?
170
- @diff_result << operation_description
171
- @diff_result << '<table>'
172
- unless @diff_lines.empty?
173
- removals = []
174
- additions = []
175
- @diff_lines.each_with_index do |line, index|
176
- removals << line if line[:op] == :removal
177
- additions << line if line[:op] == :addition
178
- if line[:op] == :unchanged || index == @diff_lines.size - 1 # unchanged line or end of block, add prev lines to result
179
- if removals.size > 0 && additions.size > 0 # block of removed and added lines - perform intelligent diff
180
- add_block_to_results(lcs_diff(removals, additions), escape = false)
181
- else # some lines removed or added - no need to perform intelligent diff
182
- add_block_to_results(removals + additions, escape = true)
183
- end
184
- removals = []
185
- additions = []
186
- if index > 0 && index != @diff_lines.size - 1
187
- prev_line = @diff_lines[index - 1]
188
- add_separator unless lines_are_sequential?(prev_line, line)
189
- end
190
- add_line_to_result(line, escape = true) if line[:op] == :unchanged
191
- end
192
- end
193
- @diff_lines = []
194
- @diff_result << '</table>'
195
- end
196
- # reset values
197
- @right_ln = nil
198
- @left_ln = nil
199
- @file_added = false
200
- @file_removed = false
201
- @binary = false
202
- end
203
-
204
- def diff_for_revision(content)
205
- @left_ln = @right_ln = nil
206
-
207
- @diff_result = []
208
- @diff_lines = []
209
- @removed_files = []
210
- @current_file_name = nil
211
-
212
- content.split("\n").each do |line|
213
- if line =~ /^diff\s\-\-git\sa\/(.*)\sb\//
214
- file_name = $1
215
- add_changes_to_result
216
- @current_file_name = file_name
217
- end
218
-
219
- op = line[0,1]
220
- @left_ln.nil? || op == '@' ? process_info_line(line, op) : process_code_line(line, op)
221
- end
222
- add_changes_to_result
223
- @diff_result.join("\n")
224
- end
225
-
226
- def process_code_line(line, op)
227
- if op == '-'
228
- @diff_lines << { :removed => @left_ln, :added => nil, :op => :removal, :content => line[1..-1] }
229
- @left_ln += 1
230
- elsif op == '+'
231
- @diff_lines << { :added => @right_ln, :removed => nil, :op => :addition, :content => line[1..-1] }
232
- @right_ln += 1
233
- else
234
- @diff_lines << { :added => @right_ln, :removed => @left_ln, :op => :unchanged, :content => line }
235
- @right_ln += 1
236
- @left_ln += 1
237
- end
238
- end
239
-
240
- def process_info_line(line, op)
241
- if line =~/^deleted\sfile\s/
242
- @file_removed = true
243
- elsif line =~ /^\-\-\-\s/ && line =~ /\/dev\/null/
244
- @file_added = true
245
- elsif line =~ /^\+\+\+\s/ && line =~ /\/dev\/null/
246
- @file_removed = true
247
- elsif line =~ /^Binary files \/dev\/null/ # Binary files /dev/null and ... differ (addition)
248
- @binary = true
249
- @file_added = true
250
- elsif line =~ /\/dev\/null differ/ # Binary files ... and /dev/null differ (removal)
251
- @binary = true
252
- @file_removed = true
253
- elsif op == '@'
254
- @left_ln, @right_ln = range_info(line)
255
- end
256
- end
257
-
258
- def extract_diff_from_git_show_output(content)
259
- diff = []
260
- diff_found = false
261
- content.split("\n").each do |line|
262
- diff_found = true if line =~ /^diff\s\-\-git/
263
- next unless diff_found
264
- diff << line
265
- end
266
- diff.join("\n")
267
- end
268
-
269
- def extract_commit_info_from_git_show_output(content)
270
- result = { :message => [], :commit => '', :author => '', :date => '', :email => '' }
271
- content.split("\n").each do |line|
272
- if line =~ /^diff/ # end of commit info, return results
273
- return result
274
- elsif line =~ /^commit/
275
- result[:commit] = line[7..-1]
276
- elsif line =~ /^Author/
277
- result[:author], result[:email] = author_name_and_email(line[8..-1])
278
- elsif line =~ /^Date/
279
- result[:date] = line[8..-1]
280
- elsif line =~ /^Merge/
281
- result[:merge] = line[8..-1]
282
- else
283
- clean_line = line.strip
284
- result[:message] << clean_line unless clean_line.empty?
285
- end
286
- end
287
- result
288
- end
289
-
290
- def message_array_as_html(message)
291
- message_map(message.collect { |m| CGI.escapeHTML(m) }.join('<br />'))
292
- end
293
-
294
- def author_name_and_email(info)
295
- # input string format: "autor name <author@email.net>"
296
- return [$1, $2] if info =~ /^([^\<]+)\s+\<\s*(.*)\s*\>\s*$/ # normal operation
297
- # incomplete author info - return it as author name
298
- [info, '']
299
- end
300
-
301
- def first_sentence(message_array)
302
- msg = message_array.first.to_s.strip
303
- return message_array.first if msg.empty? || msg =~ /^Merge\:/
304
- msg
305
- end
306
-
307
- def unique_commits_per_branch?
308
- !!@config['unique_commits_per_branch']
309
- end
310
-
311
- def get_previous_commits(previous_file)
312
- previous_list = []
313
- if File.exists?(previous_file)
314
- lines = IO.read(previous_file)
315
- lines = lines.lines if lines.respond_to?(:lines) # Ruby 1.9 tweak
316
- previous_list = lines.to_a.map { |s| s.chomp }.compact.uniq
317
- lines = nil
318
- end
319
- previous_list
320
- end
321
-
322
- def check_handled_commits(commits, branch)
323
- previous_dir = (!@previous_dir.nil? && File.exists?(@previous_dir)) ? @previous_dir : '/tmp'
324
- prefix = unique_commits_per_branch? ? "#{Digest::SHA1.hexdigest(branch)}." : ''
325
- previous_name = "#{prefix}#{HANDLED_COMMITS_FILE}"
326
- new_name = "#{prefix}#{NEW_HANDLED_COMMITS_FILE}"
327
- previous_file = File.join(previous_dir, previous_name)
328
- new_file = File.join(previous_dir, new_name)
329
-
330
- previous_list = get_previous_commits(previous_file)
331
-
332
- commits.reject! {|c| c.find { |sha| previous_list.include?(sha) } }
333
-
334
- # if commit list empty there is no need to override list of handled commits
335
- flatten_commits = commits.flatten
336
- unless flatten_commits.empty?
337
- current_list = (previous_list + flatten_commits).last(MAX_COMMITS_PER_ACTION)
338
-
339
- # use new file, unlink and rename to make it more atomic
340
- File.open(new_file, 'w') { |f| f << current_list.join("\n") }
341
- File.unlink(previous_file) if File.exists?(previous_file)
342
- File.rename(new_file, previous_file)
343
- end
344
- commits
345
- end
346
-
347
- def diff_between_revisions(rev1, rev2, repo, branch)
348
- @branch = branch
349
- @result = []
350
- if rev1 == rev2
351
- commits = [rev1]
352
- elsif rev1 =~ /^0+$/
353
- # creating a new remote branch
354
- commits = Git.branch_commits(branch)
355
- elsif rev2 =~ /^0+$/
356
- # deleting an existing remote branch
357
- commits = []
358
- else
359
- log = Git.log(rev1, rev2)
360
- commits = log.scan(/^commit\s([a-f0-9]+)/).map { |a| a.first }
361
- end
362
-
363
- commits = check_handled_commits(commits, branch)
364
-
365
- commits.each_with_index do |commit, i|
366
-
367
- raw_diff = Git.show(commit)
368
- raise "git show output is empty" if raw_diff.empty?
369
-
370
- commit_info = extract_commit_info_from_git_show_output(raw_diff)
371
-
372
- title = "<div class=\"title\">"
373
- title += "<strong>Message:</strong> #{message_array_as_html commit_info[:message]}<br />\n"
374
- title += "<strong>Commit:</strong> "
375
-
376
- if (@config["link_files"] && @config["link_files"] == "gitweb" && @config["gitweb"])
377
- title += "<a href='#{@config['gitweb']['path']}?p=#{@config['gitweb']['project']};a=commitdiff;h=#{commit_info[:commit]}'>#{commit_info[:commit]}</a>"
378
- elsif (@config["link_files"] && @config["link_files"] == "gitorious" && @config["gitorious"])
379
- title += "<a href='#{@config['gitorious']['path']}/#{@config['gitorious']['project']}/#{@config['gitorious']['repository']}/commit/#{commit_info[:commit]}'>#{commit_info[:commit]}</a>"
380
- elsif (@config["link_files"] && @config["link_files"] == "trac" && @config["trac"])
381
- title += "<a href='#{@config['trac']['path']}/#{commit_info[:commit]}'>#{commit_info[:commit]}</a>"
382
- elsif (@config["link_files"] && @config["link_files"] == "cgit" && @config["cgit"])
383
- title += "<a href='#{@config['cgit']['path']}/#{@config['cgit']['project']}/commit/?id=#{commit_info[:commit]}'>#{commit_info[:commit]}</a>"
384
- else
385
- title += " #{commit_info[:commit]}"
386
- end
387
-
388
- title += "<br />\n"
389
-
390
- title += "<strong>Branch:</strong> #{branch}\n<br />" unless branch =~ /\/head/
391
- title += "<strong>Date:</strong> #{CGI.escapeHTML commit_info[:date]}\n<br />"
392
- title += "<strong>Author:</strong> #{CGI.escapeHTML(commit_info[:author])} &lt;#{commit_info[:email]}&gt;\n</div>"
393
-
394
- text = "#{raw_diff}\n\n\n"
395
-
396
- html = title
397
- html += diff_for_revision(extract_diff_from_git_show_output(raw_diff))
398
- html += "<br /><br />"
399
- commit_info[:message] = first_sentence(commit_info[:message])
400
- @result << {:commit_info => commit_info, :html_content => html, :text_content => text }
401
- end
402
- end
403
-
404
- def message_replace!(message, search_for, replace_with)
405
- full_replace_with = "<a href=\"#{replace_with}\">\\0</a>"
406
- message.gsub!(Regexp.new(search_for), full_replace_with)
407
- end
408
-
409
- def message_map(message)
410
- if @config['message_integration'].respond_to?(:each_pair)
411
- @config['message_integration'].each_pair do |pm, url|
412
- pm_def = DiffToHtml::INTEGRATION_MAP[pm.to_sym] or next
413
- replace_with = pm_def[:replace_with].gsub('#{url}', url)
414
- message_replace!(message, pm_def[:search_for], replace_with)
415
- end
416
- end
417
- if @config['message_map'].respond_to?(:each_pair)
418
- @config['message_map'].each_pair do |search_for, replace_with|
419
- message_replace!(message, Regexp.new(search_for), replace_with)
420
- end
421
- end
422
- message
423
- end
424
- end
425
-
426
- class DiffCallback
427
- attr_reader :tags
428
-
429
- def initialize
430
- @tags = []
431
- end
432
-
433
- def match(event)
434
- @tags << { :action => :match, :token => event.old_element }
435
- end
436
-
437
- def discard_b(event)
438
- @tags << { :action => :discard_b, :token => event.new_element }
439
- end
440
-
441
- def discard_a(event)
442
- @tags << { :action => :discard_a, :token => event.old_element }
443
- end
444
-
445
- end
446
-
447
- __END__
448
-
449
- vim: tabstop=2:expandtab:shiftwidth=2