diff --git a/.gitignore b/.gitignore index 853230d740e..61c1e709091 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ /tmp/test/* /vendor/rails *.rbc +/.idea +*.pyc diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 474c68c9134..3d1dbe9f1b3 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -44,6 +44,8 @@ class IssuesController < ApplicationController include AttachmentsHelper helper :queries include QueriesHelper + helper :repositories + include RepositoriesHelper helper :sort include SortHelper include IssuesHelper diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index b6dcc317343..66286071a32 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -174,6 +174,9 @@ def diff @diff = @repository.diff(@path, @rev, @rev_to) show_error_not_found unless @diff end + + @changeset = @repository.find_changeset_by_name(@rev) + @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8d4f90c7600..3a4d4fdcd97 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -104,8 +104,10 @@ def link_to_attachment(attachment, options={}) # * :text - Link text (default to the formatted revision) def link_to_revision(revision, project, options={}) text = options.delete(:text) || format_revision(revision) + rev = revision.respond_to?(:identifier) ? revision.identifier : revision - link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision)) + link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev}, + :title => l(:label_revision_id, format_revision(revision))) end # Generates a link to a project if active @@ -642,7 +644,7 @@ def parse_redmine_links(text, project, obj, attr, only_path, options) end when 'commit' if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"])) - link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, + link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier}, :class => 'changeset', :title => truncate_single_line(changeset.comments, :length => 100) end diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 95962873541..62af7e6abd7 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -18,8 +18,12 @@ require 'iconv' module RepositoriesHelper - def format_revision(txt) - txt.to_s[0,8] + def format_revision(revision) + if revision.respond_to? :format_identifier + revision.format_identifier + else + revision.to_s + end end def truncate_at_line_break(text, length = 255) @@ -87,7 +91,7 @@ def render_changes_tree(tree) :action => 'show', :id => @project, :path => path_param, - :rev => @changeset.revision) + :rev => @changeset.identifier) output << "
  • #{text}
  • " output << render_changes_tree(s) elsif c = tree[file][:c] @@ -97,13 +101,13 @@ def render_changes_tree(tree) :action => 'entry', :id => @project, :path => path_param, - :rev => @changeset.revision) unless c.action == 'D' + :rev => @changeset.identifier) unless c.action == 'D' text << " - #{c.revision}" unless c.revision.blank? text << ' (' + link_to('diff', :controller => 'repositories', :action => 'diff', :id => @project, :path => path_param, - :rev => @changeset.revision) + ') ' if c.action == 'M' + :rev => @changeset.identifier) + ') ' if c.action == 'M' text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank? output << "
  • #{text}
  • " end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 804719481e2..dfd8191a015 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -23,10 +23,10 @@ class Changeset < ActiveRecord::Base has_many :changes, :dependent => :delete_all has_and_belongs_to_many :issues - acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, + acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, :description => :long_comments, :datetime => :committed_on, - :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}} + :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}} acts_as_searchable :columns => 'comments', :include => {:repository => :project}, @@ -47,6 +47,15 @@ class Changeset < ActiveRecord::Base def revision=(r) write_attribute :revision, (r.nil? ? nil : r.to_s) end + + # Returns the identifier of this changeset; depending on repository backends + def identifier + if repository.class.respond_to? :changeset_identifier + repository.class.changeset_identifier self + else + revision.to_s + end + end def comments=(comment) write_attribute(:comments, Changeset.normalize_comments(comment)) @@ -56,6 +65,15 @@ def committed_on=(date) self.commit_date = date super end + + # Returns the readable identifier + def format_identifier + if repository.class.respond_to? :format_changeset_identifier + repository.class.format_changeset_identifier self + else + identifier + end + end def committer=(arg) write_attribute(:committer, self.class.to_utf8(arg.to_s)) diff --git a/app/models/repository.rb b/app/models/repository.rb index dee705c97d3..4362bed3aa0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -172,9 +172,24 @@ def find_committer_user(committer) def self.fetch_changesets Project.active.has_module(:repository).find(:all, :include => :repository).each do |project| if project.repository - project.repository.fetch_changesets + begin + project.repository.fetch_changesets + rescue Redmine::Scm::Adapters::CommandFailed => e + logger.error "Repository: error during fetching changesets: #{e.message}" + end + end + end + end + + # Wipes out all changesets for this repository and fetches them new. Good for + # VCSes like Bazaar where revision numbers can change. + def self.clear_and_fetch_changesets + Project.active.has_module(:repository).find(:all, :include => :repository).each do |project| + if project.repository && project.repository.respond_to?(:clear_changesets) + project.repository.clear_changesets end end + self.fetch_changesets end # scan changeset comments to find related and fixed issues for all repositories @@ -196,6 +211,14 @@ def self.factory(klass_name, *args) rescue nil end + + def clear_changesets + cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}" + connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") + connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") + connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}") + end + private @@ -206,10 +229,4 @@ def before_save true end - def clear_changesets - cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}" - connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") - connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") - connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}") - end end diff --git a/app/models/repository/bazaar.rb b/app/models/repository/bazaar.rb index ec953bd4542..c59a971f007 100644 --- a/app/models/repository/bazaar.rb +++ b/app/models/repository/bazaar.rb @@ -28,7 +28,17 @@ def scm_adapter def self.scm_name 'Bazaar' end - + + # Returns the identifier for the given bazaar changeset + def self.changeset_identifier(changeset) + changeset.scmid + end + + # Returns the readable identifier for the given bazaar changeset + def self.format_changeset_identifier(changeset) + changeset.revision + end + def entries(path=nil, identifier=nil) entries = scm.entries(path, identifier) if entries @@ -51,41 +61,59 @@ def entries(path=nil, identifier=nil) end end end - + + # With SCM's that have a sequential commit numbering, redmine is able to be + # clever and only fetch changesets going forward from the most recent one + # it knows about. However, with bazaar, you never know if people have merged + # commits into the middle of the repository history, so we should parse + # the entire log. Since it's way too slow for large repositories, we only + # parse 1 week before the last known commit. + # The repository can still be fully reloaded by calling #clear_changesets + # before fetching changesets (eg. for offline resync), you can set this up + # as an external job with rake redmine:clear_and_fetch_changesets def fetch_changesets - scm_info = scm.info - if scm_info - # latest revision found in database - db_revision = latest_changeset ? latest_changeset.revision.to_i : 0 - # latest revision in the repository - scm_revision = scm_info.lastrev.identifier.to_i - if db_revision < scm_revision - logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? - identifier_from = db_revision + 1 - while (identifier_from <= scm_revision) - # loads changesets by batches of 200 - identifier_to = [identifier_from + 199, scm_revision].min - revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true) - transaction do - revisions.reverse_each do |revision| - changeset = Changeset.create(:repository => self, - :revision => revision.identifier, - :committer => revision.author, - :committed_on => revision.time, - :scmid => revision.scmid, - :comments => revision.message) - - revision.paths.each do |change| - Change.create(:changeset => changeset, - :action => change[:action], - :path => change[:path], - :revision => change[:revision]) - end - end - end unless revisions.nil? - identifier_from = identifier_to + 1 - end - end + c = changesets.find(:first, :order => 'committed_on DESC') + since = (c ? c.committed_on - 7.days : nil) + + revisions = scm.revisions('', nil, nil, :all => true, :since => since) + return if revisions.nil? || revisions.empty? + + recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since]) + + # Clean out revisions that are no longer in bazaar + recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }} + + # Subtract revisions that redmine already knows about + recent_revisions = recent_changesets.map{|c| c.scmid} + revisions.reject!{|r| recent_revisions.include?(r.scmid)} + + # Save the remaining ones to the database + revisions.each{|r| r.save(self)} unless revisions.nil? + end + + # Finds and returns a revision with a number or the beginning of a hash + def find_changeset_by_name(name) + if /[^\d\.]/ =~ name + e = changesets.find(:first, :conditions => ['scmid = ?', name.to_s]) + else + e = changesets.find(:first, :conditions => ['revision = ?', name.to_s]) end + return e if e + changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) # last ditch end + + def latest_changesets(path,rev,limit=10) + revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false) + return [] if revisions.nil? || revisions.empty? + + changesets.find( + :all, + :conditions => [ + "scmid IN (?)", + revisions.map!{|c| c.scmid} + ], + :order => 'committed_on DESC' + ) + end + end diff --git a/app/models/repository/git.rb b/app/models/repository/git.rb index 473eb07e290..9349f3c113a 100644 --- a/app/models/repository/git.rb +++ b/app/models/repository/git.rb @@ -29,6 +29,16 @@ def self.scm_name 'Git' end + # Returns the identifier for the given git changeset + def self.changeset_identifier(changeset) + changeset.scmid + end + + # Returns the readable identifier for the given git changeset + def self.format_changeset_identifier(changeset) + changeset.revision[0, 8] + end + def branches scm.branches end diff --git a/app/views/issues/_changesets.rhtml b/app/views/issues/_changesets.rhtml index 52cd60ff571..d5a292896cc 100644 --- a/app/views/issues/_changesets.rhtml +++ b/app/views/issues/_changesets.rhtml @@ -1,7 +1,7 @@ <% changesets.each do |changeset| %>
    -

    <%= link_to("#{l(:label_revision)} #{changeset.revision}", - :controller => 'repositories', :action => 'revision', :id => changeset.project, :rev => changeset.revision) %>
    +

    <%= link_to_revision(changeset, changeset.project, + :text => "#{l(:label_revision)} #{changeset.format_identifier}") %>
    <%= authoring(changeset.committed_on, changeset.author) %>

    <%= textilizable(changeset, :comments) %> diff --git a/app/views/repositories/_dir_list_content.rhtml b/app/views/repositories/_dir_list_content.rhtml index c5bd53ea78b..66574f1c86e 100644 --- a/app/views/repositories/_dir_list_content.rhtml +++ b/app/views/repositories/_dir_list_content.rhtml @@ -17,7 +17,7 @@ <%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %> <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %> -<%= link_to_revision(changeset.revision, @project) if changeset %> +<%= link_to_revision(changeset, @project) if changeset %> <%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %> <%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %> <%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %> diff --git a/app/views/repositories/_revisions.rhtml b/app/views/repositories/_revisions.rhtml index 26fb5b6992a..92c6fb535a7 100644 --- a/app/views/repositories/_revisions.rhtml +++ b/app/views/repositories/_revisions.rhtml @@ -13,9 +13,9 @@ <% line_num = 1 %> <% revisions.each do |changeset| %> -<%= link_to_revision(changeset.revision, project) %> -<%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %> -<%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %> +<%= link_to_revision(changeset, project) %> +<%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %> +<%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %> <%= format_time(changeset.committed_on) %> <%=h changeset.author %> <%= textilizable(truncate_at_line_break(changeset.comments)) %> diff --git a/app/views/repositories/annotate.rhtml b/app/views/repositories/annotate.rhtml index a18e9bbac3e..498507148de 100644 --- a/app/views/repositories/annotate.rhtml +++ b/app/views/repositories/annotate.rhtml @@ -19,7 +19,7 @@ <%= line_num %> - <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %> + <%= (revision.identifier ? link_to_revision(revision, @project) : format_revision(revision)) if revision %> <%= h(revision.author.to_s.split('<').first) if revision %>
    <%= line %>
    diff --git a/app/views/repositories/diff.rhtml b/app/views/repositories/diff.rhtml index 24f92a540ae..efe17f17769 100644 --- a/app/views/repositories/diff.rhtml +++ b/app/views/repositories/diff.rhtml @@ -1,4 +1,4 @@ -

    <%= l(:label_revision) %> <%= format_revision(@rev_to) + ':' if @rev_to %><%= format_revision(@rev) %> <%=h @path %>

    +

    <%= l(:label_revision) %> <%= format_revision(@changeset_to) + ' : ' if @changeset_to %><%= format_revision(@changeset) %> <%=h @path %>

    <% form_tag({:path => to_path_param(@path)}, :method => 'get') do %> diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml index 92597dff7c9..483e358de39 100644 --- a/app/views/repositories/revision.rhtml +++ b/app/views/repositories/revision.rhtml @@ -1,25 +1,25 @@
    « <% unless @changeset.previous.nil? -%> - <%= link_to_revision(@changeset.previous.revision, @project, :text => l(:label_previous)) %> + <%= link_to_revision(@changeset.previous, @project, :text => l(:label_previous)) %> <% else -%> <%= l(:label_previous) %> <% end -%> | <% unless @changeset.next.nil? -%> - <%= link_to_revision(@changeset.next.revision, @project, :text => l(:label_next)) %> + <%= link_to_revision(@changeset.next, @project, :text => l(:label_next)) %> <% else -%> <%= l(:label_next) %> <% end -%> »  <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %> - <%= text_field_tag 'rev', @rev[0,8], :size => 8 %> + <%= text_field_tag 'rev', @rev, :size => 8 %> <%= submit_tag 'OK', :name => nil %> <% end %>
    -

    <%= l(:label_revision) %> <%= format_revision(@changeset.revision) %>

    +

    <%= l(:label_revision) %> <%= format_revision(@changeset) %>

    <% if @changeset.scmid %>ID: <%= @changeset.scmid %>
    <% end %> <%= authoring(@changeset.committed_on, @changeset.author) %>

    @@ -45,7 +45,7 @@
  • <%= l(:label_deleted) %>
  • -

    <%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %>

    +

    <%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.identifier) if @changeset.changes.any? %>

    <%= render_changeset_changes %> @@ -56,4 +56,4 @@ <%= stylesheet_link_tag "scm" %> <% end %> -<% html_title("#{l(:label_revision)} #{@changeset.revision}") -%> +<% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%> diff --git a/config/routes.rb b/config/routes.rb index 3b37b515089..cbfa4ace30f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -195,11 +195,11 @@ repository_views.connect 'projects/:id/repository/statistics', :action => 'stats' repository_views.connect 'projects/:id/repository/revisions', :action => 'revisions' repository_views.connect 'projects/:id/repository/revisions.:format', :action => 'revisions' - repository_views.connect 'projects/:id/repository/revisions/:rev', :action => 'revision' - repository_views.connect 'projects/:id/repository/revisions/:rev/diff', :action => 'diff' - repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format', :action => 'diff' - repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path', :action => 'entry', :format => 'raw', :requirements => { :rev => /[a-z0-9\.\-_]+/ } - repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path', :requirements => { :rev => /[a-z0-9\.\-_]+/ } + repository_views.connect 'projects/:id/repository/revisions/:rev', :action => 'revision', :requirements => { :rev => /[a-z0-9\.\-_@]+/ } + repository_views.connect 'projects/:id/repository/revisions/:rev/diff', :action => 'diff', :requirements => { :rev => /[a-z0-9\.\-_@]+/ } + repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format', :action => 'diff', :requirements => { :rev => /[a-z0-9\.\-_@]+/ } + repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path', :action => 'entry', :format => 'raw', :requirements => { :rev => /[a-z0-9\.\-_@]+/ } + repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path', :requirements => { :rev => /[a-z0-9\.\-_@]+/ } repository_views.connect 'projects/:id/repository/raw/*path', :action => 'entry', :format => 'raw' # TODO: why the following route is required? repository_views.connect 'projects/:id/repository/entry/*path', :action => 'entry' diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index a3ca61e2361..f98e90958f9 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -271,7 +271,8 @@ def latest end class Revision - attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch + attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch + attr_writer :identifier def initialize(attributes={}) self.identifier = attributes[:identifier] @@ -285,6 +286,16 @@ def initialize(attributes={}) self.branch = attributes[:branch] end + # Returns the identifier of this revision; see also Changeset model + def identifier + (@identifier || revision).to_s + end + + # Returns the readable identifier. + def format_identifier + identifier + end + def save(repo) Changeset.transaction do changeset = Changeset.new( diff --git a/lib/redmine/scm/adapters/bazaar_adapter.rb b/lib/redmine/scm/adapters/bazaar_adapter.rb index 3c6bdf542bc..95c208fea28 100644 --- a/lib/redmine/scm/adapters/bazaar_adapter.rb +++ b/lib/redmine/scm/adapters/bazaar_adapter.rb @@ -21,6 +21,13 @@ module Redmine module Scm module Adapters class BazaarAdapter < AbstractAdapter + + class Revision < Redmine::Scm::Adapters::Revision + # Returns the readable identifier + def format_identifier + revision + end + end # Bazaar executable name BZR_BIN = "bzr" @@ -30,10 +37,11 @@ def info cmd = "#{BZR_BIN} revno #{target('')}" info = nil shellout(cmd) do |io| - if io.read =~ %r{^(\d+)\r?$} + if io.read =~ %r{^(\d+(\.\d+)*)\r?$} info = Info.new({:root_url => url, :lastrev => Revision.new({ - :identifier => $1 + :identifier => $1, + :revision => $1 }) }) end @@ -50,8 +58,8 @@ def entries(path=nil, identifier=nil) path ||= '' entries = Entries.new cmd = "#{BZR_BIN} ls -v --show-ids" - identifier = -1 unless identifier && identifier.to_i > 0 - cmd << " -r#{identifier.to_i}" + rev_spec = identifier ? "revid:#{identifier}" : "-1" + cmd << " -r #{rev_spec}" cmd << " #{target(path)}" shellout(cmd) do |io| prefix = "#{url}/#{path}".gsub('\\', '/') @@ -74,35 +82,43 @@ def entries(path=nil, identifier=nil) def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) path ||= '' - identifier_from = 'last:1' unless identifier_from and identifier_from.to_i > 0 - identifier_to = 1 unless identifier_to and identifier_to.to_i > 0 revisions = Revisions.new - cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to.to_i}..#{identifier_from} #{target(path)}" + cmd = "#{BZR_BIN} log -n 0 -v --show-ids" + if options[:since] + cmd << " -r date:#{options[:since].strftime("%Y-%m-%d,%H:%M:%S")}.." + else + from = identifier_from ? "revid:#{identifier_from}" : "last:1" + to = identifier_to ? "revid:#{identifier_to}" : "1" + cmd << " -r #{to}..#{from}" + end + cmd << " -l #{options[:limit]} " if options[:limit] + cmd << " #{target(path)}" shellout(cmd) do |io| revision = nil parsing = nil io.each_line do |line| - if line =~ /^----/ + if line =~ /^\s*----/ revisions << revision if revision revision = Revision.new(:paths => [], :message => '') parsing = nil else next unless revision - if line =~ /^revno: (\d+)($|\s\[merge\]$)/ - revision.identifier = $1.to_i - elsif line =~ /^committer: (.+)$/ + if line =~ /^\s*revno: (\d+(\.\d+)*)($|\s\[merge\]$)/ + revision.revision = $1 + elsif line =~ /^\s*committer: (.+)$/ revision.author = $1.strip - elsif line =~ /^revision-id:(.+)$/ + elsif line =~ /^\s*revision-id:(.+)$/ revision.scmid = $1.strip - elsif line =~ /^timestamp: (.+)$/ + revision.identifier = revision.revision + elsif line =~ /^\s*timestamp: (.+)$/ revision.time = Time.parse($1).localtime - elsif line =~ /^ -----/ + elsif line =~ /^ \s*-----/ # partial revisions parsing = nil unless parsing == 'message' - elsif line =~ /^(message|added|modified|removed|renamed):/ + elsif line =~ /^\s*(message|added|modified|removed|renamed):/ parsing = $1 - elsif line =~ /^ (.*)$/ + elsif line =~ /^ \s*(.*)$/ if parsing == 'message' revision.message << "#{$1}\n" else @@ -135,12 +151,13 @@ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) def diff(path, identifier_from, identifier_to=nil) path ||= '' - if identifier_to - identifier_to = identifier_to.to_i + cmd = "#{BZR_BIN} diff" + if identifier_to.nil? + cmd << " -c revid:#{identifier_from}" else - identifier_to = identifier_from.to_i - 1 + cmd << " -r revid:#{identifier_to}..revid:#{identifier_from}" end - cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}" + cmd << " #{target(path)}" diff = [] shellout(cmd) do |io| io.each_line do |line| @@ -153,7 +170,7 @@ def diff(path, identifier_from, identifier_to=nil) def cat(path, identifier=nil) cmd = "#{BZR_BIN} cat" - cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0 + cmd << " -r revid:#{identifier}" if identifier cmd << " #{target(path)}" cat = nil shellout(cmd) do |io| @@ -165,18 +182,35 @@ def cat(path, identifier=nil) end def annotate(path, identifier=nil) - cmd = "#{BZR_BIN} annotate --all" - cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0 + cmd = "#{BZR_BIN} annotate --all --show-ids" + cmd << " -r revid:#{identifier}" if identifier cmd << " #{target(path)}" blame = Annotate.new + # With Bazaar, there is no way to show both the regular revision + # number (ie 121, 4.1.1, etc.) and the internal ID with one command. + # So run through the command twice + lines = [] shellout(cmd) do |io| - author = nil - identifier = nil io.each_line do |line| - next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$} - blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip)) + next unless line =~ %r{^\s*(\S+?)\-(\d+)\-(.+?) \| (.*)$} + lines << {:text => $4.rstrip, :scmid => "#{$1}-#{$2}-#{$3}", :author => $1.strip} end end + cmd.gsub!(/ \-\-show\-ids/, "") + i = 0 + shellout(cmd) do |io| + io.each_line do |line| + next unless line =~ %r{^\s*([\d\.]+) .*? \| (.*)$} + lines[i][:revision] = $1 + i += 1 + end + end + + lines.each do |l| + blame.add_line(l[:text], Revision.new(:identifier => l[:scmid], + :author => l[:author], :scmid => l[:scmid], + :revision => l[:revision])) + end return nil if $? && $?.exitstatus != 0 blame end diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb index e801f22f7b6..c02d566a5c0 100644 --- a/lib/redmine/scm/adapters/git_adapter.rb +++ b/lib/redmine/scm/adapters/git_adapter.rb @@ -264,6 +264,13 @@ def cat(path, identifier=nil) return nil if $? && $?.exitstatus != 0 cat end + + class Revision < Redmine::Scm::Adapters::Revision + # Returns the readable identifier + def format_identifier + identifier[0,8] + end + end end end end diff --git a/lib/tasks/fetch_changesets.rake b/lib/tasks/fetch_changesets.rake index 681032bd680..6ffb0a5173b 100644 --- a/lib/tasks/fetch_changesets.rake +++ b/lib/tasks/fetch_changesets.rake @@ -21,4 +21,7 @@ namespace :redmine do task :fetch_changesets => :environment do Repository.fetch_changesets end + task :clear_and_fetch_changesets => :environment do + Repository.clear_and_fetch_changesets + end end diff --git a/test/fixtures/repositories/bazaar_repository.tar.gz b/test/fixtures/repositories/bazaar_repository.tar.gz index 621c2f145e7..9900163b532 100644 Binary files a/test/fixtures/repositories/bazaar_repository.tar.gz and b/test/fixtures/repositories/bazaar_repository.tar.gz differ diff --git a/test/functional/repositories_bazaar_controller_test.rb b/test/functional/repositories_bazaar_controller_test.rb index 5f7de1de8e8..be0d847fb1d 100644 --- a/test/functional/repositories_bazaar_controller_test.rb +++ b/test/functional/repositories_bazaar_controller_test.rb @@ -51,7 +51,7 @@ def test_browse_root assert_not_nil assigns(:entries) assert_equal 2, assigns(:entries).size assert assigns(:entries).detect {|e| e.name == 'directory' && e.kind == 'dir'} - assert assigns(:entries).detect {|e| e.name == 'doc-mkdir.txt' && e.kind == 'file'} + assert assigns(:entries).detect {|e| e.name == 'root_level.txt' && e.kind == 'file'} end def test_browse_directory @@ -59,7 +59,7 @@ def test_browse_directory assert_response :success assert_template 'show' assert_not_nil assigns(:entries) - assert_equal ['doc-ls.txt', 'document.txt', 'edit.png'], assigns(:entries).collect(&:name) + assert_equal ["config.txt", "edit.png", "second_file.txt", "source2.txt"], assigns(:entries).collect(&:name) entry = assigns(:entries).detect {|e| e.name == 'edit.png'} assert_not_nil entry assert_equal 'file', entry.kind @@ -67,36 +67,36 @@ def test_browse_directory end def test_browse_at_given_revision - get :show, :id => 3, :path => [], :rev => 3 + get :show, :id => 3, :path => [], :rev => 'johndoe@no.server-20100927142810-5hx3443dk9mdbs3t' assert_response :success assert_template 'show' assert_not_nil assigns(:entries) - assert_equal ['directory', 'doc-deleted.txt', 'doc-ls.txt', 'doc-mkdir.txt'], assigns(:entries).collect(&:name) + assert_equal ['directory', 'mainfile.txt'], assigns(:entries).collect(&:name) end def test_changes - get :changes, :id => 3, :path => ['doc-mkdir.txt'] + get :changes, :id => 3, :path => ['root_level.txt'] assert_response :success assert_template 'changes' - assert_tag :tag => 'h2', :content => 'doc-mkdir.txt' + assert_tag :tag => 'h2', :content => 'root_level.txt' end def test_entry_show - get :entry, :id => 3, :path => ['directory', 'doc-ls.txt'] + get :entry, :id => 3, :path => ['directory', 'second_file.txt'] assert_response :success assert_template 'entry' - # Line 19 + # Line 2 assert_tag :tag => 'th', - :content => /29/, + :content => /2/, :attributes => { :class => /line-num/ }, - :sibling => { :tag => 'td', :content => /Show help message/ } + :sibling => { :tag => 'td', :content => /More code from/ } end def test_entry_download - get :entry, :id => 3, :path => ['directory', 'doc-ls.txt'], :format => 'raw' + get :entry, :id => 3, :path => ['directory', 'second_file.txt'], :format => 'raw' assert_response :success # File content - assert @response.body.include?('Show help message') + assert @response.body.include?('More code from') end def test_directory_entry @@ -109,7 +109,7 @@ def test_directory_entry def test_diff # Full diff of changeset 3 - get :diff, :id => 3, :rev => 3 + get :diff, :id => 3, :rev => 'johndoe@no.server-20100927142810-5hx3443dk9mdbs3t' assert_response :success assert_template 'diff' # Line 22 removed @@ -117,18 +117,18 @@ def test_diff :content => /2/, :sibling => { :tag => 'td', :attributes => { :class => /diff_in/ }, - :content => /Main purpose/ } + :content => /Added another line to the file/ } end def test_annotate - get :annotate, :id => 3, :path => ['doc-mkdir.txt'] + get :annotate, :id => 3, :path => ['root_level.txt'] assert_response :success assert_template 'annotate' # Line 2, revision 3 assert_tag :tag => 'th', :content => /2/, - :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /3/ } }, - :sibling => { :tag => 'td', :content => /jsmith/ }, - :sibling => { :tag => 'td', :content => /Main purpose/ } + :sibling => { :tag => 'td', :content => /5/, :child => { :tag => 'a', :content => /second@no\.server\-20100927143241\-aknlenpvde342upv/ } }, + :sibling => { :tag => 'td', :content => /second@no\.server/ }, + :sibling => { :tag => 'td', :content => /The above line is incorrect/ } end else puts "Bazaar test repository NOT FOUND. Skipping functional tests !!!" diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb index 9265fe9c5f0..2f0415d3ab6 100644 --- a/test/unit/changeset_test.rb +++ b/test/unit/changeset_test.rb @@ -218,4 +218,9 @@ def test_invalid_utf8_sequences_in_comments_should_be_stripped c.comments = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt") assert_equal "Texte encod en ISO-8859-1.", c.comments end + + def test_identifier + c = Changeset.find_by_revision('1') + assert_equal c.revision, c.identifier + end end diff --git a/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb new file mode 100644 index 00000000000..381f22b870f --- /dev/null +++ b/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb @@ -0,0 +1,112 @@ +require File.dirname(__FILE__) + '/../../../../../test_helper' + +class BazaarAdapterTest < ActiveSupport::TestCase + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository' + + if File.directory?(REPOSITORY_PATH) + def setup + @adapter = Redmine::Scm::Adapters::BazaarAdapter.new(REPOSITORY_PATH) + end + + def test_info + info = @adapter.info + assert_equal "7", info.lastrev.revision + end + + def test_entries + current_entries = @adapter.entries + assert_equal 2, current_entries.length + assert_equal "root_level.txt", current_entries[1].name + + old_entries = @adapter.entries("", "johndoe@no.server-20100927142810-5hx3443dk9mdbs3t") + assert_equal 2, old_entries.length + assert_equal "mainfile.txt", old_entries[1].name + + entries_dir = @adapter.entries("directory") + assert_equal 4, entries_dir.length + assert_equal "config.txt", entries_dir[0].name + end + + def test_revisions_no_options + revs = @adapter.revisions + assert_equal 8, revs.length + last_rev = revs[0] + sub_rev = revs[1] + assert_equal "7", last_rev.revision + assert_equal "second@no.server-20100927143627-e2mreqlpaodcixpg", last_rev.scmid + assert_equal "Second Developer ", last_rev.author + assert_equal Time.gm(2010,9,27,14,36,27).utc, last_rev.time.utc + assert_equal "4.1.1", sub_rev.revision + assert_equal "johndoe@no.server-20100927143451-vw9ij1q1max8nakq", sub_rev.scmid + assert_equal "John Doe ", sub_rev.author + assert_equal Time.gm(2010,9,27,14,34,51).utc, sub_rev.time.utc + [last_rev, sub_rev].each do |r| + assert_equal 1, r.paths.length + assert_equal "/directory/config.txt", r.paths[0][:path] + assert_equal "A", r.paths[0][:action] + assert_equal "config.txt-20100927143445-xgkt26w4b98wdc15-1", r.paths[0][:revision] + end + end + + def test_revision_path + revs = @adapter.revisions("directory/second_file.txt") + assert_equal 2, revs.length + assert_equal "6", revs[0].revision + assert_equal "5", revs[-1].revision + assert_equal 2, revs[0].paths.length + end + + def test_revisions_identifers + revs = @adapter.revisions(nil, "second@no.server-20100927143409-waety1q0cm1ur3sv", "johndoe@no.server-20100927142845-un2x20a6r2t3nz1w") + assert_equal 3, revs.length + assert_equal "6", revs[0].revision + assert_equal "4", revs[-1].revision + + revs = @adapter.revisions(nil, "second@no.server-20100927143409-waety1q0cm1ur3sv") + assert_equal 6, revs.length + assert_equal "6", revs[0].revision + assert_equal "1", revs[-1].revision + + revs = @adapter.revisions(nil, nil, "johndoe@no.server-20100927142845-un2x20a6r2t3nz1w") + assert_equal 5, revs.length + assert_equal "7", revs[0].revision + assert_equal "4", revs[-1].revision + end + + def test_revisions_options + revs = @adapter.revisions(nil, nil, nil, {:since => Time.gm(2010,9,27,14,34,0).localtime}) + assert_equal 3, revs.length + assert_equal "7", revs[0].revision + assert_equal "6", revs[-1].revision + end + + def test_diff + diff = @adapter.diff(nil, "second@no.server-20100927143627-e2mreqlpaodcixpg") + assert_equal 6, diff.length + assert_equal "+This is a placeholder for configuration data\n", diff[4] + + diff = @adapter.diff(nil, "johndoe@no.server-20100927142810-5hx3443dk9mdbs3t", "johndoe@no.server-20100927142357-09lh9svlopfrt2zh") + assert_equal 10, diff.length + assert_equal "+This file is in the directory\n", diff[7] + end + + def test_cat + assert_equal "#First file, not much to say here\nThe above line is incorrect\n", @adapter.cat("root_level.txt") + assert_equal "First file, not much to say here\n", @adapter.cat("root_level.txt", "johndoe@no.server-20100927142845-un2x20a6r2t3nz1w") + end + + def test_annotate + an = @adapter.annotate("directory/second_file.txt") + assert_equal 2, an.lines.length + assert_equal "second@no.server-20100927143241-aknlenpvde342upv", an.revisions[0].identifier + assert_equal 'second@no.server', an.revisions[0].author + assert_equal "This file was created by second developer", an.lines[0] + assert_equal "second@no.server-20100927143409-waety1q0cm1ur3sv", an.revisions[1].identifier + assert_equal "More code from", an.lines[1] + end + + else + puts "Bazaar test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff --git a/test/unit/repository_bazaar_test.rb b/test/unit/repository_bazaar_test.rb index bd1c9a9b420..bff5357a949 100644 --- a/test/unit/repository_bazaar_test.rb +++ b/test/unit/repository_bazaar_test.rb @@ -34,20 +34,22 @@ def test_fetch_changesets_from_scratch @repository.fetch_changesets @repository.reload - assert_equal 4, @repository.changesets.count - assert_equal 9, @repository.changes.count - assert_equal 'Initial import', @repository.changesets.find_by_revision('1').comments + assert_equal 8, @repository.changesets.count + assert_equal 13, @repository.changes.count + assert_equal 'Initial commit, just one file', @repository.find_changeset_by_name('1').comments + assert_equal 'Initial commit, just one file', + @repository.find_changeset_by_name('johndoe@no.server-20100927142357-09lh9svlopfrt2zh').comments end def test_fetch_changesets_incremental @repository.fetch_changesets - # Remove changesets with revision > 5 + # Remove changesets with revision > 2 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2} @repository.reload assert_equal 2, @repository.changesets.count @repository.fetch_changesets - assert_equal 4, @repository.changesets.count + assert_equal 8, @repository.changesets.count end def test_entries @@ -58,29 +60,17 @@ def test_entries assert_equal 'directory', entries[0].name assert_equal 'file', entries[1].kind - assert_equal 'doc-mkdir.txt', entries[1].name + assert_equal 'root_level.txt', entries[1].name end def test_entries_in_subdirectory entries = @repository.entries('directory') - assert_equal 3, entries.size + assert_equal 4, entries.size assert_equal 'file', entries.last.kind - assert_equal 'edit.png', entries.last.name + assert_equal 'source2.txt', entries.last.name end - def test_cat - cat = @repository.scm.cat('directory/document.txt') - assert cat =~ /Write the contents of a file as of a given revision to standard output/ - end - - def test_annotate - annotate = @repository.scm.annotate('doc-mkdir.txt') - assert_equal 17, annotate.lines.size - assert_equal 1, annotate.revisions[0].identifier - assert_equal 'jsmith@', annotate.revisions[0].author - assert_equal 'mkdir', annotate.lines[0] - end else puts "Bazaar test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end diff --git a/test/unit/repository_git_test.rb b/test/unit/repository_git_test.rb index acf4f174a86..621d0256510 100644 --- a/test/unit/repository_git_test.rb +++ b/test/unit/repository_git_test.rb @@ -62,6 +62,32 @@ def test_fetch_changesets_incremental @repository.fetch_changesets assert_equal 15, @repository.changesets.count end + + def test_identifier + @repository.fetch_changesets + @repository.reload + c = @repository.changesets.find_by_revision('7234cb2750b63f47bff735edc50a1c0a433c2518') + assert_equal c.scmid, c.identifier + end + + def test_format_identifier + @repository.fetch_changesets + @repository.reload + c = @repository.changesets.find_by_revision('7234cb2750b63f47bff735edc50a1c0a433c2518') + assert_equal c.format_identifier, '7234cb27' + end + + def test_activities + @repository.fetch_changesets + @repository.reload + f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1)) + f.scope = ['changesets'] + events = f.events + assert_kind_of Array, events + eve = events[-1] + assert eve.event_title.include?('7234cb27:') + assert_equal eve.event_url[:rev], '7234cb2750b63f47bff735edc50a1c0a433c2518' + end else puts "Git test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end diff --git a/test/unit/repository_subversion_test.rb b/test/unit/repository_subversion_test.rb index 903cdd049c4..cda6d5bdbe9 100644 --- a/test/unit/repository_subversion_test.rb +++ b/test/unit/repository_subversion_test.rb @@ -88,6 +88,57 @@ def test_directory_listing_with_square_brackets_in_base assert_equal 1, entries.size, 'Expect a single entry' assert_equal 'README.txt', entries.first.name end + + def test_identifier + @repository.fetch_changesets + @repository.reload + c = @repository.changesets.find_by_revision('1') + assert_equal c.revision, c.identifier + end + + def test_identifier_nine_digit + c = Changeset.new(:repository => @repository, :committed_on => Time.now, + :revision => '123456789', :comments => 'test') + assert_equal c.identifier, c.revision + end + + def test_format_identifier + @repository.fetch_changesets + @repository.reload + c = @repository.changesets.find_by_revision('1') + assert_equal c.format_identifier, c.revision + end + + def test_format_identifier_nine_digit + c = Changeset.new(:repository => @repository, :committed_on => Time.now, + :revision => '123456789', :comments => 'test') + assert_equal c.format_identifier, c.revision + end + + def test_activities + @repository.fetch_changesets + @repository.reload + f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1)) + f.scope = ['changesets'] + events = f.events + assert_kind_of Array, events + eve = events[-1] + assert eve.event_title.include?('1:') + assert_equal eve.event_url[:rev], '1' + end + + def test_activities_nine_digit + c = Changeset.new(:repository => @repository, :committed_on => Time.now, + :revision => '123456789', :comments => 'test') + assert( c.save ) + f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1)) + f.scope = ['changesets'] + events = f.events + assert_kind_of Array, events + eve = events[-1] + assert eve.event_title.include?('123456789:') + assert_equal eve.event_url[:rev], '123456789' + end else puts "Subversion test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end