require 'rubygems' require 'diff/lcs' require File.dirname(__FILE__) + '/result_processor' def escape_content(s) CGI.escapeHTML(s).gsub(" ", " ") end class DiffToHtml INTEGRATION_MAP = { :mediawiki => { :search_for => /\[\[([^\[\]]+)\]\]/, :replace_with => '#{url}/\1' }, :redmine => { :search_for => /\b(?:refs|fixes)\s*\#(\d+)/i, :replace_with => '#{url}/issues/show/\1' }, :bugzilla => { :search_for => /\bBUG\s*(\d+)/i, :replace_with => '#{url}/show_bug.cgi?id=\1' } }.freeze attr_accessor :file_prefix, :current_file_name attr_reader :result def initialize(previous_dir = nil, config = nil) @previous_dir = previous_dir @config = config || {} @lines_added = 0 end def range_info(range) matches = range.match(/^@@ \-(\S+) \+(\S+)/) return matches[1..2].map { |m| m.split(',')[0].to_i } end def line_class(line) if line[:op] == :removal return " class=\"r\"" elsif line[:op] == :addition return " class=\"a\"" else return '' end end def add_block_to_results(block, escape) return if block.empty? block.each do |line| add_line_to_result(line, escape) end end def add_line_to_result(line, escape) @lines_added += 1 lines_per_diff = @config['lines_per_diff'] if lines_per_diff if @lines_added == lines_per_diff @diff_result << 'Diff too large and stripped…' end if @lines_added >= lines_per_diff return end end klass = line_class(line) content = escape ? escape_content(line[:content]) : line[:content] padding = ' ' if klass != '' @diff_result << "\n#{line[:removed]}\n#{line[:added]}\n#{padding}#{content}" end def extract_block_content(block) block.collect { |b| b[:content]}.join("\n") end def lcs_diff(removals, additions) # arrays always have at least 1 element callback = DiffCallback.new s1 = extract_block_content(removals) s2 = extract_block_content(additions) s1 = tokenize_string(s1) s2 = tokenize_string(s2) Diff::LCS.traverse_balanced(s1, s2, callback) processor = ResultProcessor.new(callback.tags) diff_for_removals, diff_for_additions = processor.results result = [] ln_start = removals[0][:removed] diff_for_removals.each_with_index do |line, i| result << { :removed => ln_start + i, :added => nil, :op => :removal, :content => line} end ln_start = additions[0][:added] diff_for_additions.each_with_index do |line, i| result << { :removed => nil, :added => ln_start + i, :op => :addition, :content => line} end result end def tokenize_string(str) # tokenize by non-alphanumerical characters tokens = [] token = '' str = str.split('') str.each_with_index do |char, i| alphanumeric = !char.match(/[a-zA-Z0-9]/).nil? if !alphanumeric || str.size == i+1 token += char if alphanumeric tokens << token unless token.empty? tokens << char unless alphanumeric token = '' else token += char end end return tokens end def operation_description binary = @binary ? 'binary ' : '' if @file_removed op = "Deleted" elsif @file_added op = "Added" else op = "Changed" end file_name = @current_file_name if (@config["link_files"] && @config["link_files"] == "gitweb" && @config["gitweb"]) file_name = "#{file_name}" elsif (@config["link_files"] && @config["link_files"] == "gitorious" && @config["gitorious"]) file_name = "#{file_name}" end header = "#{op} #{binary}file #{file_name}" "

#{header}

\n" end def add_changes_to_result return if @current_file_name.nil? @diff_result << operation_description @diff_result << '' unless @diff_lines.empty? removals = [] additions = [] @diff_lines.each do |line| if [:addition, :removal].include?(line[:op]) removals << line if line[:op] == :removal additions << line if line[:op] == :addition end if line[:op] == :unchanged || line == @diff_lines.last # unchanged line or end of block, add prev lines to result if removals.size > 0 && additions.size > 0 # block of removed and added lines - perform intelligent diff add_block_to_results(lcs_diff(removals, additions), escape = false) else # some lines removed or added - no need to perform intelligent diff add_block_to_results(removals + additions, escape = true) end removals = [] additions = [] add_line_to_result(line, escape = true) if line[:op] == :unchanged end end @diff_lines = [] @diff_result << '
' end # reset values @right_ln = nil @left_ln = nil @file_added = false @file_removed = false @binary = false end def diff_for_revision(content) @left_ln = @right_ln = nil @diff_result = [] @diff_lines = [] @removed_files = [] @current_file_name = nil content.split("\n").each do |line| if line =~ /^diff\s\-\-git/ line.match(/diff --git a\/(.*)\sb\//) file_name = $1 add_changes_to_result @current_file_name = file_name end op = line[0,1] @left_ln.nil? || op == '@' ? process_info_line(line, op) : process_code_line(line, op) end add_changes_to_result @diff_result.join("\n") end def process_code_line(line, op) if op == '-' @diff_lines << { :removed => @left_ln, :added => nil, :op => :removal, :content => line[1..-1] } @left_ln += 1 elsif op == '+' @diff_lines << { :added => @right_ln, :removed => nil, :op => :addition, :content => line[1..-1] } @right_ln += 1 else @right_ln @diff_lines << { :added => @right_ln, :removed => @left_ln, :op => :unchanged, :content => line } @right_ln += 1 @left_ln += 1 end end def process_info_line(line, op) if line =~/^deleted\sfile\s/ @file_removed = true elsif line =~ /^\-\-\-\s/ && line =~ /\/dev\/null/ @file_added = true elsif line =~ /^\+\+\+\s/ && line =~ /\/dev\/null/ @file_removed = true elsif line =~ /^Binary files \/dev\/null/ # Binary files /dev/null and ... differ (addition) @binary = true @file_added = true elsif line =~ /\/dev\/null differ/ # Binary files ... and /dev/null differ (removal) @binary = true @file_removed = true elsif op == '@' @left_ln, @right_ln = range_info(line) end end def extract_diff_from_git_show_output(content) diff = [] diff_found = false content.split("\n").each do |line| diff_found = true if line =~ /^diff \-\-git/ next unless diff_found diff << line end diff.join("\n") end def extract_commit_info_from_git_show_output(content) result = { :message => [], :commit => '', :author => '', :date => '', :email => '' } content.split("\n").each do |line| if line =~ /^diff/ # end of commit info, return results return result elsif line =~ /^commit/ result[:commit] = line[7..-1] elsif line =~ /^Author/ result[:author], result[:email] = author_name_and_email(line[8..-1]) elsif line =~ /^Date/ result[:date] = line[8..-1] elsif line =~ /^Merge/ result[:merge] = line[8..-1] else clean_line = line.strip result[:message] << clean_line unless clean_line.empty? end end result end def message_array_as_html(message) message_map(message.collect { |m| CGI.escapeHTML(m)}.join("
")) end def author_name_and_email(info) # input string format: "autor name " result = info.scan(/(.*)\s<(.*)>/)[0] return result if result.is_a?(Array) && result.size == 2 # normal operation # incomplete author info - return it as author name return [info, ''] if result.nil? end def first_sentence(message_array) msg = message_array.first.to_s.strip return message_array.first if msg.empty? || msg =~ /^Merge\:/ msg end def diff_between_revisions(rev1, rev2, repo, branch) @result = [] if rev1 == rev2 commits = [rev1] elsif rev1 =~ /^0+$/ # creating a new remote branch commits = Git.branch_commits(branch) elsif rev2 =~ /^0+$/ # deleting an existing remote branch commits = [] else log = Git.log(rev1, rev2) commits = log.scan(/^commit\s([a-f0-9]+)/).map{|match| match[0]} end if defined?(Test::Unit) previous_list = [] else previous_file = (!@previous_dir.nil? && File.exists?(@previous_dir)) ? File.join(@previous_dir, "previously.txt") : "/tmp/previously.txt" previous_list = (File.read(previous_file).to_a.map {|sha| sha.chomp!} if File.exist?(previous_file)) || [] end commits.reject!{|c| c.find{|sha| previous_list.include?(sha)} } current_list = (previous_list + commits.flatten).last(1000) File.open(previous_file, "w"){|f| f << current_list.join("\n") } unless current_list.empty? || defined?(Test::Unit) commits.each_with_index do |commit, i| raw_diff = Git.show(commit) raise "git show output is empty" if raw_diff.empty? @last_raw = raw_diff commit_info = extract_commit_info_from_git_show_output(raw_diff) title = "
" title += "Message: #{message_array_as_html commit_info[:message]}
\n" title += "Commit: " if (@config["link_files"] && @config["link_files"] == "gitweb" && @config["gitweb"]) title += "#{commit_info[:commit]}" elsif (@config["link_files"] && @config["link_files"] == "gitorious" && @config["gitorious"]) title += "#{commit_info[:commit]}" else title += " #{commit_info[:commit]}" end title += "
\n" title += "Branch: #{branch}\n
" unless branch =~ /\/head/ title += "Date: #{CGI.escapeHTML commit_info[:date]}\n
" title += "Author: #{CGI.escapeHTML(commit_info[:author])} <#{commit_info[:email]}>\n
" text = "#{raw_diff}\n\n\n" html = title html += diff_for_revision(extract_diff_from_git_show_output(raw_diff)) html += "

" commit_info[:message] = first_sentence(commit_info[:message]) @result << {:commit_info => commit_info, :html_content => html, :text_content => text } end end def message_replace!(message, search_for, replace_with) full_replace_with = "\\0" message.gsub!(Regexp.new(search_for), full_replace_with) end def message_map(message) if @config.include?('message_integration') @config['message_integration'].each_pair do |pm, url| pm_def = DiffToHtml::INTEGRATION_MAP[pm.to_sym] or next replace_with = pm_def[:replace_with].gsub('#{url}', url) message_replace!(message, pm_def[:search_for], replace_with) end end if @config.include?('message_map') @config['message_map'].each_pair do |search_for, replace_with| message_replace!(message, Regexp.new(search_for), replace_with) end end message end end class DiffCallback attr_reader :tags def initialize @tags = [] end def match(event) @tags << { :action => :match, :token => event.old_element } end def discard_b(event) @tags << { :action => :discard_b, :token => event.new_element } end def discard_a(event) @tags << { :action => :discard_a, :token => event.old_element } end end