# 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 %> |
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 %> |
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