git-commit-notifier 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2008 Csoma Zoltan, Primalgrasp
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,54 @@
1
+ _h1. Git Commit Notifier
2
+
3
+ <pre>by Bodo Tasche (bodo 'at' wannawork 'dot' de), Csoma Zoltan (Primalgrasp) (zoltan 'at' primalgrasp 'dot' com)</pre>
4
+
5
+ Sends email commit messages splitting commits that were pushed in one
6
+ step. Email is delivered as text or HTML with changes refined per
7
+ word. Emails have a scanable subject containing the first sentence of
8
+ the commit as well as the author, project and branch name.
9
+
10
+ For example:
11
+ <pre>[rails][master] Fix Brasilia timezone. [#1180 state:resolved] </pre>
12
+
13
+ A reply-to header is added containing the author of the commit. This makes follow up really simple. If multiple commits are pushed at once, emails are numbered in chronological order:
14
+ <pre>
15
+ [rails][master][000] Added deprecated warning messages to Float#months and Float#years deprications.
16
+ [rails][master][001] Enhance testing for fractional days and weeks. Update changelog.
17
+ </pre>
18
+
19
+ Example email:
20
+
21
+ !http://img171.imageshack.us/img171/954/gitcommitnotifieremailpq3.png!
22
+
23
+ h1. Requirements
24
+
25
+ * Ruby
26
+ * RubyGems
27
+ * diff/lcs gem
28
+ * SMTP server or sendmail compatible mailer
29
+ * mocha, hpricot gems for testing
30
+
31
+ h1. Installing and Configuring
32
+
33
+ Before installing the Git Commit Notification script, make sure the following Git settings are correct:
34
+ * git config hooks.mailinglist (email address of the recipient, probably your mailing list address)
35
+ * git config hooks.emailprefix (application name, used in email subject)
36
+
37
+ See /usr/local/share/git_commit_notifier/config/config.yml for overriding these settings
38
+
39
+ Run the automated installation script:
40
+ <pre>sudo gem install git-commit-notifier</pre>
41
+
42
+ After you installed the gem, you need to configure your git repository. Add a File called "post-receive" to the __hooks__
43
+ directory and add this content:
44
+
45
+ <pre>
46
+ #!/bin/sh
47
+ git-commit-notifier path_to_config.yml
48
+ </pre>
49
+
50
+ And don't forget to make that file executable. An example for the config-file can be found in __/usr/local/share/git_commit_notifier/config/config.yml__ .
51
+
52
+ h1. License
53
+
54
+ MIT License, see the file LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "git-commit-notifier"
8
+ gem.summary = %Q{Sends git commit messages with diffs}
9
+ gem.description = %Q{This git commit notifier sends html mails with nice diffs for every changed file.}
10
+ gem.email = "bodo@wannawork.de"
11
+ gem.homepage = "http://github.com/bodo/git-commit-notifier"
12
+ gem.authors = ["Bodo Tasche"]
13
+ gem.add_dependency('diff-lcs')
14
+ gem.add_dependency('mocha')
15
+ gem.add_dependency('hpricot')
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:test) do |test|
25
+ test.libs << 'lib' << 'test'
26
+ test.pattern = 'test/**/test_*.rb'
27
+ test.verbose = true
28
+ end
29
+
30
+ begin
31
+ require 'rcov/rcovtask'
32
+ Rcov::RcovTask.new do |test|
33
+ test.libs << 'test'
34
+ test.pattern = 'test/**/test_*.rb'
35
+ test.verbose = true
36
+ end
37
+ rescue LoadError
38
+ task :rcov do
39
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
40
+ end
41
+ end
42
+
43
+ task :test => :check_dependencies
44
+
45
+ task :default => :test
46
+
47
+ require 'rake/rdoctask'
48
+ Rake::RDocTask.new do |rdoc|
49
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "git-commit-notifier #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # parameters: revision1, revision 2, branch
3
+ THIS_FILE = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
4
+ $:.unshift File.join(File.dirname(THIS_FILE), "../lib")
5
+ require "commit_hook"
6
+
7
+ if ARGV.length == 0
8
+ CommitHook.show_error("You have to add a path to the config file for git-commit-notifier")
9
+ elsif ARGV.length == 1
10
+ param = STDIN.gets.strip.split
11
+ CommitHook.run ARGV[0], param[0], param[1], param[2]
12
+ else
13
+ CommitHook.run ARGV[0], ARGV[1], ARGV[2]
14
+ end
@@ -0,0 +1,15 @@
1
+ delivery_method: sendmail # smtp or sendmail
2
+
3
+ smtp_server:
4
+ address: localhost
5
+ port: 25
6
+ domain: localhost
7
+ user_name:
8
+ password:
9
+ authentication:
10
+
11
+ sendmail_options:
12
+ location: /usr/sbin/sendmail
13
+ arguments:
14
+
15
+
@@ -0,0 +1,79 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{git-commit-notifier}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Bodo Tasche"]
12
+ s.date = %q{2010-03-24}
13
+ s.default_executable = %q{git-commit-notifier}
14
+ s.description = %q{This git commit notifier sends html mails with nice diffs for every changed file.}
15
+ s.email = %q{bodo@wannawork.de}
16
+ s.executables = ["git-commit-notifier"]
17
+ s.extra_rdoc_files = [
18
+ "LICENSE",
19
+ "README.textile"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".gitignore",
24
+ "LICENSE",
25
+ "README.textile",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "bin/git-commit-notifier",
29
+ "config/git-notifier-configl.yml.sample",
30
+ "git-commit-notifier.gemspec",
31
+ "lib/commit_hook.rb",
32
+ "lib/diff_to_html.rb",
33
+ "lib/emailer.rb",
34
+ "lib/git.rb",
35
+ "lib/result_processor.rb",
36
+ "template/email.html.erb",
37
+ "template/styles.css",
38
+ "test/fixtures/git_log",
39
+ "test/fixtures/git_show_51b986619d88f7ba98be7d271188785cbbb541a0",
40
+ "test/fixtures/git_show_a4629e707d80a5769f7a71ca6ed9471015e14dc9",
41
+ "test/fixtures/git_show_dce6ade4cdc2833b53bd600ef10f9bce83c7102d",
42
+ "test/fixtures/git_show_e28ad77bba0574241e6eb64dfd0c1291b221effe",
43
+ "test/test_helper.rb",
44
+ "test/unit/test_commit_hook.rb",
45
+ "test/unit/test_diff_to_html.rb",
46
+ "test/unit/test_result_processor.rb"
47
+ ]
48
+ s.homepage = %q{http://github.com/bodo/git-commit-notifier}
49
+ s.rdoc_options = ["--charset=UTF-8"]
50
+ s.require_paths = ["lib"]
51
+ s.rubygems_version = %q{1.3.6}
52
+ s.summary = %q{Sends git commit messages with diffs}
53
+ s.test_files = [
54
+ "test/test_helper.rb",
55
+ "test/unit/test_commit_hook.rb",
56
+ "test/unit/test_diff_to_html.rb",
57
+ "test/unit/test_result_processor.rb"
58
+ ]
59
+
60
+ if s.respond_to? :specification_version then
61
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
62
+ s.specification_version = 3
63
+
64
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
65
+ s.add_runtime_dependency(%q<diff-lcs>, [">= 0"])
66
+ s.add_runtime_dependency(%q<mocha>, [">= 0"])
67
+ s.add_runtime_dependency(%q<hpricot>, [">= 0"])
68
+ else
69
+ s.add_dependency(%q<diff-lcs>, [">= 0"])
70
+ s.add_dependency(%q<mocha>, [">= 0"])
71
+ s.add_dependency(%q<hpricot>, [">= 0"])
72
+ end
73
+ else
74
+ s.add_dependency(%q<diff-lcs>, [">= 0"])
75
+ s.add_dependency(%q<mocha>, [">= 0"])
76
+ s.add_dependency(%q<hpricot>, [">= 0"])
77
+ end
78
+ end
79
+
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'cgi'
3
+ require 'net/smtp'
4
+ require 'sha1'
5
+
6
+ require 'diff_to_html'
7
+ require 'emailer'
8
+ require 'git'
9
+
10
+ class CommitHook
11
+
12
+ def self.show_error(message)
13
+ puts "************** GIT NOTIFIER PROBLEM *******************"
14
+ puts "\n"
15
+ puts message
16
+ puts "\n"
17
+ puts "************** GIT NOTIFIER PROBLEM *******************"
18
+ end
19
+
20
+ def self.run(config, rev1, rev2, ref_name)
21
+ project_path = Dir.getwd
22
+ recipient = Git.mailing_list_address
23
+
24
+ if (recipient.nil? || recipient.length == 0)
25
+ CommitHook.show_error(
26
+ "Please add a recipient for the emails. Eg : \n" +
27
+ " git config hooks.mailinglist developer@example.com")
28
+ return
29
+ end
30
+
31
+ prefix = Git.repo_name
32
+ branch_name = (ref_name =~ /master$/i) ? "" : "/#{ref_name.split("/").last}"
33
+
34
+ @config = {}
35
+ @config = YAML::load_file(config) if File.exist?(config)
36
+
37
+ diff2html = DiffToHtml.new(Dir.pwd)
38
+ diff2html.diff_between_revisions rev1, rev2, prefix, ref_name
39
+ diff2html.result.reverse.each_with_index do |result, i|
40
+ nr = number(diff2html.result.size, i)
41
+ emailer = Emailer.new @config, project_path, recipient, result[:commit_info][:email], result[:commit_info][:author],
42
+ "[#{prefix}#{branch_name}]#{nr} #{result[:commit_info][:message]}", result[:text_content], result[:html_content], rev1, rev2, ref_name
43
+ emailer.send
44
+ end
45
+ end
46
+
47
+ def self.number(total_entries, i)
48
+ return '' if total_entries <= 1
49
+ digits = total_entries < 10 ? 1 : 3
50
+ '[' + sprintf("%0#{digits}d", i) + ']'
51
+ end
52
+
53
+ end
@@ -0,0 +1,321 @@
1
+ require 'rubygems'
2
+ require 'diff/lcs'
3
+ require File.dirname(__FILE__) + '/result_processor'
4
+
5
+ def escape_content(s)
6
+ CGI.escapeHTML(s).gsub(" ", "&nbsp;")
7
+ end
8
+
9
+ class DiffToHtml
10
+ attr_accessor :file_prefix
11
+ attr_reader :result
12
+
13
+ def initialize(config_dir = nil)
14
+ @previous_dir = config_dir
15
+ end
16
+
17
+ def range_info(range)
18
+ range.match(/^@@ \-(\d+),\d+ \+(\d+),\d+ @@/)
19
+ left_ln = Integer($1)
20
+ right_ln = Integer($2)
21
+ return left_ln, right_ln
22
+ end
23
+
24
+ def line_class(line)
25
+ if line[:op] == :removal
26
+ return " class=\"r\""
27
+ elsif line[:op] == :addition
28
+ return " class=\"a\""
29
+ else
30
+ return ''
31
+ end
32
+ end
33
+
34
+ def add_block_to_results(block, escape)
35
+ return if block.empty?
36
+ block.each do |line|
37
+ add_line_to_result(line, escape)
38
+ end
39
+ end
40
+
41
+ def add_line_to_result(line, escape)
42
+ klass = line_class(line)
43
+ content = escape ? escape_content(line[:content]) : line[:content]
44
+ padding = '&nbsp;' if klass != ''
45
+ @diff_result << "<tr#{klass}>\n<td class=\"ln\">#{line[:removed]}</td>\n<td class=\"ln\">#{line[:added]}</td>\n<td>#{padding}#{content}</td></tr>"
46
+ end
47
+
48
+ def extract_block_content(block)
49
+ block.collect { |b| b[:content]}.join("\n")
50
+ end
51
+
52
+ def lcs_diff(removals, additions)
53
+ # arrays always have at least 1 element
54
+ callback = DiffCallback.new
55
+
56
+ s1 = extract_block_content(removals)
57
+ s2 = extract_block_content(additions)
58
+
59
+ s1 = tokenize_string(s1)
60
+ s2 = tokenize_string(s2)
61
+
62
+ Diff::LCS.traverse_balanced(s1, s2, callback)
63
+
64
+ processor = ResultProcessor.new(callback.tags)
65
+
66
+ diff_for_removals, diff_for_additions = processor.results
67
+ result = []
68
+
69
+ ln_start = removals[0][:removed]
70
+ diff_for_removals.each_with_index do |line, i|
71
+ result << { :removed => ln_start + i, :added => nil, :op => :removal, :content => line}
72
+ end
73
+
74
+ ln_start = additions[0][:added]
75
+ diff_for_additions.each_with_index do |line, i|
76
+ result << { :removed => nil, :added => ln_start + i, :op => :addition, :content => line}
77
+ end
78
+
79
+ result
80
+ end
81
+
82
+ def tokenize_string(str)
83
+ # tokenize by non-alphanumerical characters
84
+ tokens = []
85
+ token = ''
86
+ str = str.split('')
87
+ str.each_with_index do |char, i|
88
+ alphanumeric = !char.match(/[a-zA-Z0-9]/).nil?
89
+ if !alphanumeric || str.size == i+1
90
+ token += char if alphanumeric
91
+ tokens << token unless token.empty?
92
+ tokens << char unless alphanumeric
93
+ token = ''
94
+ else
95
+ token += char
96
+ end
97
+ end
98
+ return tokens
99
+ end
100
+
101
+ def operation_description
102
+ binary = @binary ? 'binary ' : ''
103
+ if @file_removed
104
+ op = "Deleted"
105
+ elsif @file_added
106
+ op = "Added"
107
+ else
108
+ op = "Changed"
109
+ end
110
+ header = "#{op} #{binary}file #{@current_file_name}"
111
+ "<h2>#{header}</h2>\n"
112
+ end
113
+
114
+ def add_changes_to_result
115
+ return if @current_file_name.nil?
116
+ @diff_result << operation_description
117
+ @diff_result << '<table>'
118
+ unless @diff_lines.empty?
119
+ removals = []
120
+ additions = []
121
+ @diff_lines.each do |line|
122
+ if [:addition, :removal].include?(line[:op])
123
+ removals << line if line[:op] == :removal
124
+ additions << line if line[:op] == :addition
125
+ end
126
+ if line[:op] == :unchanged || line == @diff_lines.last # unchanged line or end of block, add prev lines to result
127
+ if removals.size > 0 && additions.size > 0 # block of removed and added lines - perform intelligent diff
128
+ add_block_to_results(lcs_diff(removals, additions), escape = false)
129
+ else # some lines removed or added - no need to perform intelligent diff
130
+ add_block_to_results(removals + additions, escape = true)
131
+ end
132
+ removals = []
133
+ additions = []
134
+ add_line_to_result(line, escape = true) if line[:op] == :unchanged
135
+ end
136
+ end
137
+ @diff_lines = []
138
+ @diff_result << '</table>'
139
+ end
140
+ # reset values
141
+ @right_ln = nil
142
+ @left_ln = nil
143
+ @file_added = false
144
+ @file_removed = false
145
+ @binary = false
146
+ end
147
+
148
+ def diff_for_revision(content)
149
+ @left_ln = @right_ln = nil
150
+
151
+ @diff_result = []
152
+ @diff_lines = []
153
+ @removed_files = []
154
+ @current_file_name = nil
155
+
156
+ content.split("\n").each do |line|
157
+ if line =~ /^diff\s\-\-git/
158
+ line.match(/diff --git a\/(.*)\sb\//)
159
+ file_name = $1
160
+ add_changes_to_result
161
+ @current_file_name = file_name
162
+ end
163
+
164
+ op = line[0,1]
165
+ @left_ln.nil? || op == '@' ? process_info_line(line, op) : process_code_line(line, op)
166
+ end
167
+ add_changes_to_result
168
+ @diff_result.join("\n")
169
+ end
170
+
171
+ def process_code_line(line, op)
172
+ if op == '-'
173
+ @diff_lines << { :removed => @left_ln, :added => nil, :op => :removal, :content => line[1..-1] }
174
+ @left_ln += 1
175
+ elsif op == '+'
176
+ @diff_lines << { :added => @right_ln, :removed => nil, :op => :addition, :content => line[1..-1] }
177
+ @right_ln += 1
178
+ else @right_ln
179
+ @diff_lines << { :added => @right_ln, :removed => @left_ln, :op => :unchanged, :content => line }
180
+ @right_ln += 1
181
+ @left_ln += 1
182
+ end
183
+ end
184
+
185
+ def process_info_line(line, op)
186
+ if line =~/^deleted\sfile\s/
187
+ @file_removed = true
188
+ elsif line =~ /^\-\-\-\s/ && line =~ /\/dev\/null/
189
+ @file_added = true
190
+ elsif line =~ /^\+\+\+\s/ && line =~ /\/dev\/null/
191
+ @file_removed = true
192
+ elsif line =~ /^Binary files \/dev\/null/ # Binary files /dev/null and ... differ (addition)
193
+ @binary = true
194
+ @file_added = true
195
+ elsif line =~ /\/dev\/null differ/ # Binary files ... and /dev/null differ (removal)
196
+ @binary = true
197
+ @file_removed = true
198
+ elsif op == '@'
199
+ @left_ln, @right_ln = range_info(line)
200
+ end
201
+ end
202
+
203
+ def extract_diff_from_git_show_output(content)
204
+ diff = []
205
+ diff_found = false
206
+ content.split("\n").each do |line|
207
+ diff_found = true if line =~ /^diff \-\-git/
208
+ next unless diff_found
209
+ diff << line
210
+ end
211
+ diff.join("\n")
212
+ end
213
+
214
+ def extract_commit_info_from_git_show_output(content)
215
+ result = { :message => [], :commit => '', :author => '', :date => '', :email => '' }
216
+ content.split("\n").each do |line|
217
+ if line =~ /^diff/ # end of commit info, return results
218
+ return result
219
+ elsif line =~ /^commit/
220
+ result[:commit] = line[7..-1]
221
+ elsif line =~ /^Author/
222
+ result[:author], result[:email] = author_name_and_email(line[8..-1])
223
+ elsif line =~ /^Date/
224
+ result[:date] = line[8..-1]
225
+ else
226
+ clean_line = line.strip
227
+ result[:message] << clean_line unless clean_line.empty?
228
+ end
229
+ end
230
+ result
231
+ end
232
+
233
+ def message_array_as_html(message)
234
+ message.collect { |m| CGI.escapeHTML(m)}.join("<br />")
235
+ end
236
+
237
+ def author_name_and_email(info)
238
+ # input string format: "autor name <author@email.net>"
239
+ result = info.scan(/(.*)\s<(.*)>/)[0]
240
+ return result if result.is_a?(Array) && result.size == 2 # normal operation
241
+ # incomplete author info - return it as author name
242
+ return [info, ''] if result.nil?
243
+ end
244
+
245
+ def first_sentence(message_array)
246
+ msg = message_array.first.to_s.strip
247
+ return message_array.first if msg.empty? || msg =~ /^Merge\:/
248
+ msg
249
+ end
250
+
251
+ def diff_between_revisions(rev1, rev2, repo, branch)
252
+ @result = []
253
+ if rev1 == rev2
254
+ commits = [rev1]
255
+ elsif rev1 =~ /^0+$/
256
+ # creating a new remote branch
257
+ commits = Git.branch_commits(branch)
258
+ elsif rev2 =~ /^0+$/
259
+ # deleting an existing remote branch
260
+ commits = []
261
+ else
262
+ log = Git.log(rev1, rev2)
263
+ commits = log.scan(/^commit\s([a-f0-9]+)/).map{|match| match[0]}
264
+ end
265
+
266
+ if defined?(Test::Unit)
267
+ previous_list = []
268
+ else
269
+ previous_file = (!@previous_dir.nil? && File.exists?(@previous_dir)) ? File.join(@previous_dir, "previously.txt") : "/tmp/previously.txt"
270
+ previous_list = (File.read(previous_file).to_a.map {|sha| sha.chomp!} if File.exist?(previous_file)) || []
271
+ end
272
+
273
+ commits.reject!{|c| c.find{|sha| previous_list.include?(sha)} }
274
+ current_list = (previous_list + commits.flatten).last(1000)
275
+ File.open(previous_file, "w"){|f| f << current_list.join("\n") } unless current_list.empty? || defined?(Test::Unit)
276
+
277
+ commits.each_with_index do |commit, i|
278
+ raw_diff = Git.show(commit)
279
+ raise "git show output is empty" if raw_diff.empty?
280
+ @last_raw = raw_diff
281
+
282
+ commit_info = extract_commit_info_from_git_show_output(raw_diff)
283
+
284
+ title = "<div class=\"title\">"
285
+ title += "<strong>Message:</strong> #{message_array_as_html commit_info[:message]}<br />\n"
286
+ title += "<strong>Commit</strong> #{commit_info[:commit]}<br />\n"
287
+ title += "<strong>Branch:</strong> #{branch}\n<br />" unless branch =~ /\/head/
288
+ title += "<strong>Date:</strong> #{CGI.escapeHTML commit_info[:date]}\n<br />"
289
+ title += "<strong>Author:</strong> #{CGI.escapeHTML(commit_info[:author])} &lt;#{commit_info[:email]}&gt;\n</div>"
290
+
291
+ text = "#{raw_diff}\n\n\n"
292
+
293
+ html = title
294
+ html += diff_for_revision(extract_diff_from_git_show_output(raw_diff))
295
+ html += "<br /><br />"
296
+ commit_info[:message] = first_sentence(commit_info[:message])
297
+ @result << {:commit_info => commit_info, :html_content => html, :text_content => text }
298
+ end
299
+ end
300
+ end
301
+
302
+ class DiffCallback
303
+ attr_reader :tags
304
+
305
+ def initialize
306
+ @tags = []
307
+ end
308
+
309
+ def match(event)
310
+ @tags << { :action => :match, :token => event.old_element }
311
+ end
312
+
313
+ def discard_b(event)
314
+ @tags << { :action => :discard_b, :token => event.new_element }
315
+ end
316
+
317
+ def discard_a(event)
318
+ @tags << { :action => :discard_a, :token => event.old_element }
319
+ end
320
+
321
+ end