# HG changeset patch # User Chris Cannam # Date 1280317201 -3600 # Node ID 7c48bad7d85d3749baf3e19862f3c6b887d1a640 # Parent b940d200fbd2e3c25bbc36d0987b31e80c151fce * Import Mercurial overhaul patches from Yuya Nishihara (see http://www.redmine.org/issues/4455) diff -r b940d200fbd2 -r 7c48bad7d85d app/helpers/application_helper.rb --- a/app/helpers/application_helper.rb Wed Jul 28 12:12:43 2010 +0100 +++ b/app/helpers/application_helper.rb Wed Jul 28 12:40:01 2010 +0100 @@ -99,8 +99,10 @@ # * :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 def toggle_link(name, id, options={}) @@ -612,7 +614,7 @@ 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 -r b940d200fbd2 -r 7c48bad7d85d app/helpers/repositories_helper.rb --- a/app/helpers/repositories_helper.rb Wed Jul 28 12:12:43 2010 +0100 +++ b/app/helpers/repositories_helper.rb Wed Jul 28 12:40:01 2010 +0100 @@ -18,9 +18,24 @@ require 'iconv' module RepositoriesHelper - def format_revision(txt) - txt.to_s[0,8] + # truncate rev to 8 chars if it's quite long + def truncate_long_revision_name(rev) + rev.to_s.size <= 12 ? rev.to_s : rev.to_s[0, 8] end + private :truncate_long_revision_name + + def format_revision(revision) + if [:identifier, :revision, :scmid].all? { |e| revision.respond_to? e } + if revision.scmid and revision.revision != revision.scmid and /[^\d]/ !~ revision.revision + "#{revision.revision}:#{revision.scmid}" # number:hashid + else + truncate_long_revision_name(revision.identifier) + end + else + truncate_long_revision_name(revision) + end + end + module_function :format_revision # callable as RepositoriesHelper.format_revision def truncate_at_line_break(text, length = 255) if text @@ -87,7 +102,7 @@ :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 +112,13 @@ :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 -r b940d200fbd2 -r 7c48bad7d85d app/models/changeset.rb --- a/app/models/changeset.rb Wed Jul 28 12:12:43 2010 +0100 +++ b/app/models/changeset.rb Wed Jul 28 12:40:01 2010 +0100 @@ -23,10 +23,10 @@ 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)} #{RepositoriesHelper.format_revision(o)}" + (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,22 @@ def revision=(r) write_attribute :revision, (r.nil? ? nil : r.to_s) end + + # Returns the identifier of this changeset. + # e.g. revision number for centralized system; hash id for DVCS + def identifier + scmid || revision + end + + # Returns the wiki identifier, "rN" or "commit:ABCDEF" + def wiki_identifier + if scmid # hash-like + "commit:#{scmid}" + else # numeric + "r#{revision}" + end + end + private :wiki_identifier def comments=(comment) write_attribute(:comments, Changeset.normalize_comments(comment)) @@ -109,11 +125,7 @@ issue.reload # don't change the status is the issue is closed next if issue.status.is_closed? - csettext = "r#{self.revision}" - if self.scmid && (! (csettext =~ /^r[0-9]+$/)) - csettext = "commit:\"#{self.scmid}\"" - end - journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext)) + journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, wiki_identifier)) issue.status = fix_status unless Setting.commit_fix_done_ratio.blank? issue.done_ratio = Setting.commit_fix_done_ratio.to_i diff -r b940d200fbd2 -r 7c48bad7d85d app/models/issue.rb --- a/app/models/issue.rb Wed Jul 28 12:12:43 2010 +0100 +++ b/app/models/issue.rb Wed Jul 28 12:40:01 2010 +0100 @@ -27,7 +27,7 @@ has_many :journals, :as => :journalized, :dependent => :destroy has_many :time_entries, :dependent => :delete_all - has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" + has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.id ASC" has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all diff -r b940d200fbd2 -r 7c48bad7d85d app/models/repository.rb --- a/app/models/repository.rb Wed Jul 28 12:12:43 2010 +0100 +++ b/app/models/repository.rb Wed Jul 28 12:40:01 2010 +0100 @@ -17,7 +17,7 @@ class Repository < ActiveRecord::Base belongs_to :project - has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" + has_many :changesets, :order => "#{Changeset.table_name}.id DESC" has_many :changes, :through => :changesets # Raw SQL to delete changesets and changes in the database @@ -94,7 +94,10 @@ # Finds and returns a revision with a number or the beginning of a hash def find_changeset_by_name(name) - changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%'])) + # TODO: is this query efficient enough? can we write as single query? + e = changesets.find(:first, :conditions => ['revision = ? OR scmid = ?', name.to_s, name.to_s]) + return e if e + changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) end def latest_changeset @@ -106,12 +109,11 @@ def latest_changesets(path, rev, limit=10) if path.blank? changesets.find(:all, :include => :user, - :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", :limit => limit) else changes.find(:all, :include => {:changeset => :user}, :conditions => ["path = ?", path.with_leading_slash], - :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", + :order => "#{Changeset.table_name}.id DESC", :limit => limit).collect(&:changeset) end end @@ -172,7 +174,11 @@ 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 diff -r b940d200fbd2 -r 7c48bad7d85d app/models/repository/git.rb --- a/app/models/repository/git.rb Wed Jul 28 12:12:43 2010 +0100 +++ b/app/models/repository/git.rb Wed Jul 28 12:40:01 2010 +0100 @@ -49,7 +49,7 @@ 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) + revisions = scm.revisions('', nil, nil, :all => true, :since => since, :reverse => true) return if revisions.nil? || revisions.empty? recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since]) @@ -75,7 +75,7 @@ "scmid IN (?)", revisions.map!{|c| c.scmid} ], - :order => 'committed_on DESC' + :order => 'id DESC' ) end end diff -r b940d200fbd2 -r 7c48bad7d85d app/models/repository/mercurial.rb --- a/app/models/repository/mercurial.rb Wed Jul 28 12:12:43 2010 +0100 +++ b/app/models/repository/mercurial.rb Wed Jul 28 12:40:01 2010 +0100 @@ -21,6 +21,8 @@ attr_protected :root_url validates_presence_of :url + FETCH_AT_ONCE = 100 # number of changesets to fetch at once + def scm_adapter Redmine::Scm::Adapters::MercurialAdapter end @@ -30,61 +32,64 @@ end def entries(path=nil, identifier=nil) - entries=scm.entries(path, identifier) - if entries - entries.each do |entry| - next unless entry.is_file? - # Set the filesize unless browsing a specific revision - if identifier.nil? - full_path = File.join(root_url, entry.path) - entry.size = File.stat(full_path).size if File.file?(full_path) - end - # Search the DB for the entry's last change - change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC") - if change - entry.lastrev.identifier = change.changeset.revision - entry.lastrev.name = change.changeset.revision - entry.lastrev.author = change.changeset.committer - entry.lastrev.revision = change.revision + scm.entries(path, identifier) + end + + def branches + bras = scm.branches + bras.sort unless bras == %w|default| + end + + # Returns the latest changesets for +path+ + def latest_changesets(path, rev, limit=10) + changesets.find(:all, :include => :user, + :conditions => latest_changesets_cond(path, rev, limit), + :limit => limit) + end + + def latest_changesets_cond(path, rev, limit) + cond, args = [], [] + + if scm.branchmap.member? rev + # dirty hack to filter by branch. branch name should be in database. + cond << "#{Changeset.table_name}.scmid IN (?)" + args << scm.nodes_in_branch(rev, path, rev, 0, :limit => limit) + elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil + cond << "#{Changeset.table_name}.id <= ?" + args << last.id + end + + unless path.blank? + # TODO: there must be a better way to build sub-query + cond << "EXISTS (SELECT * FROM #{Change.table_name} + WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id + AND (#{Change.table_name}.path = ? OR #{Change.table_name}.path LIKE ?))" + args << path.with_leading_slash << "#{path.with_leading_slash}/%" + end + + [cond.join(' AND '), *args] unless cond.empty? + end + private :latest_changesets_cond + + def fetch_changesets + scm_rev = scm.info.lastrev.revision.to_i + db_rev = latest_changeset ? latest_changeset.revision.to_i : -1 + return unless db_rev < scm_rev # already up-to-date + + logger.debug "Fetching changesets for repository #{url}" if logger + (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i| + transaction do + scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re| + cs = Changeset.create(:repository => self, + :revision => re.revision, + :scmid => re.scmid, + :committer => re.author, + :committed_on => re.time, + :comments => re.message) + re.paths.each { |e| cs.create_change(e) } end end end - entries - end - - def fetch_changesets - scm_info = scm.info - if scm_info - # latest revision found in database - db_revision = latest_changeset ? latest_changeset.revision.to_i : -1 - # latest revision in the repository - latest_revision = scm_info.lastrev - return if latest_revision.nil? - scm_revision = latest_revision.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 100 - identifier_to = [identifier_from + 99, scm_revision].min - revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true) - transaction do - revisions.each do |revision| - changeset = Changeset.create(:repository => self, - :revision => revision.identifier, - :scmid => revision.scmid, - :committer => revision.author, - :committed_on => revision.time, - :comments => revision.message) - - revision.paths.each do |change| - changeset.create_change(change) - end - end - end unless revisions.nil? - identifier_from = identifier_to + 1 - end - end - end + self end end diff -r b940d200fbd2 -r 7c48bad7d85d app/views/repositories/_dir_list_content.rhtml --- a/app/views/repositories/_dir_list_content.rhtml Wed Jul 28 12:12:43 2010 +0100 +++ b/app/views/repositories/_dir_list_content.rhtml Wed Jul 28 12:40:01 2010 +0100 @@ -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 -r b940d200fbd2 -r 7c48bad7d85d app/views/repositories/_revisions.rhtml --- a/app/views/repositories/_revisions.rhtml Wed Jul 28 12:12:43 2010 +0100 +++ b/app/views/repositories/_revisions.rhtml Wed Jul 28 12:40:01 2010 +0100 @@ -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 -r b940d200fbd2 -r 7c48bad7d85d app/views/repositories/annotate.rhtml --- a/app/views/repositories/annotate.rhtml Wed Jul 28 12:12:43 2010 +0100 +++ b/app/views/repositories/annotate.rhtml Wed Jul 28 12:40:01 2010 +0100 @@ -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 -r b940d200fbd2 -r 7c48bad7d85d app/views/repositories/revision.rhtml --- a/app/views/repositories/revision.rhtml Wed Jul 28 12:12:43 2010 +0100 +++ b/app/views/repositories/revision.rhtml Wed Jul 28 12:40:01 2010 +0100 @@ -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 %> diff -r b940d200fbd2 -r 7c48bad7d85d extra/mercurial/redminehelper.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/mercurial/redminehelper.py Wed Jul 28 12:40:01 2010 +0100 @@ -0,0 +1,91 @@ +# redminehelper: Redmine helper extension for Mercurial +# it's a draft to show a possible way to explore repository by the Redmine overhaul patch +# see: http://www.redmine.org/issues/4455 +# +# Copyright 2010 Alessio Franceschelli (alefranz.net) +# Copyright 2010 Yuya Nishihara +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''command to list revision of each file +''' + +import re, time +from mercurial import cmdutil, commands, node, error + +SPECIAL_TAGS = ('tip',) + +def rhsummary(ui, repo, **opts): + """output the summary of the repository""" + # see mercurial/commands.py:tip + ui.write(':tip: rev node\n') + tipctx = repo[len(repo) - 1] + ui.write('%d %s\n' % (tipctx.rev(), tipctx)) + + # see mercurial/commands.py:root + ui.write(':root: path\n') + ui.write(repo.root + '\n') + + # see mercurial/commands.py:tags + ui.write(':tags: rev node name\n') + for t, n in reversed(repo.tagslist()): + if t in SPECIAL_TAGS: + continue + try: + r = repo.changelog.rev(n) + except error.LookupError: + r = -1 + ui.write('%d %s %s\n' % (r, node.short(n), t)) + + # see mercurial/commands.py:branches + def iterbranches(): + for t, n in repo.branchtags().iteritems(): + yield t, n, repo.changelog.rev(n) + + ui.write(':branches: rev node name\n') + for t, n, r in sorted(iterbranches(), key=lambda e: e[2], reverse=True): + if repo.lookup(r) in repo.branchheads(t, closed=False): + ui.write('%d %s %s\n' % (r, node.short(n), t)) # only open branch + +def rhentries(ui, repo, path='', **opts): + """output the entries of the specified directory""" + rev = opts.get('rev') + pathprefix = (path.rstrip('/') + '/').lstrip('/') + + # TODO: clean up + dirs, files = {}, {} + mf = repo[rev].manifest() + for f in repo[rev]: + if not f.startswith(pathprefix): + continue + + name = re.sub(r'/.*', '', f[len(pathprefix):]) + if '/' in f[len(pathprefix):]: + dirs[name] = (name,) + else: + try: + fctx = repo.filectx(f, fileid=mf[f]) + ctx = fctx.changectx() + tm, tzoffset = ctx.date() + localtime = int(tm) + tzoffset - time.timezone + files[name] = (ctx.rev(), node.short(ctx.node()), localtime, + fctx.size(), name) + except LookupError: # TODO: when this occurs? + pass + + ui.write(':dirs: name\n') + for n, v in sorted(dirs.iteritems(), key=lambda e: e[0]): + ui.write(' '.join(v) + '\n') + + ui.write(':files: rev node time size name\n') + for n, v in sorted(files.iteritems(), key=lambda e: e[0]): + ui.write(' '.join(str(e) for e in v) + '\n') + + +cmdtable = { + 'rhsummary': (rhsummary, [], 'hg rhsummary'), + 'rhentries': (rhentries, + [('r', 'rev', '', 'show the specified revision')], + 'hg rhentries [path]'), +} diff -r b940d200fbd2 -r 7c48bad7d85d lib/redmine/scm/adapters/abstract_adapter.rb --- a/lib/redmine/scm/adapters/abstract_adapter.rb Wed Jul 28 12:12:43 2010 +0100 +++ b/lib/redmine/scm/adapters/abstract_adapter.rb Wed Jul 28 12:40:01 2010 +0100 @@ -271,7 +271,8 @@ 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,12 @@ self.branch = attributes[:branch] end + # Returns the identifier of this revision. + # e.g. revision number for centralized system; hash id for DVCS + def identifier + @identifier || scmid || revision + end + def save(repo) Changeset.transaction do changeset = Changeset.new( diff -r b940d200fbd2 -r 7c48bad7d85d lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl --- a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl Wed Jul 28 12:12:43 2010 +0100 +++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl Wed Jul 28 12:40:01 2010 +0100 @@ -9,4 +9,4 @@ file_copy = '{name|urlescape}\n' tag = '{tag|escape}\n' header='\n\n\n' -# footer="" \ No newline at end of file +footer='' diff -r b940d200fbd2 -r 7c48bad7d85d lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl --- a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl Wed Jul 28 12:12:43 2010 +0100 +++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl Wed Jul 28 12:40:01 2010 +0100 @@ -9,4 +9,4 @@ file_copy = '{name|urlescape}\n' tag = '{tag|escape}\n' header='\n\n\n' -# footer="" +footer='' diff -r b940d200fbd2 -r 7c48bad7d85d lib/redmine/scm/adapters/mercurial_adapter.rb --- a/lib/redmine/scm/adapters/mercurial_adapter.rb Wed Jul 28 12:12:43 2010 +0100 +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb Wed Jul 28 12:40:01 2010 +0100 @@ -16,6 +16,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'redmine/scm/adapters/abstract_adapter' +require 'rexml/document' module Redmine module Scm @@ -24,31 +25,34 @@ # Mercurial executable name HG_BIN = "hg" + HG_HELPER_EXT = "#{RAILS_ROOT}/extra/mercurial/redminehelper.py" TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" TEMPLATE_NAME = "hg-template" TEMPLATE_EXTENSION = "tmpl" + # raised if hg command exited with error, e.g. unknown revision. + class HgCommandAborted < CommandFailed; end + class << self def client_version - @@client_version ||= (hgversion || []) + @client_version ||= hgversion end def hgversion # The hg version is expressed either as a # release number (eg 0.9.5 or 1.0) or as a revision # id composed of 12 hexa characters. - theversion = hgversion_from_command_line - if theversion.match(/^\d+(\.\d+)+/) - theversion.split(".").collect(&:to_i) - end + hgversion_str.to_s.split('.').map { |e| e.to_i } end + private :hgversion - def hgversion_from_command_line - %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] + def hgversion_str + shellout("#{HG_BIN} --version") { |io| io.gets }.to_s[/\d+(\.\d+)+/] end + private :hgversion_str def template_path - @@template_path ||= template_path_for(client_version) + template_path_for(client_version) end def template_path_for(version) @@ -59,146 +63,202 @@ end "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" end + private :template_path_for end def info - cmd = "#{HG_BIN} -R #{target('')} root" - root_url = nil - shellout(cmd) do |io| - root_url = io.read - end - return nil if $? && $?.exitstatus != 0 - info = Info.new({:root_url => root_url.chomp, - :lastrev => revisions(nil,nil,nil,{:limit => 1}).last - }) - info - rescue CommandFailed - return nil + tip = summary['tip'].first + Info.new(:root_url => summary['root'].first['path'], + :lastrev => Revision.new(:identifier => tip['rev'].to_i, + :revision => tip['rev'], + :scmid => tip['node'])) + end + + def tags + summary['tags'].map { |e| e['name'] } end + # Returns map of {'tag' => 'nodeid', ...} + def tagmap + alist = summary['tags'].map { |e| e.values_at('name', 'node') } + Hash[*alist.flatten] + end + + def branches + summary['branches'].map { |e| e['name'] } + end + + # Returns map of {'branch' => 'nodeid', ...} + def branchmap + alist = summary['branches'].map { |e| e.values_at('name', 'node') } + Hash[*alist.flatten] + end + + # NOTE: DO NOT IMPLEMENT default_branch !! + # It's used as the default revision by RepositoriesController. + + def summary + @summary ||= fetchg 'rhsummary' + end + private :summary + def entries(path=nil, identifier=nil) - path ||= '' entries = Entries.new - cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate" - cmd << " -r " + (identifier ? identifier.to_s : "tip") - cmd << " " + shell_quote("path:#{path}") unless path.empty? - shellout(cmd) do |io| - io.each_line do |line| - # HG uses antislashs as separator on Windows - line = line.gsub(/\\/, "/") - if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'') - e ||= line - e = e.chomp.split(%r{[\/\\]}) - entries << Entry.new({:name => e.first, - :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"), - :kind => (e.size > 1 ? 'dir' : 'file'), - :lastrev => Revision.new - }) unless e.empty? || entries.detect{|entry| entry.name == e.first} + fetched_entries = fetchg('rhentries', '-r', hgrev(identifier), + without_leading_slash(path.to_s)) + + fetched_entries['dirs'].each do |e| + entries << Entry.new(:name => e['name'], + :path => "#{with_trailling_slash(path)}#{e['name']}", + :kind => 'dir') + end + + fetched_entries['files'].each do |e| + entries << Entry.new(:name => e['name'], + :path => "#{with_trailling_slash(path)}#{e['name']}", + :kind => 'file', + :size => e['size'].to_i, + :lastrev => Revision.new(:identifier => e['rev'].to_i, + :time => Time.at(e['time'].to_i))) + end + + entries + rescue HgCommandAborted + nil # means not found + end + + # TODO: is this api necessary? + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + revisions = Revisions.new + each_revision { |e| revisions << e } + revisions + end + + # Iterates the revisions by using a template file that + # makes Mercurial produce a xml output. + def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) + hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] + hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" + hg_args << '--limit' << options[:limit] if options[:limit] + hg_args << without_leading_slash(path) unless path.blank? + doc = hg(*hg_args) { |io| REXML::Document.new(io.read) } + # TODO: ??? HG doesn't close the XML Document... + + doc.each_element('log/logentry') do |le| + cpalist = le.get_elements('paths/path-copied').map do |e| + [e.text, e.attributes['copyfrom-path']] + end + cpmap = Hash[*cpalist.flatten] + + paths = le.get_elements('paths/path').map do |e| + {:action => e.attributes['action'], :path => with_leading_slash(e.text), + :from_path => (cpmap.member?(e.text) ? with_leading_slash(cpmap[e.text]) : nil), + :from_revision => (cpmap.member?(e.text) ? le.attributes['revision'] : nil)} + end.sort { |a, b| a[:path] <=> b[:path] } + + yield Revision.new(:identifier => le.attributes['revision'], + :revision => le.attributes['revision'], + :scmid => le.attributes['node'], + :author => (le.elements['author'].text rescue ''), + :time => Time.parse(le.elements['date'].text).localtime, + :message => le.elements['msg'].text, + :paths => paths) + end + self + end + + # Returns list of nodes in the specified branch + def nodes_in_branch(branch, path=nil, identifier_from=nil, identifier_to=nil, options={}) + hg_args = ['log', '--template', '{node|short}\n', '-b', branch] + hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" + hg_args << '--limit' << options[:limit] if options[:limit] + hg_args << without_leading_slash(path) unless path.blank? + hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } } + end + + def diff(path, identifier_from, identifier_to=nil) + hg_args = ['diff', '--nodates'] + if identifier_to + hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) + else + hg_args << '-c' << hgrev(identifier_from) + end + hg_args << without_leading_slash(path) unless path.blank? + + hg *hg_args do |io| + io.collect + end + rescue HgCommandAborted + nil # means not found + end + + def cat(path, identifier=nil) + hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io| + io.binmode + io.read + end + rescue HgCommandAborted + nil # means not found + end + + def annotate(path, identifier=nil) + blame = Annotate.new + hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io| + io.each do |line| + next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$} + r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3) + blame.add_line($4.rstrip, r) + end + end + blame + rescue HgCommandAborted + nil # means not found or cannot be annotated + end + + # Runs 'hg' command with the given args + def hg(*args, &block) + full_args = [HG_BIN, '--cwd', url] + full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" + full_args += args + ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block) + if $? && $?.exitstatus != 0 + raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}" + end + ret + end + private :hg + + # Runs 'hg' helper, then parses output to return + def fetchg(*args) + # command output example: + # :tip: rev node + # 100 abcdef012345 + # :tags: rev node name + # 100 abcdef012345 tip + # ... + data = Hash.new { |h, k| h[k] = [] } + hg(*args) do |io| + key, attrs = nil, nil + io.each do |line| + next if line.chomp.empty? + if /^:(\w+): ([\w ]+)/ =~ line + key = $1 + attrs = $2.split(/ /) + elsif key + alist = attrs.zip(line.chomp.split(/ /, attrs.size)) + data[key] << Hash[*alist.flatten] end end end - return nil if $? && $?.exitstatus != 0 - entries.sort_by_name + data end - - # Fetch the revisions by using a template file that - # makes Mercurial produce a xml output. - def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) - revisions = Revisions.new - cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}" - if identifier_from && identifier_to - cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}" - elsif identifier_from - cmd << " -r #{identifier_from.to_i}:" - end - cmd << " --limit #{options[:limit].to_i}" if options[:limit] - cmd << " #{path}" if path - shellout(cmd) do |io| - begin - # HG doesn't close the XML Document... - doc = REXML::Document.new(io.read << "") - doc.elements.each("log/logentry") do |logentry| - paths = [] - copies = logentry.get_elements('paths/path-copied') - logentry.elements.each("paths/path") do |path| - # Detect if the added file is a copy - if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text } - from_path = c.attributes['copyfrom-path'] - from_rev = logentry.attributes['revision'] - end - paths << {:action => path.attributes['action'], - :path => "/#{path.text}", - :from_path => from_path ? "/#{from_path}" : nil, - :from_revision => from_rev ? from_rev : nil - } - end - paths.sort! { |x,y| x[:path] <=> y[:path] } - - revisions << Revision.new({:identifier => logentry.attributes['revision'], - :scmid => logentry.attributes['node'], - :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""), - :time => Time.parse(logentry.elements['date'].text).localtime, - :message => logentry.elements['msg'].text, - :paths => paths - }) - end - rescue - logger.debug($!) - end - end - return nil if $? && $?.exitstatus != 0 - revisions + private :fetchg + + # Returns correct revision identifier + def hgrev(identifier) + identifier.blank? ? 'tip' : identifier.to_s end - - def diff(path, identifier_from, identifier_to=nil) - path ||= '' - if identifier_to - identifier_to = identifier_to.to_i - else - identifier_to = identifier_from.to_i - 1 - end - cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates" - cmd << " -I #{target(path)}" unless path.empty? - diff = [] - shellout(cmd) do |io| - io.each_line do |line| - diff << line - end - end - return nil if $? && $?.exitstatus != 0 - diff - end - - def cat(path, identifier=nil) - cmd = "#{HG_BIN} -R #{target('')} cat" - cmd << " -r " + (identifier ? identifier.to_s : "tip") - cmd << " #{target(path)}" - cat = nil - shellout(cmd) do |io| - io.binmode - cat = io.read - end - return nil if $? && $?.exitstatus != 0 - cat - end - - def annotate(path, identifier=nil) - path ||= '' - cmd = "#{HG_BIN} -R #{target('')}" - cmd << " annotate -n -u" - cmd << " -r " + (identifier ? identifier.to_s : "tip") - cmd << " -r #{identifier.to_i}" if identifier - cmd << " #{target(path)}" - blame = Annotate.new - shellout(cmd) do |io| - io.each_line do |line| - next unless line =~ %r{^([^:]+)\s(\d+):(.*)$} - blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip)) - end - end - return nil if $? && $?.exitstatus != 0 - blame - end + private :hgrev end end end