diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index bebe1d5ac..d75b85b64 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -15,6 +15,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'diff' + class WikiController < ApplicationController layout 'base' before_filter :find_wiki, :check_project_privacy @@ -84,12 +86,18 @@ class WikiController < ApplicationController @versions = @page.content.versions.find :all, :select => "id, author_id, comments, updated_on, version", :order => 'version DESC', - :limit => @version_pages.items_per_page, + :limit => @version_pages.items_per_page + 1, :offset => @version_pages.current.offset render :layout => false if request.xhr? end + def diff + @page = @wiki.find_page(params[:page]) + @diff = @page.diff(params[:version], params[:version_from]) + render_404 unless @diff + end + # remove a wiki page and its history def destroy @page = @wiki.find_page(params[:page]) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3b6b19e35..5e3146ca5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -171,6 +171,14 @@ module ApplicationHelper text = @do_textilize ? auto_link(RedCloth.new(text, [:hard_breaks]).to_html) : simple_format(auto_link(h(text))) end + # Same as Rails' simple_format helper without using paragraphs + def simple_format_without_paragraph(text) + text.to_s. + gsub(/\r\n?/, "\n"). # \r\n and \r -> \n + gsub(/\n\n+/, "

"). # 2+ newline -> 2 br + gsub(/([^\n]\n)(?=[^\n])/, '\1
') # 1 newline -> br + end + def error_messages_for(object_name, options = {}) options = options.symbolize_keys object = instance_variable_get("@#{object_name}") diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 32b376925..980035bd4 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -16,4 +16,41 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module WikiHelper + + def html_diff(wdiff) + words = wdiff.words.collect{|word| h(word)} + words_add = 0 + words_del = 0 + dels = 0 + del_off = 0 + wdiff.diff.diffs.each do |diff| + add_at = nil + add_to = nil + del_at = nil + deleted = "" + diff.each do |change| + pos = change[1] + if change[0] == "+" + add_at = pos + dels unless add_at + add_to = pos + dels + words_add += 1 + else + del_at = pos unless del_at + deleted << ' ' + change[2] + words_del += 1 + end + end + if add_at + words[add_at] = '' + words[add_at] + words[add_to] = words[add_to] + '' + end + if del_at + words.insert del_at - del_off + dels + words_add, '' + deleted + '' + dels += 1 + del_off += words_del + words_del = 0 + end + end + simple_format_without_paragraph(words.join(' ')) + end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 562465197..074d36daa 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -15,6 +15,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'diff' + class WikiPage < ActiveRecord::Base belongs_to :wiki has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy @@ -39,6 +41,17 @@ class WikiPage < ActiveRecord::Base result end + def diff(version_to=nil, version_from=nil) + version_to = version_to ? version_to.to_i : self.content.version + version_from = version_from ? version_from.to_i : version_to - 1 + version_to, version_from = version_from, version_to unless version_from < version_to + + content_to = content.versions.find_by_version(version_to) + content_from = content.versions.find_by_version(version_from) + + (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil + end + def self.pretty_title(str) (str && str.is_a?(String)) ? str.tr('_', ' ') : str end @@ -47,3 +60,17 @@ class WikiPage < ActiveRecord::Base wiki.project end end + +class WikiDiff + attr_reader :diff, :words, :content_to, :content_from + + def initialize(content_to, content_from) + @content_to = content_to + @content_from = content_from + @words = content_to.text.split(/(\s+)/) + @words = @words.select {|word| word != ' '} + words_from = content_from.text.split(/(\s+)/) + words_from = words_from.select {|word| word != ' '} + @diff = words_from.diff @words + end +end diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index 5e9d0f992..0caa4585a 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -38,7 +38,8 @@ <% elsif e.is_a? Document %> <%= e.created_on.strftime("%H:%M") %> <%=l(:label_document)%>: <%= link_to h(e.title), :controller => 'documents', :action => 'show', :id => e %>
<% elsif e.is_a? WikiContent.versioned_class %> - <%= e.created_on.strftime("%H:%M") %> <%=l(:label_wiki_edit)%>: <%= link_to h(WikiPage.pretty_title(e.title)), :controller => 'wiki', :page => e.title %> (<%= link_to '#' + e.version.to_s, :controller => 'wiki', :page => e.title, :version => e.version %>)
+ <%= e.created_on.strftime("%H:%M") %> <%=l(:label_wiki_edit)%>: <%= link_to h(WikiPage.pretty_title(e.title)), :controller => 'wiki', :page => e.title %> + (<%= link_to '#' + e.version.to_s, :controller => 'wiki', :page => e.title, :version => e.version %><%= ', ' + link_to('diff', :controller => 'wiki', :action => 'diff', :page => e.title, :version => e.version) if e.version > 1 %>)
<% unless e.comments.blank? %><%=h e.comments %><% end %> <% elsif e.is_a? Changeset %> <%= e.created_on.strftime("%H:%M") %> <%=l(:label_revision)%> <%= link_to h(e.revision), :controller => 'repositories', :action => 'revision', :id => @project, :rev => e.revision %>
diff --git a/app/views/wiki/diff.rhtml b/app/views/wiki/diff.rhtml new file mode 100644 index 000000000..8db2df008 --- /dev/null +++ b/app/views/wiki/diff.rhtml @@ -0,0 +1,19 @@ +
+<%= link_to(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') %> +<%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %> +<%= link_to(l(:label_page_index), {:action => 'special', :page => 'Page_index'}, :class => 'icon icon-index') %> +
+ +

<%= @page.pretty_title %>

+ +

+<%= l(:label_version) %> <%= link_to @diff.content_from.version, :action => 'index', :page => @page.title, :version => @diff.content_from.version %> +(<%= @diff.content_from.author ? @diff.content_from.author.name : "anonyme" %>, <%= format_time(@diff.content_from.updated_on) %>) +→ +<%= l(:label_version) %> <%= link_to @diff.content_to.version, :action => 'index', :page => @page.title, :version => @diff.content_to.version %>/<%= @page.content.version %> +(<%= @diff.content_to.author ? @diff.content_to.author.name : "anonyme" %>, <%= format_time(@diff.content_to.updated_on) %>) +

+ +
+ +<%= html_diff(@diff) %> diff --git a/app/views/wiki/history.rhtml b/app/views/wiki/history.rhtml index 6040072ea..85b1541cc 100644 --- a/app/views/wiki/history.rhtml +++ b/app/views/wiki/history.rhtml @@ -6,26 +6,33 @@

<%= l(:label_history) %>

+<% form_tag({:action => "diff"}, :method => :get) do %> + + +<% show_diff = @versions.size > 1 %> +<% line_num = 1 %> <% @versions.each do |ver| %> "> + + +<% line_num += 1 %> <% end %>
# <%= l(:field_updated_on) %> <%= l(:field_author) %> <%= l(:field_comments) %>
<%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true || $('version_from').value > #{ver.version}) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %> <%= format_time(ver.updated_on) %> <%= ver.author ? ver.author.name : "anonyme" %> <%=h ver.comments %>
- -

<%= pagination_links_full @version_pages, :page_param => :p %> -[ <%= @version_pages.current.first_item %> - <%= @version_pages.current.last_item %> / <%= @version_count %> ]

- -

<%= link_to l(:button_back), :action => 'index', :page => @page.title %>

+<%= submit_tag l(:label_view_diff), :class => 'small' %> +<%= pagination_links_full @version_pages, :page_param => :p %> +[ <%= @version_pages.current.first_item %> - <%= @version_pages.current.last_item %> / <%= @version_count %> ] +<% end %> diff --git a/app/views/wiki/show.rhtml b/app/views/wiki/show.rhtml index 3a830a47d..c7a2985be 100644 --- a/app/views/wiki/show.rhtml +++ b/app/views/wiki/show.rhtml @@ -9,7 +9,8 @@ <% if @content.version != @page.content.version %>

<%= link_to(('« ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %> - <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %> - + <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %> + <%= '(' + link_to('diff', :controller => 'wiki', :action => 'diff', :page => @page.title, :version => @content.version) + ')' if @content.version > 1 %> - <%= link_to((l(:label_next) + ' »'), :action => 'index', :page => @page.title, :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %> <%= link_to(l(:label_current_version), :action => 'index', :page => @page.title) %>
diff --git a/lib/diff.rb b/lib/diff.rb new file mode 100644 index 000000000..646f91bae --- /dev/null +++ b/lib/diff.rb @@ -0,0 +1,280 @@ +class Diff + + VERSION = 0.3 + + def Diff.lcs(a, b) + astart = 0 + bstart = 0 + afinish = a.length-1 + bfinish = b.length-1 + mvector = [] + + # First we prune off any common elements at the beginning + while (astart <= afinish && bstart <= afinish && a[astart] == b[bstart]) + mvector[astart] = bstart + astart += 1 + bstart += 1 + end + + # now the end + while (astart <= afinish && bstart <= bfinish && a[afinish] == b[bfinish]) + mvector[afinish] = bfinish + afinish -= 1 + bfinish -= 1 + end + + bmatches = b.reverse_hash(bstart..bfinish) + thresh = [] + links = [] + + (astart..afinish).each { |aindex| + aelem = a[aindex] + next unless bmatches.has_key? aelem + k = nil + bmatches[aelem].reverse.each { |bindex| + if k && (thresh[k] > bindex) && (thresh[k-1] < bindex) + thresh[k] = bindex + else + k = thresh.replacenextlarger(bindex, k) + end + links[k] = [ (k==0) ? nil : links[k-1], aindex, bindex ] if k + } + } + + if !thresh.empty? + link = links[thresh.length-1] + while link + mvector[link[1]] = link[2] + link = link[0] + end + end + + return mvector + end + + def makediff(a, b) + mvector = Diff.lcs(a, b) + ai = bi = 0 + while ai < mvector.length + bline = mvector[ai] + if bline + while bi < bline + discardb(bi, b[bi]) + bi += 1 + end + match(ai, bi) + bi += 1 + else + discarda(ai, a[ai]) + end + ai += 1 + end + while ai < a.length + discarda(ai, a[ai]) + ai += 1 + end + while bi < b.length + discardb(bi, b[bi]) + bi += 1 + end + match(ai, bi) + 1 + end + + def compactdiffs + diffs = [] + @diffs.each { |df| + i = 0 + curdiff = [] + while i < df.length + whot = df[i][0] + s = @isstring ? df[i][2].chr : [df[i][2]] + p = df[i][1] + last = df[i][1] + i += 1 + while df[i] && df[i][0] == whot && df[i][1] == last+1 + s << df[i][2] + last = df[i][1] + i += 1 + end + curdiff.push [whot, p, s] + end + diffs.push curdiff + } + return diffs + end + + attr_reader :diffs, :difftype + + def initialize(diffs_or_a, b = nil, isstring = nil) + if b.nil? + @diffs = diffs_or_a + @isstring = isstring + else + @diffs = [] + @curdiffs = [] + makediff(diffs_or_a, b) + @difftype = diffs_or_a.class + end + end + + def match(ai, bi) + @diffs.push @curdiffs unless @curdiffs.empty? + @curdiffs = [] + end + + def discarda(i, elem) + @curdiffs.push ['-', i, elem] + end + + def discardb(i, elem) + @curdiffs.push ['+', i, elem] + end + + def compact + return Diff.new(compactdiffs) + end + + def compact! + @diffs = compactdiffs + end + + def inspect + @diffs.inspect + end + +end + +module Diffable + def diff(b) + Diff.new(self, b) + end + + # Create a hash that maps elements of the array to arrays of indices + # where the elements are found. + + def reverse_hash(range = (0...self.length)) + revmap = {} + range.each { |i| + elem = self[i] + if revmap.has_key? elem + revmap[elem].push i + else + revmap[elem] = [i] + end + } + return revmap + end + + def replacenextlarger(value, high = nil) + high ||= self.length + if self.empty? || value > self[-1] + push value + return high + end + # binary search for replacement point + low = 0 + while low < high + index = (high+low)/2 + found = self[index] + return nil if value == found + if value > found + low = index + 1 + else + high = index + end + end + + self[low] = value + # $stderr << "replace #{value} : 0/#{low}/#{init_high} (#{steps} steps) (#{init_high-low} off )\n" + # $stderr.puts self.inspect + #gets + #p length - low + return low + end + + def patch(diff) + newary = nil + if diff.difftype == String + newary = diff.difftype.new('') + else + newary = diff.difftype.new + end + ai = 0 + bi = 0 + diff.diffs.each { |d| + d.each { |mod| + case mod[0] + when '-' + while ai < mod[1] + newary << self[ai] + ai += 1 + bi += 1 + end + ai += 1 + when '+' + while bi < mod[1] + newary << self[ai] + ai += 1 + bi += 1 + end + newary << mod[2] + bi += 1 + else + raise "Unknown diff action" + end + } + } + while ai < self.length + newary << self[ai] + ai += 1 + bi += 1 + end + return newary + end +end + +class Array + include Diffable +end + +class String + include Diffable +end + +=begin += Diff +(({diff.rb})) - computes the differences between two arrays or +strings. Copyright (C) 2001 Lars Christensen + +== Synopsis + + diff = Diff.new(a, b) + b = a.patch(diff) + +== Class Diff +=== Class Methods +--- Diff.new(a, b) +--- a.diff(b) + Creates a Diff object which represent the differences between + ((|a|)) and ((|b|)). ((|a|)) and ((|b|)) can be either be arrays + of any objects, strings, or object of any class that include + module ((|Diffable|)) + +== Module Diffable +The module ((|Diffable|)) is intended to be included in any class for +which differences are to be computed. Diffable is included into String +and Array when (({diff.rb})) is (({require}))'d. + +Classes including Diffable should implement (({[]})) to get element at +integer indices, (({<<})) to append elements to the object and +(({ClassName#new})) should accept 0 arguments to create a new empty +object. + +=== Instance Methods +--- Diffable#patch(diff) + Applies the differences from ((|diff|)) to the object ((|obj|)) + and return the result. ((|obj|)) is not changed. ((|obj|)) and + can be either an array or a string, but must match the object + from which the ((|diff|)) was created. +=end diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 79fe2799a..809bdd024 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -647,6 +647,14 @@ div.wiki img { margin: 6px; } +.diff_out{ + background: #fcc; +} + +.diff_in{ + background: #cfc; +} + #preview .preview { background: #fafbfc url(../images/draft.png); } #ajax-indicator { diff --git a/public/stylesheets/scm.css b/public/stylesheets/scm.css index 4a9655a52..377575342 100644 --- a/public/stylesheets/scm.css +++ b/public/stylesheets/scm.css @@ -26,11 +26,3 @@ table.list thead th.list-filename { font-weight: bolder; text-align: left; } - -.diff_out{ - background: #fdd; -} - -.diff_in{ - background: #dfd; -}