git-commit-notifier 0.8.1 → 0.9.0

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