# HG changeset patch # User Alessio Caiazza # Date 1268857262 -3600 # Node ID 7f692581605fef75ca01e31c87730e22e706cb77 # Parent 40f2607efd173437c7df45c958f5452fe0f6cb04 Imported marutosi's work and added a bare hgrc support Summarized marutosi patches till http://github.com/marutosi/redmine/commit/765fd0b3dbba8b8282d03a9edc157a581b91c84f from http://github.com/marutosi/redmine/tree/hg-overhaul-0.9 diff -r 40f2607efd173437c7df45c958f5452fe0f6cb04 -r 7f692581605fef75ca01e31c87730e22e706cb77 add_ini_support.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/add_ini_support.diff Wed Mar 17 21:21:02 2010 +0100 @@ -0,0 +1,273 @@ +# HG changeset patch +# Parent 00c97ea02408516a73abc3886e932c3c0928be83 +Add INI file parser for .hgrc + +diff --git a/lib/ini.rb b/lib/ini.rb +new file mode 100644 +--- /dev/null ++++ b/lib/ini.rb +@@ -0,0 +1,263 @@ ++# ++# ini.rb - read and write ini files ++# ++# Copyright (C) 2007 Jeena Paradies ++# License: GPL ++# Author: Jeena Paradies (info@jeenaparadies.net) ++# ++# == Overview ++# ++# This file provides a read-wite handling for ini files. ++# The data of a ini file is represented by a object which ++# is populated with strings. ++ ++class Ini ++ ++ # Class with methods to read from and write into ini files. ++ # ++ # A ini file is a text file in a specific format, ++ # it may include several fields which are sparated by ++ # field headlines which are enclosured by "[]". ++ # Each field may include several key-value pairs. ++ # ++ # Each key-value pair is represented by one line and ++ # the value is sparated from the key by a "=". ++ # ++ # == Examples ++ # ++ # === Example ini file ++ # ++ # # this is the first comment which will be saved in the comment attribute ++ # mail=info@example.com ++ # domain=example.com # this is a comment which will not be saved ++ # [database] ++ # db=example ++ # user=john ++ # passwd=very-secure ++ # host=localhost ++ # # this is another comment ++ # [filepaths] ++ # tmp=/tmp/example ++ # lib=/home/john/projects/example/lib ++ # htdocs=/home/john/projects/example/htdocs ++ # [ texts ] ++ # wellcome=Wellcome on my new website! ++ # Website description = This is only a example. # and another comment ++ # ++ # === Example object ++ # ++ # A Ini#comment stores: ++ # "this is the first comment which will be saved in the comment attribute" ++ # ++ # A Ini object stores: ++ # ++ # { ++ # "mail" => "info@example.com", ++ # "domain" => "example.com", ++ # "database" => { ++ # "db" => "example", ++ # "user" => "john", ++ # "passwd" => "very-secure", ++ # "host" => "localhost" ++ # }, ++ # "filepaths" => { ++ # "tmp" => "/tmp/example", ++ # "lib" => "/home/john/projects/example/lib", ++ # "htdocs" => "/home/john/projects/example/htdocs" ++ # } ++ # "texts" => { ++ # "wellcome" => "Wellcome on my new website!", ++ # "Website description" => "This is only a example." ++ # } ++ # } ++ # ++ # As you can see this module gets rid of all comments, linebreaks ++ # and unnecessary spaces at the beginning and the end of each ++ # field headline, key or value. ++ # ++ # === Using the object ++ # ++ # Using the object is stright forward: ++ # ++ # ini = Ini.new("path/settings.ini") ++ # ini["mail"] = "info@example.com" ++ # ini["filepaths"] = { "tmp" => "/tmp/example" } ++ # ini.comment = "This is\na comment" ++ # puts ini["filepaths"]["tmp"] ++ # # => /tmp/example ++ # ini.update() ++ # ++ ++ # ++ # :inihash is a hash which holds all ini data ++ # :comment is a string which holds the comments on the top of the file ++ # ++ attr_accessor :inihash, :comment ++ ++ # ++ # Creating a new Ini object ++ # ++ # +path+ is a path to the ini file ++ # +load+ if nil restores the data if possible ++ # if true restores the data, if not possible raises an error ++ # if false does not resotre the data ++ # ++ def initialize(path, load=nil) ++ @path = path ++ @inihash = {} ++ ++ if load or ( load.nil? and FileTest.readable_real? @path ) ++ restore() ++ end ++ end ++ ++ # ++ # Retrive the ini data for the key +key+ ++ # ++ def [](key) ++ @inihash[key] ++ end ++ ++ # ++ # Set the ini data for the key +key+ ++ # ++ def []=(key, value) ++ raise TypeError, "String expected" unless key.is_a? String ++ raise TypeError, "String or Hash expected" unless value.is_a? String or value.is_a? Hash ++ ++ @inihash[key] = value ++ end ++ ++ # ++ # Restores the data from file into the object ++ # ++ def restore() ++ @inihash = Ini.read_from_file(@path) ++ @comment = Ini.read_comment_from_file(@path) ++ end ++ ++ # ++ # Store data from the object in the file ++ # ++ def update() ++ Ini.write_to_file(@path, @inihash, @comment) ++ end ++ ++ # ++ # Reading data from file ++ # ++ # +path+ is a path to the ini file ++ # ++ # returns a hash which represents the data from the file ++ # ++ def Ini.read_from_file(path) ++ ++ inihash = {} ++ headline = nil ++ ++ IO.foreach(path) do |line| ++ ++ line = line.strip.split(/#/)[0] ++ ++ # read it only if the line doesn't begin with a "=" and is long enough ++ unless line.length < 2 and line[0,1] == "=" ++ ++ # it's a headline if the line begins with a "[" and ends with a "]" ++ if line[0,1] == "[" and line[line.length - 1, line.length] == "]" ++ ++ # get rid of the [] and unnecessary spaces ++ headline = line[1, line.length - 2 ].strip ++ inihash[headline] = {} ++ else ++ ++ key, value = line.split(/=/, 2) ++ ++ key = key.strip unless key.nil? ++ value = value.strip unless value.nil? ++ ++ unless headline.nil? ++ inihash[headline][key] = value ++ else ++ inihash[key] = value unless key.nil? ++ end ++ end ++ end ++ end ++ ++ inihash ++ end ++ ++ # ++ # Reading comments from file ++ # ++ # +path+ is a path to the ini file ++ # ++ # Returns a string with comments from the beginning of the ++ # ini file. ++ # ++ def Ini.read_comment_from_file(path) ++ comment = "" ++ ++ IO.foreach(path) do |line| ++ line.strip! ++ break unless line[0,1] == "#" or line == "" ++ ++ comment << "#{line[1, line.length ].strip}\n" ++ end ++ ++ comment ++ end ++ ++ # ++ # Writing a ini hash into a file ++ # ++ # +path+ is a path to the ini file ++ # +inihash+ is a hash representing the ini File. Default is a empty hash. ++ # +comment+ is a string with comments which appear on the ++ # top of the file. Each line will get a "#" before. ++ # Default is no comment. ++ # ++ def Ini.write_to_file(path, inihash={}, comment=nil) ++ raise TypeError, "String expected" unless comment.is_a? String or comment.nil? ++ ++ raise TypeError, "Hash expected" unless inihash.is_a? Hash ++ File.open(path, "w") { |file| ++ ++ unless comment.nil? ++ comment.each do |line| ++ file << "# #{line}" ++ end ++ end ++ ++ file << Ini.to_s(inihash) ++ } ++ end ++ ++ # ++ # Turn a hash (up to 2 levels deepness) into a ini string ++ # ++ # +inihash+ is a hash representing the ini File. Default is a empty hash. ++ # ++ # Returns a string in the ini file format. ++ # ++ def Ini.to_s(inihash={}) ++ str = "" ++ ++ inihash.each do |key, value| ++ ++ if value.is_a? Hash ++ str << "[#{key.to_s}]\n" ++ ++ value.each do |under_key, under_value| ++ str << "#{under_key.to_s}=#{under_value.to_s unless under_value.nil?}\n" ++ end ++ ++ else ++ str << "#{key.to_s}=#{value.to_s unless value.nil?}\n" ++ end ++ end ++ ++ str ++ end ++ ++end +\ No newline at end of file diff -r 40f2607efd173437c7df45c958f5452fe0f6cb04 -r 7f692581605fef75ca01e31c87730e22e706cb77 marutosi.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/marutosi.diff Wed Mar 17 21:21:02 2010 +0100 @@ -0,0 +1,2057 @@ +# HG changeset patch +# Parent afc53258c0b8992af1e3aa5960d0df9fab5ec358 +Summarized marutosi patches till +http://github.com/marutosi/redmine/commit/765fd0b3dbba8b8282d03a9edc157a581b91c84f +from http://github.com/marutosi/redmine/tree/hg-overhaul-0.9 + +diff --git a/.gitignore b/.gitignore +--- a/.gitignore ++++ b/.gitignore +@@ -17,3 +17,11 @@ + /tmp/sockets/* + /tmp/test/* + /vendor/rails ++ ++/extra/mercurial/size.pyo ++/extra/mercurial/size.pyc ++ ++/.hg/ ++/.hgignore ++/.hgtags ++ +diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb +--- a/app/controllers/repositories_controller.rb ++++ b/app/controllers/repositories_controller.rb +@@ -66,7 +66,7 @@ + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository' + end + +- def show ++ def show + @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty? + + @entries = @repository.entries(@path, @rev) +@@ -122,12 +122,14 @@ + @content.gsub!("\r\n", "\n") + end + end +- ++ + def annotate + @entry = @repository.entry(@path, @rev) + (show_error_not_found; return) unless @entry + +- @annotate = @repository.scm.annotate(@path, @rev) ++ # Redmine 0.9.x Mercurial adapter has revision number. ++ # @annotate = @repository.scm.annotate(@path, @rev) ++ @annotate = @repository.annotate(@path, @rev) + (render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty? + end + +diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb +--- a/app/helpers/repositories_helper.rb ++++ b/app/helpers/repositories_helper.rb +@@ -158,13 +158,27 @@ + def darcs_field_tags(form, repository) + content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?))) + end +- ++ + def mercurial_field_tags(form, repository) +- content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) ++ content_tag('p', form.text_field( ++ :url, ++ :label => 'Root directory', ++ :size => 60, ++ :required => true, ++ ## Mercurial repository is removable. ++ # :disabled => (repository && !repository.root_url.blank?) ++ :disabled => false ++ )) + end + + def git_field_tags(form, repository) +- content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) ++ content_tag('p', form.text_field( ++ :url, ++ :label => 'Path to .git directory', ++ :size => 60, ++ :required => true, ++ :disabled => (repository && !repository.root_url.blank?) ++ )) + end + + def cvs_field_tags(form, repository) +diff --git a/app/models/repository.rb b/app/models/repository.rb +--- a/app/models/repository.rb ++++ b/app/models/repository.rb +@@ -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}.scm_order DESC, #{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" + has_many :changes, :through => :changesets + + # Raw SQL to delete changesets and changes in the database +@@ -30,6 +30,9 @@ + # Removes leading and trailing whitespace + def url=(arg) + write_attribute(:url, arg ? arg.to_s.strip : nil) ++ write_attribute(:root_url, nil) ++ @scm = nil ++ scm.url + end + + # Removes leading and trailing whitespace +@@ -86,17 +89,21 @@ + def diff(path, rev, rev_to) + scm.diff(path, rev, rev_to) + end +- ++ ++ def annotate(path, identifier=nil) ++ scm.annotate(path, identifier) ++ end ++ + # Returns a path relative to the url of the repository + def relative_path(path) + path + end +- ++ + # 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 + '%'])) + end +- ++ + def latest_changeset + @latest_changeset ||= changesets.find(:first) + end +@@ -105,17 +112,18 @@ + # Default behaviour is to search in cached changesets + def latest_changesets(path, rev, limit=10) + if path.blank? ++ # this is defined at "has_many" ++ # :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", + 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}.scm_order DESC, #{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", + :limit => limit).collect(&:changeset) + end + end +- ++ + def scan_changesets_for_issue_ids + self.changesets.each(&:scan_comment_for_issue_ids) + end +diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb +--- a/app/models/repository/mercurial.rb ++++ b/app/models/repository/mercurial.rb +@@ -18,77 +18,364 @@ + require 'redmine/scm/adapters/mercurial_adapter' + + class Repository::Mercurial < Repository +- attr_protected :root_url ++ attr_protected :root_url + validates_presence_of :url + ++ @@limit_check_strip = 100 ++ @@num_convert_redmine_0_9 = 20 ++ + def scm_adapter + Redmine::Scm::Adapters::MercurialAdapter + end +- ++ + def self.scm_name + 'Mercurial' + end +- ++ ++ def branches ++ brs = scm.branches ++ # if brs.size <= 1 ++ if false ++ nil ++ else ++ brs ++ end ++ end ++ ++ def tags ++ scm.tags ++ end ++ ++ def get_branch_or_tag_or_scmid(identifier) ++ return nil if identifier.nil? ++ return identifier if ( branches && branches.index(identifier) ) ++ return identifier if tags.index(identifier) ++ ident1 = find_changeset_by_name(identifier) ++ ident2 = nil ++ ident2 = ident1.scmid if ident1 ++ return ident2 ++ end ++ ++ def entry(path=nil, identifier=nil) ++ scm.entry(path, get_branch_or_tag_or_scmid(identifier)) ++ end ++ ++ # TODO: ++ # This process is very heavy. ++ # We need "named_branch" field on Changesets table for performance. ++ # But, we do not need for single named branch? ++ # Closed branch... + def entries(path=nil, identifier=nil) +- entries=scm.entries(path, identifier) ++ branch = nil ++ if ( branches && branches.index(identifier) ) ++ branch = identifier ++ end ++ entries=scm.entries( ++ path, ++ get_branch_or_tag_or_scmid(identifier), ++ :include_file_revs => true , ++ :branch => branch ++ ) + 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 ++ if entry && entry.lastrev && entry.lastrev.identifier ++ rev = nil ++ if false ++ rev = changesets.find( ++ :first, ++ :conditions => [ "revision = ?" , entry.lastrev.identifier ] ++ ) ++ next if rev ++ end ++ rev = changesets.find( ++ :first, ++ :conditions => ["scmid LIKE ?", entry.lastrev.identifier + '%'] ++ ) ++ entry.lastrev.identifier = rev.revision if rev + end + end + end + entries + end + ++ # TODO: ++ # This logic fails in following case. ++ # ++ # Before ++ # ++ # /-C ++ # A-------B ++ # ++ # After ++ # ++ # /-D ++ # A-------B ++ # ++ # This is very very rare case. ++ # ++ # For this case, we need to store HEAD info on DB? ++ # http://www.redmine.org/issues/4773#note-11 ++ # + 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| +- Change.create(:changeset => changeset, +- :action => change[:action], +- :path => change[:path], +- :from_path => change[:from_path], +- :from_revision => change[:from_revision]) ++ # return if ( scm_info.nil? ) ++ return if ( scm_info.nil? || ( scm_info && scm_info.lastrev.nil? ) ) ++ ++ # last_rev = scm.lastrev('','tip','tip') ++ # return if ( last_rev.nil? ) ++ ++ transaction do ++ Changeset.update_all( ++ "scm_order = -1" , ++ ["repository_id = ? AND scm_order is null", id] ++ ) ++ end ++ ++ identifier_from = 0 ++ ++ transaction do ++ tip_on_db = changesets.find(:first, :order => 'scm_order DESC') ++ tip_on_db = convert_changeset(tip_on_db) if ( tip_on_db && ( tip_on_db.scm_order == -1 ) ) ++ tip_revno_on_db = -1 ++ hg_revno_dbtip = -1 ++ if tip_on_db ++ tip_revno_on_db = tip_on_db.scm_order ++ revs = scm.revisions( nil, tip_on_db.scmid, tip_on_db.scmid, ++ :lite => true) ++ hg_revno_dbtip = revs.first.scm_order if revs && revs.first ++ end ++ ++ # Redmine cannot check changeset.count == scm.num_revisions ++ # because Redmine ver.0.9.x has stripped revision on DB ++ # and 'revision' is uniq. ++ if ( tip_revno_on_db == hg_revno_dbtip ) ++ identifier_from = tip_revno_on_db + 1 ++ else ++ # At Redmine SVN r3394, git check history editing for only one week. ++ # Mercurial has revision number, ++ # so Redmine can check from big revision number. ++ # And strip revision in middle of history on shared repository ++ # is very rare case. ++ ver09x_changeset_first = ++ changesets.find(:first,:conditions =>[ "scm_order = -1" ] ) ++ if ( ver09x_changeset_first ) ++ convert_changesets(@@num_convert_redmine_0_9) ++ end ++ converted_cs = nil ++ flag_loop_break = false ++ convert_limit = @@limit_check_strip ++ changesets.find( ++ :all, ++ :order => 'scm_order DESC', ++ :limit => convert_limit).each do |cs| ++ prev_no = cs.scm_order ++ converted_cs = convert_changeset(cs) ++ if ( converted_cs && converted_cs.scm_order ) ++ if ( converted_cs.scm_order == prev_no ) ++ changesets.find(:all,:conditions => ++ [ "scm_order = ? AND scmid != ?" , cs.scm_order , cs.scmid ] ++ ).each do |cs1| ++ convert_changeset(cs1) + end ++ flag_loop_break = true ++ break + end +- end unless revisions.nil? +- identifier_from = identifier_to + 1 ++ end ++ end ++ if ( flag_loop_break ) ++ identifier_from = converted_cs.scm_order + 1 ++ else ++ identifier_from = [scm.num_revisions - convert_limit , 0].max + end + end + end ++ ++ scm_revision = scm.num_revisions - 1 ++ # Reffered from Subversion logic. ++ while (identifier_from <= scm_revision) ++ transaction do ++ identifier_to = [identifier_from + 19, scm_revision].min ++ revisions = scm.revisions( ++ nil, identifier_from, identifier_to ) ++ if revisions ++ revisions.each do |rev| ++ dups = changesets.find( ++ :all, ++ :conditions =>["scmid = ? or scmid = ? " , ++ rev.scmid , rev.identifier] ++ ) ++ unless dups.empty? ++ dups.each do |cs1| ++ ## There is no way to store ++ ## Redmine 0.9.x original revision number. ++ cs1.scmid = rev.scmid ++ cs1.scm_order = rev.scm_order.to_i ++ cs1.save ++ end ++ next ++ end ++ rev.save(self) ++ end ++ end ++ identifier_from = identifier_to + 1 ++ end ++ end ++ end ++ ++ # TODO: ++ # 1. Mercurial has Named branches. ++ # http://mercurial.selenic.com/wiki/NamedBranches ++ # a) ++ # Mercurial has --only-branch option. ++ # But, this is show this branch only. ++ # TortoiseHg 0.9 Repository Explorer is different. ++ # How does TortoiseHg handle branches? ++ # b) ++ # If no changeset on this named branch, ++ # no revisons show on repository tab. ++ # c) ++ # Mercurial version 1.2 introduced the ability to close a branch. ++ # http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches ++ # ++ # 2. ++ # If Setting.autofetch_changesets is off, ++ # no revisons show on repository tab. ++ # ++ def latest_changesets(path, rev, limit=10) ++ branch = nil ++ if ( branches && branches.index(rev) ) ++ branch = rev ++ end ++ revisions = scm.revisions( ++ path, ++ get_branch_or_tag_or_scmid(rev), ++ 0, ++ :limit => limit, ++ :lite => true , ++ :branch => branch ++ ) ++ ++ return [] if revisions.nil? or revisions.empty? ++ ++ # Redmine 0.9.x changeset scmid is short id. ++ changesets.find( ++ :all, ++ :conditions => [ ++ "scmid IN (?) or scmid IN (?)", ++ # revisions.map!{|c| c.scmid} , ++ revisions.map{|c| c.scmid} , ++ revisions.map{|c| c.identifier} ++ ] ++ ) ++ end ++ ++ def cat(path, identifier=nil) ++ scm.cat(path, get_branch_or_tag_or_scmid(identifier)) ++ end ++ ++ def diff(path, rev, rev_to) ++ from_rev = get_branch_or_tag_or_scmid(rev) ++ return nil if from_rev.nil? ++ scm.diff( ++ path, ++ from_rev, ++ get_branch_or_tag_or_scmid(rev_to) ++ ) ++ end ++ ++ def annotate(path, identifier=nil) ++ scm.annotate(path, get_branch_or_tag_or_scmid(identifier)) ++ end ++ ++ # Redmine 0.9.x has revision with revision number. ++ # TODO: ++ # In case of strip, revision number is renumberd, ++ # this logic fails? ++ # We need to test with fixtures? ++ # And tag and branch can use '\d'??? ++ def find_changeset_by_name(name) ++ if name ++ ret = changesets.find( ++ :first, ++ :conditions => ++ ( ++ name.match(/^\d*$/) ? ++ ["revision = ?", name.to_s] : ++ ["scmid LIKE ?", name + '%'] ++ ) ++ ) ++ ret ++ else ++ nil ++ end ++ end ++ ++ # This method is not used now. ++ # But, I plan to use in rake task. ++ def convert_changesets_all ++ changesets.find_each( ++ :conditions => [ "scm_order = -1 or scm_order is null" ], ++ :batch_size => 100) do |cs| ++ convert_changeset(cs) ++ end ++ end ++ ++ def convert_changesets(limit=10) ++ changesets.find( ++ :all, ++ :conditions =>[ "scm_order = -1 or scm_order is null" ], ++ :limit => limit).each do |cs| ++ convert_changeset(cs) ++ end ++ end ++ ++ # Mercurial revision number is sequential from 0. ++ # And Mercurial has multipile heads. ++ # If one head in middle of history was stripped, ++ # revision number was renumbered. ++ # In this case, Redmine 0.9.x had duplicate scmid on DB. ++ # ++ # Because revision is uniq on table, convert fails. ++ # And "rNN" is written in Wiki, issue message, etc. ++ def convert_changeset(cs) ++ ret = cs ++ revs = scm.revisions(nil, cs.scmid, cs.scmid, :lite => true) ++ rev = nil ++ rev = revs.first if revs ++ if rev ++ ret = convert_changeset_with_revision(cs, rev) ++ else ++ cs.delete ++ ret = nil ++ end ++ ret ++ end ++ ++ def convert_changeset_with_revision(cs, rev) ++ cs.scm_order = rev.scm_order.to_i ++ if false ++ dup_first1 = changesets.find( ++ :first, ++ :conditions =>["revision = ?" , rev.identifier ] ++ ) ++ if dup_first1.nil? ++ then ++ ## There is no way to store ++ ## Redmine 0.9.x original revision number. ++ # cs.revision = rev.identifier if cs.revision != rev.identifier ++ end ++ end ++ dup_first = changesets.find( ++ :first, ++ :conditions =>["scmid = ?" , rev.scmid ] ++ ) ++ if dup_first.nil? ++ then ++ cs.scmid = rev.scmid ++ end ++ ++ cs.save ++ ret = cs ++ ret + end + end +diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml +--- a/app/views/repositories/revision.rhtml ++++ b/app/views/repositories/revision.rhtml +@@ -21,8 +21,15 @@ + +

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

+ +-

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

++

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

+ + <%= textilizable @changeset.comments %> + +diff --git a/db/migrate/20100222000000_add_scm_order_to_changesets.rb b/db/migrate/20100222000000_add_scm_order_to_changesets.rb +new file mode 100644 +--- /dev/null ++++ b/db/migrate/20100222000000_add_scm_order_to_changesets.rb +@@ -0,0 +1,12 @@ ++ ++class AddScmOrderToChangesets < ActiveRecord::Migration ++ def self.up ++ add_column :changesets, :scm_order, :integer, :null => true ++ add_index :changesets, :scm_order ++ end ++ ++ def self.down ++ remove_index :changesets, :scm_order ++ remove_column :changesets, :scm_order ++ end ++end +diff --git a/extra/mercurial/size.py b/extra/mercurial/size.py +new file mode 100644 +--- /dev/null ++++ b/extra/mercurial/size.py +@@ -0,0 +1,49 @@ ++# size.py - returns the size of the files in the ++# repository without checking them out ++# ++# Copyright 2008 Ian P. Cardenas ++# ++# This software may be used and distributed according to the terms ++# of the GNU General Public License, incorporated herein by reference. ++# ++# $Id$ ++# ++# Setup in hgrc: ++# ++# [extensions] ++# # enable extension ++# hgext.size = ++# # Run "hg help size" to get info on configuration. ++'''size information in local repositories ++''' ++ ++from mercurial import hg, cmdutil ++ ++def size(ui, repo, file1, *pats, **opts): ++ """output the size of files ++ returns the size of the files in a local repository ++ """ ++ ctx = repo[opts['rev']] ++ err = 1 ++ m = cmdutil.match(repo, (file1,) + pats, opts) ++ for abs in ctx.walk(m): ++ fp = cmdutil.make_file(repo, opts['output'], ctx.node(), pathname=abs) ++ size = ctx[abs].size() ++ formatted_size = "%d\n" % ( size ) ++ fp.write(formatted_size) ++ err = 0 ++ return err ++ ++walkopts = [ ++ ('I', 'include', [], 'include names matching the given patterns'), ++ ('X', 'exclude', [], 'exclude names matching the given patterns'), ++] ++ ++cmdtable = { ++ "size": (size, ++ [('o', 'output', '', 'print output to file with formatted name'), ++ ('r', 'rev', '', 'print the given revision'), ++ ] + walkopts, ++ 'hg size [OPTION]... FILE...') ++} ++ +diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb +--- a/lib/redmine/scm/adapters/abstract_adapter.rb ++++ b/lib/redmine/scm/adapters/abstract_adapter.rb +@@ -128,7 +128,7 @@ + def cat(path, identifier=nil) + return nil + end +- ++ + def with_leading_slash(path) + path ||= '' + (path[0,1]!="/") ? "/#{path}" : path +@@ -269,9 +269,10 @@ + }.last + end + end +- ++ ++ + class Revision +- attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch ++ attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch, :scm_order + + def initialize(attributes={}) + self.identifier = attributes[:identifier] +@@ -283,6 +284,7 @@ + self.paths = attributes[:paths] + self.revision = attributes[:revision] + self.branch = attributes[:branch] ++ self.scm_order = attributes[:scm_order] + end + + def save(repo) +@@ -293,8 +295,9 @@ + :scmid => scmid, + :committer => author, + :committed_on => time, +- :comments => message) +- ++ :comments => message , ++ :scm_order => scm_order ++ ) + if changeset.save + paths.each do |file| + Change.create( +@@ -306,7 +309,7 @@ + end + end + end +- ++ + class Annotate + attr_reader :lines, :revisions + +diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5-lite.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5-lite.tmpl +new file mode 100644 +--- /dev/null ++++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5-lite.tmpl +@@ -0,0 +1,8 @@ ++changeset = 'This template must be used with --debug option\n' ++changeset_quiet = 'This template must be used with --debug option\n' ++changeset_verbose = 'This template must be used with --debug option\n' ++changeset_debug = '\n{author|escape}\n{date|isodate}\n{desc|escape}\n{tags}\n\n' ++ ++tag = '{tag|escape}\n' ++header='\n\n\n' ++# footer="" +\ No newline at end of file +diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl +--- a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl ++++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl +@@ -1,7 +1,7 @@ + changeset = 'This template must be used with --debug option\n' + changeset_quiet = 'This template must be used with --debug option\n' + changeset_verbose = 'This template must be used with --debug option\n' +-changeset_debug = '\n{author|escape}\n{date|isodate}\n\n{files}{file_adds}{file_dels}{file_copies}\n{desc|escape}\n{tags}\n\n' ++changeset_debug = '\n{author|escape}\n{date|isodate}\n\n{files}{file_adds}{file_dels}{file_copies}\n{desc|escape}\n{tags}\n\n' + + file = '{file|escape}\n' + file_add = '{file_add|escape}\n' +diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-1.0-lite.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-1.0-lite.tmpl +new file mode 100644 +--- /dev/null ++++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0-lite.tmpl +@@ -0,0 +1,8 @@ ++changeset = 'This template must be used with --debug option\n' ++changeset_quiet = 'This template must be used with --debug option\n' ++changeset_verbose = 'This template must be used with --debug option\n' ++changeset_debug = '\n{author|escape}\n{date|isodate}\n\n{desc|escape}\n{tags}\n\n' ++ ++tag = '{tag|escape}\n' ++header='\n\n\n' ++# footer="" +diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl +--- a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl ++++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl +@@ -1,7 +1,7 @@ + changeset = 'This template must be used with --debug option\n' + changeset_quiet = 'This template must be used with --debug option\n' + changeset_verbose = 'This template must be used with --debug option\n' +-changeset_debug = '\n{author|escape}\n{date|isodate}\n\n{file_mods}{file_adds}{file_dels}{file_copies}\n{desc|escape}\n{tags}\n\n' ++changeset_debug = '\n{author|escape}\n{date|isodate}\n\n{file_mods}{file_adds}{file_dels}{file_copies}\n{desc|escape}\n{tags}\n\n' + + file_mod = '{file_mod|escape}\n' + file_add = '{file_add|escape}\n' +diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb +--- a/lib/redmine/scm/adapters/mercurial_adapter.rb ++++ b/lib/redmine/scm/adapters/mercurial_adapter.rb +@@ -16,24 +16,31 @@ + # 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 +- module Adapters ++ module Adapters + class MercurialAdapter < AbstractAdapter +- + # Mercurial executable name + HG_BIN = "hg" + TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" + TEMPLATE_NAME = "hg-template" + TEMPLATE_EXTENSION = "tmpl" +- ++ + class << self ++ @@limit_include_file_revs = 20 ++ @@show_file_size = true ++ @@has_size_ext = true ++ + def client_version +- @@client_version ||= (hgversion || []) ++ @@client_version ||= (hgversion || []) + end +- +- def hgversion ++ ++ # TODO: ++ # Mercurial version 1.2 introduced the ability to close a branch. ++ # http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches ++ 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. +@@ -42,46 +49,153 @@ + theversion.split(".").collect(&:to_i) + end + end +- ++ + def hgversion_from_command_line + %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] + end +- ++ + def template_path + @@template_path ||= template_path_for(client_version) + end +- +- def template_path_for(version) ++ ++ def lite_template_path ++ @@lite_template_path ||= template_path_for(client_version,'lite') ++ end ++ ++ def template_path_for(version,style=nil) + if ((version <=> [0,9,5]) > 0) || version.empty? + ver = "1.0" + else + ver = "0.9.5" + end +- "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" ++ if style ++ tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}-#{style}.#{TEMPLATE_EXTENSION}" ++ else ++ tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" ++ end ++ tmpl + end + end +- ++ ++ # Mercurial default branch is "default". ++ # But, Mercurial has multipile heads. ++ def default_branch ++ @default_branch ||= 'tip' ++ end ++ ++ def branches ++ @branches ||= get_branches ++ end ++ ++ # TODO: ++ # Mercurial version 1.2 introduced the ability to close a branch. ++ # http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches ++ def get_branches ++ branches = [] ++ cmd = "#{HG_BIN} -R #{target('')} branches" ++ shellout(cmd) do |io| ++ io.each_line do |line| ++ branches << line.chomp.match('^([^:]+[^\s]+)[\s]+[\d]+:.*$')[1] ++ end ++ end ++ branches ++ end ++ ++ def tags ++ @tags ||= get_tags ++ end ++ ++ def get_tags ++ tags = [] ++ cmd = "#{HG_BIN} -R #{target('')} tags -v" ++ shellout(cmd) do |io| ++ io.each_line do |line| ++ strs = line.chomp.match('^([^:]+[^\s]+)[\s]+[\d]+:(.*)$') ++ if strs[2] !~ /[\s]+local/ ++ tags << strs[1] ++ end ++ end ++ end ++ tags ++ end ++ ++ def info_simple ++ lrev = lastrev('',nil) ++ if lrev ++ Info.new(:root_url => url, :lastrev => lrev) ++ else ++ nil ++ end ++ end ++ ++ # This method needs for "entries" method. + def info +- cmd = "#{HG_BIN} -R #{target('')} root" +- root_url = nil ++ begin ++ cmd = "#{HG_BIN} -R #{target('')} root" ++ root_url = nil ++ shellout(cmd) do |io| ++ root_url = io.gets.chomp ++ end ++ return nil if $? && $?.exitstatus != 0 ++ info = Info.new( ++ { ++ :root_url => root_url.chomp, ++ # :lastrev => revisions(nil,nil,nil,{:limit => 1}).last ++ :lastrev => lastrev('',nil) ++ }) ++ info ++ rescue ++ nil ++ end ++ end ++ ++ def lastrev(path=nil, identifier=nil, options={}) ++ lastrev = nil ++ if options ++ lastrev = revisions( ++ path, ++ identifier, ++ 0, ++ :limit => 1, ++ :lite => true , ++ :branch => options[:branch] ++ ) ++ else ++ lastrev = revisions( ++ path, ++ identifier, ++ 0, ++ :limit => 1, ++ :lite => true ++ ) ++ end ++ return nil if ( lastrev.nil? || ( lastrev && lastrev.empty? ) ) ++ # lastrev.last ++ lastrev.first ++ end ++ ++ def num_revisions ++ num = 0 ++ cmd = "#{HG_BIN} -R #{target('')} log -r tip --template=#{shell_quote('{rev}\n')}" + shellout(cmd) do |io| +- root_url = io.gets ++ line = io.gets ++ if line.nil? ++ num = 0 ++ else ++ num = line.chomp.to_i + 1 ++ end ++ break + 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 ++ num + end +- +- def entries(path=nil, identifier=nil) ++ ++ def entries(path=nil, identifier=nil, options={}) + 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? ++ file_cnt = 0 + shellout(cmd) do |io| + io.each_line do |line| + # HG uses antislashs as separator on Windows +@@ -89,34 +203,82 @@ + 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} ++ unless e.empty? || ++ entries.detect{|entry| entry.name == e.first} ++ kind = (e.size > 1 ? 'dir' : 'file') ++ ent_path = (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}") ++ if ( kind == 'file' ) ++ file_cnt += 1 ++ end ++ entries << Entry.new( ++ { ++ :name => e.first , ++ :path => ent_path , ++ :kind => kind , ++ :size => nil , ++ :lastrev => nil ++ } ++ ) ++ end + end + end + end + return nil if $? && $?.exitstatus != 0 ++ ++ entries.each do |ent| ++ ent.lastrev = nil ++ # "hg log -l1 DIR" is VERY VERY HEAVY!! ++ if ( ent.kind == 'file' ) ++ if ( options && options[:include_file_revs] && ++ file_cnt < @@limit_include_file_revs ) ++ # Following process is very heavy. ++ ent.lastrev = lastrev(ent.path,identifier,options[:branch]) ++ if ( @@show_file_size ) ++ ent.size = size_from_ext(ent.path,identifier) if @@has_size_ext ++ if ( ent.size.nil? && ++ (identifier.to_s == default_branch || ++ identifier.to_s == 'tip') ) ++ full_path = info.root_url + '/' + ent.path ++ ent.size = File.stat(full_path).size if File.file?(full_path) ++ end ++ end ++ end ++ end ++ ent.lastrev = Revision.new if ent.lastrev.nil? ++ end + entries.sort_by_name + 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={}) ++ # ++ # TODO: ++ # Mercurial version 1.2 introduced the ability to close a branch. ++ # http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches ++ 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}" ++ cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} --cwd #{target('')} log" ++ if options[:lite] ++ cmd << " --style #{shell_quote self.class.lite_template_path}" ++ else ++ cmd << " -C --style #{shell_quote self.class.template_path}" ++ end + if identifier_from && identifier_to +- cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}" ++ cmd << " -r #{shell_quote(identifier_from.to_s)}:#{shell_quote(identifier_to.to_s)}" + elsif identifier_from +- cmd << " -r #{identifier_from.to_i}:" ++ cmd << " -r #{shell_quote(identifier_from.to_s)}:" ++ elsif identifier_to ++ cmd << " -r :#{shell_quote(identifier_to.to_s)}" + end + cmd << " --limit #{options[:limit].to_i}" if options[:limit] ++ cmd << " --only-branch #{options[:branch]}" if options[:branch] + cmd << " #{path}" if path + shellout(cmd) do |io| + begin + # HG doesn't close the XML Document... +- doc = REXML::Document.new(io.read << "") ++ output = io.read ++ return nil if output.empty? ++ doc = REXML::Document.new(output << "") + doc.elements.each("log/logentry") do |logentry| + paths = [] + copies = logentry.get_elements('paths/path-copied') +@@ -124,7 +286,7 @@ + # 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'] ++ from_rev = logentry.attributes['shortnode'] + end + paths << {:action => path.attributes['action'], + :path => "/#{path.text}", +@@ -132,15 +294,18 @@ + :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 +- }) ++ paths.sort! { |x,y| x[:path] <=> y[:path] } unless paths.empty? ++ revisions << Revision.new( ++ { ++ :identifier => logentry.attributes['shortnode'], ++ :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, ++ :scm_order => logentry.attributes['revision'].to_i , ++ } ++ ) + end + rescue + logger.debug($!) +@@ -149,15 +314,14 @@ + return nil if $? && $?.exitstatus != 0 + revisions + end +- ++ + def diff(path, identifier_from, identifier_to=nil) + path ||= '' + if identifier_to +- identifier_to = identifier_to.to_i ++ cmd = "#{HG_BIN} -R #{target('')} diff -r #{shell_quote(identifier_to.to_s)} -r #{shell_quote(identifier_from.to_s)} --nodates" + else +- identifier_to = identifier_from.to_i - 1 ++ cmd = "#{HG_BIN} -R #{target('')} diff -c #{identifier_from} --nodates" + 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| +@@ -168,11 +332,11 @@ + 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)}" ++ cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip")) ++ cmd << " #{target(path)}" unless path.empty? + cat = nil + shellout(cmd) do |io| + io.binmode +@@ -181,19 +345,38 @@ + return nil if $? && $?.exitstatus != 0 + cat + end +- ++ ++ def size_from_ext(path, identifier=nil) ++ return nil if path.nil? || ( path && path.empty? ) ++ cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} size" ++ cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip")) ++ cmd << " #{path}" ++ size = nil ++ shellout(cmd) do |io| ++ # size = io.read ++ size = io.gets.chomp ++ end ++ return nil if $? && $?.exitstatus != 0 ++ return size.to_i ++ end ++ ++ # TODO: ++ # hg annotate behavior small changes at Ver.1.5. ++ # http://mercurial.selenic.com/wiki/UpgradeNotes#A1.5:_Small_behavior_changes ++ # hg annotate now follows copies and renames by default, ++ # use --no-follow for old behavior. + def annotate(path, identifier=nil) + path ||= '' ++ identifier = 'tip' if identifier.blank? + 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)}" ++ cmd << " annotate -c -u" ++ cmd << " -r #{shell_quote(identifier.to_s)}" ++ cmd << " #{target(path)}" unless path.empty? + 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)) ++ next unless line =~ %r{^([^:]+)\s(\w+):(.*)$} ++ blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_s, :author => $1.strip)) + end + end + return nil if $? && $?.exitstatus != 0 +diff --git a/lib/tasks/testing.rake b/lib/tasks/testing.rake +--- a/lib/tasks/testing.rake ++++ b/lib/tasks/testing.rake +@@ -26,11 +26,26 @@ + system "svnadmin create #{repo_path}" + system "gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load #{repo_path}" + end +- +- (supported_scms - [:subversion]).each do |scm| ++ ++ desc "Creates a test mercurial repository" ++ task :mercurial => :create_dir do ++ repo_path = "tmp/test/mercurial_repository" ++ size_ext_path = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + "/extra/mercurial/size.py" ++ system "hg init #{repo_path}" ++ system "cp -f test/fixtures/repositories/mercurial/hgrc #{repo_path}/.hg/hgrc " ++ system "echo size = #{size_ext_path} >> #{repo_path}/.hg/hgrc " ++ ++ system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/default.r0.bundle" ++ system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/branch01.r1.bundle" ++ system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle" ++ system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/branch00.r1.bundle" ++ end ++ ++ (supported_scms - [:subversion, :mercurial]).each do |scm| + desc "Creates a test #{scm} repository" + task scm => :create_dir do +- system "gunzip < test/fixtures/repositories/#{scm}_repository.tar.gz | tar -xv -C tmp/test" ++ # system "gunzip < test/fixtures/repositories/#{scm}_repository.tar.gz | tar -xv -C tmp/test" ++ system "tar -xvz -C tmp/test -f test/fixtures/repositories/#{scm}_repository.tar.gz" + end + end + +diff --git a/test/fixtures/changes.yml b/test/fixtures/changes.yml +--- a/test/fixtures/changes.yml ++++ b/test/fixtures/changes.yml +@@ -20,4 +20,66 @@ + path: /test/some/path/in/the/repo + from_path: + from_revision: +- +\ No newline at end of file ++changes_004: ++ changeset_id: 110 ++ action: A ++ id: 4 ++ revision: ++ branch: ++ from_path: ++ path: /README ++ from_revision: ++changes_005: ++ changeset_id: 110 ++ action: A ++ id: 5 ++ revision: ++ branch: ++ from_path: ++ path: /images/delete.png ++ from_revision: ++changes_006: ++ changeset_id: 110 ++ action: A ++ id: 6 ++ revision: ++ branch: ++ from_path: ++ path: /sources/watchers_controller.rb ++ from_revision: ++changes_007: ++ changeset_id: 111 ++ action: A ++ id: 7 ++ revision: ++ branch: ++ from_path: ++ path: /branch00-dir/hg-log.txt ++ from_revision: ++changes_008: ++ changeset_id: 112 ++ action: M ++ id: 8 ++ revision: ++ branch: ++ from_path: ++ path: /README ++ from_revision: ++changes_009: ++ changeset_id: 112 ++ action: A ++ id: 9 ++ revision: ++ branch: ++ from_path: ++ path: /images/edit.png ++ from_revision: ++changes_010: ++ changeset_id: 112 ++ action: A ++ id: 10 ++ revision: ++ branch: ++ from_path: ++ path: /sources/welcome_controller.rb ++ from_revision: +diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml +--- a/test/fixtures/changesets.yml ++++ b/test/fixtures/changesets.yml +@@ -101,4 +101,38 @@ + user_id: 3 + repository_id: 10 + committer: dlopper +- +\ No newline at end of file ++changesets_011: ++ commit_date: 2007-12-14 ++ committed_on: 2007-12-14 18:22:00 +09:00 ++ comments: |- ++ Initial import. ++ The repository contains 3 files. ++ id: 110 ++ revision: "0" ++ scm_order: ++ scmid: 0885933ad4f6 ++ user_id: 2 ++ repository_id: 12 ++ committer: jsmith ++changesets_012: ++ commit_date: 2006-01-01 ++ committed_on: 2006-01-01 00:00:00 +09:00 ++ comments: Make 'branch00' branch. ++ id: 111 ++ revision: "1" ++ scm_order: ++ scmid: 96aec45e5255 ++ user_id: ++ repository_id: 12 ++ committer: test00 ++changesets_013: ++ commit_date: 2007-12-14 ++ committed_on: 2007-12-14 18:24:00 +09:00 ++ comments: Added 2 files and modified one. ++ id: 112 ++ revision: "2" ++ scm_order: ++ scmid: 9d5b5b004199 ++ user_id: 2 ++ repository_id: 12 ++ committer: jsmith +diff --git a/test/fixtures/enabled_modules.yml b/test/fixtures/enabled_modules.yml +--- a/test/fixtures/enabled_modules.yml ++++ b/test/fixtures/enabled_modules.yml +@@ -63,3 +63,11 @@ + name: boards + project_id: 2 + id: 16 ++enabled_modules_017: ++ name: issue_tracking ++ project_id: 7 ++ id: 17 ++enabled_modules_018: ++ name: repository ++ project_id: 7 ++ id: 18 +diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml +--- a/test/fixtures/projects.yml ++++ b/test/fixtures/projects.yml +@@ -71,4 +71,32 @@ + parent_id: 5 + lft: 3 + rgt: 4 +- +\ No newline at end of file ++projects_007: ++ name: Mercurial overhaul ++ created_on: 2010-03-08 21:51:42 +02:00 ++ status: 1 ++ updated_on: 2010-03-08 21:51:42 +02:00 ++ lft: 13 ++ id: 7 ++ description: | ++ h3. Redmine 0.9.x ++ ++ Desc. ++ ++ * r1 ++ * commit:96aec45e5255 ++ ++
++    rNN, commit:shortid
++    
++ ++ h3. Redmine 1.0 or Redmine 0.10 or Redmine 0.9.X ++ ++ Desc. ++ ++ * commit:96aec45e5255 ++ homepage: "" ++ is_public: true ++ parent_id: ++ identifier: mercurial-overhaul ++ rgt: 14 +diff --git a/test/fixtures/repositories.yml b/test/fixtures/repositories.yml +--- a/test/fixtures/repositories.yml ++++ b/test/fixtures/repositories.yml +@@ -15,3 +15,11 @@ + password: "" + login: "" + type: Subversion ++repositories_003: ++ project_id: 7 ++ url: <%= RAILS_ROOT.gsub(%r{config\/\.\.}, '') %>/tmp/test/mercurial_repository ++ type: Mercurial ++ id: 12 ++ root_url: "" ++ login: "" ++ password: "" +diff --git a/test/fixtures/repositories/mercurial/branch00.r1.bundle b/test/fixtures/repositories/mercurial/branch00.r1.bundle +new file mode 100644 +index 0000000000000000000000000000000000000000..2fd5ab0eefe96b8f69f70eb7b6a35332affa2d63 +GIT binary patch +literal 772 +zc$@(Q1N;0)M=>x$T4*^jL0KkKSqU6Lc>n+l|Ns9{ajM13|Mh>O5(z*5|KYy62`(Z9 +z83+^)b>P7XQN=(4T}Kl^29zQ^D9nff>IRxL(@hUkO#{>o0ib9A000dDG%|jpYKSlZ +zOaKJIFaQ7m089d4A%FlQ0006bM8N@1Q$P&>000^q00000000dD00K!ypif7nYBfAe +z2@gnVpbb4k$p8Q}Gy#xkdQCI{(?9^5Lmmh{o?Ft=^far6gxwd*S{Q0XO2%A8dRWJ)08|df>pyo96Lh5B2N6F +z%$cXwm<1_uK#4Y+Lq|L>0-$kP+h9llkwILvDJW8YBq|mtxtcfIoBPzPmV{!8V5-~Z +zY5am2!t96lTXZgTxdWvKfEz?Ry5Uk~=Cy(xHByqISt9dS7962u2$0uG3XrnFKy?xn +zRu*q(hC8}IO$DBGkx>w7mJ@b?2kEf72^6se`f@&1^?7UNm}rnQYE!{XS9FCP-HUMA +z+p@0ar1D^*Ar_T6wyW5J`?79Wq_#m2sH-a?x>SW(CKhA~c-5~EWu-3KkuW|OR#+&r +z1*9kHx04vB)qvWnaSGTiK~XHlHGr6zEj{Kdg_#j`SBX>=8B^G@@)W{&q)P~(pu%zV +z)(ljk7N{mS!r?1*M+wuCYY#%=(?=%fDhbkHkYl+YN>)keEvPa~WpO)v45E4-((e%Q +zm<0IKKvkTeCIDb6t5?&U1W+XW9ckZSX0t)z-x$T4*^jL0KkKS+t7Jr~m*BfB){*;|jow|M&g^adLm}-{~(~%0$Ab +z5eX3oB!ZJS{3NCxB?R}mzE?0Bk7flBb5w}%HCQN3 +zl$Md{_d;v@Qos^8KoQYUcpW`Lpd{f+!ieA)+XV(VVk#NWYS#NShN(JtAW!V33MvAn +zSTQ^bnm3C%!7{EJ5Ud75=?v16_n#$^M`Q?T9IJpzqAbbEQh-3VpS!BQ7K2`>*LD>!B(8R`Uz1`F~A*6p$b8cNLYe`mBZy* +zblJj$;kJ;KMB&aTf@dD8V?--jIV;`{pm@Mg=uz3L;?>yz{n%tf0D$WwoGcCFJ9-x? +znDXG81MWP~yad*6LK&)P;B)91;z%)5*H#)El( +zA^88uCg(d0w~Np&kiW@laBhUg_7gXqItXkh_3a2Eye9fd*h?`P6N~Vcq9+5mKgHaU +LP81|9BD3lM(25U@ + +diff --git a/test/fixtures/repositories/mercurial/default.r0.bundle b/test/fixtures/repositories/mercurial/default.r0.bundle +new file mode 100644 +index 0000000000000000000000000000000000000000..bb72e796b9e6a99016291b25cd5d5e4c5b3bf855 +GIT binary patch +literal 1863 +zc$@)82e|l1M=>x$T4*^jL0KkKStYgZ+W-I-|NsC0yPN#~|NsC0|Nj5~|Nnn~jBnE^ +zDTo;t_5QDgan#TS&q>8~U2N@DbwCtwG(>?<(N9xqdTJX})EG^vjX?Dtpm{@0siu!4 +z^&X?iG#;SPXam$_9-~iFL)2&hXa<8LLFxggiaku5C^SfEF)*j-RQxJ5(=8>$ +z0iYTU00000000000000015gObq+ufz$>}l%5NYWcfHX7>003wJGynk50gwOy00000 +z0E0$`fEo=n0g%u%(?*69Kr(0z01XUAO)_8z#KZt-8e}pIBS2`$pa1|QlSr6Mrc9b9 +z(^Jx5qX^SRjT!-yMw%K7o}d5#14e)}WB@b(XaE2J00D&Mh>&;$EhpVxsKPmrpk)lj +z^L1i1SVCR8AhyVG6e1hrCUpoBP|t97(D(R1 +zOp?L5HdGJ>InvkfNIXPU*}E5GMBN4OG9W`_=BZa}2DPLV<0=`B9IpCucYKCNxJn=k +zp(q_{NkmEu1)XAsutbupyLa&xh(3 +z_Z=X}837>320&cs=5!c_R`6{mqbmJ0cv;)Mm3e5SH+4wnnZvEeZS5B=++p5Cck+O3 +z!4oMQ0un;Gu98*GBRwN|jX^(=gAzXDz4JB#<|VHRvN^!9eS+Go_Gxh&$|! +z)3%u~#`=SZMFjxM7Omi!zFaW>!5BTc*B_((Kwa{4YHhIak5`diU*xwbmtS;_)tu5S!~R^ +z8b>P-drXA)#c63u+##d@qDtgw3*`)!GN^_a;=d9v8u<}%#rZZ(&@pIBH^wAGQsmTN +z4&hAul@M2!Tq{%v5?~ClD+;&v&1vR6CI*H$7?5s4+%K%-EM!!Noq`3_ti!MZ +z66*7|AXitk7|?6XbiBrw@M}r@mX_01iRJ{cbv)wL&eU5rj^1-@3um5@N`eWeQWrE<=xZUNJ3_>b_3Z8Tk8jv%Y)O{Z +z4pkJtADrg0BvjUO7j36NO4jtuj3tWFRV7Z@MNE5C_g<(X17^X%OL?+$L*wuX?1cC +z+tPc8cbd>`c9aE#xbcL{u3ikhfYTwS3DT1t%x#BOsE3l17L$P)rEyzbV)ClP?8A= +zqbrM-5EWGPrJPcfI=ov=TW<+*1@WieKj1o%l}*rh^s_KWbCU_Ni2iQGuR1PFr+xKP_L1N!y0g} +zgc};rl|eHrN;i$D%PsKJ891jU#I6ZXNXEwg=d7zFd{oKkUiX_>z#Ah0UAiz2MtZ42 +zCPv>)p<0C6$8yXVm|=v>)j&!NjK&tFK*qyDu%fIpRf2^vm6Y?cH|j?m%xI3%7quAW +zLbqwqN{dcJQbc2CrNfe48zu^lG^09F0=r;kZRb+VLbS9>Zq8yA4trP&Dv_9KY)(MX +zEayYO4n&pIMB+jglCD9}2YY{|p7MydodAKYFhLI}@d?3LSaA%@M=6!S%_M52HBc%< +zg;WFBoVsx8&8%bKo>fU`fMwVtfYXp9TpKYPgVxY%a+wqaG~(7PgPamz>_suIyKSyx +z*}ZU}#Ziw#5pQBy9Pq{s;rVhqih`Y8KYMJg_%6(9lme%nr>f?@p&s&pDc_x}@3ND|ujZGi5~ +BLH+;$ + +diff --git a/test/fixtures/repositories/mercurial/default.r1-r5.bundle b/test/fixtures/repositories/mercurial/default.r1-r5.bundle +new file mode 100644 +index 0000000000000000000000000000000000000000..7d661edabf599e701cc1c27b13219050e313a9bb +GIT binary patch +literal 3126 +zc$@(?49W9IM=>x$T4*^jL0KkKS>kAlBme+UfB*mf`~Uv`|NsC0|NsC0|Nnpg-|zqb +z|KEStfB*me|J~pTM)=h*=D;1NUh(Rs0qALXGOF(lA|_FzO-<^X0#o%pDe0zD(;7+U +zspw@i%>ZciPZacIYCSzhk5u&=6HK6C8$eWBF)WbqzHkn2mhpFiv +zQ`GXFo`{UekZLAGlWKTV^dvO&N9i>g2kMP7Jxv;DGzJp{G#Lzl0MInh$Y=(bfCff` +zBPNXiGzNx2plE2&XblX8fYT8&0%@jzMnayALsM#K(9p(;#R7 +z001-%0ifE3gFpZP4FCWD01n!xq5uE@000000GI#(000000GeP4lK=n!00000022TJ +z6D9%x001-q&;S4c0iXZ?22BBw000^Q000000000000000GH3*n2qBOrrYEGx!kMW& +zX&~~RQ}s5cDYZREVXQT-7w+o1L9h2jb-B3u^2!oc7eT@juk32Pz +zOODZX=#+Z}^9RE<eO8E$%AFvvK7!t7ZG5A(Z-*4r(wcl;EedU+=O +z=HkY==4&Bz47KcS_bxqKEQX5+lM4sqzv1FVkt06?qfJzoZ~a3pKYg19qJveHWz~`i>7LKd21|q9P7+Z@hWg6b9)R +z2T!cmH#^FG;N^pXdBe#?e<{9~Gv$6$NyU+2ew3l&C7&E08&H+egr#3Mtak2xu84Om +zYd9^c)nsL@Gh^sAt$F%AaGj4Z6vfQYhiOdB;wn37hoxIBh_0|r+y$bS)9 +z$Ha@O1q3DSURl4Y})uqo!|N+F2%K@bnFii +zMMy_`yd3o$R|v&~`e86;?PkmZ#2_kH8d@g%{!7$Q`nBK_hpdmae@IM+-PQ$*{RSz;(*K=Z?3uZlO! +z1uO_S9x-%;62zpWA7mPGW0>FtA`udDok4<8gy52T;s})VO2|tJ6m@wHr~HrCt6k1T +zzXJow#O=IM0A;sryf^NQD(S-@xZ0Ovf`JaNXwQ?h^VzT+O1`GTW}4HaJO1Qb_@UDZ$5;!KTNWS^U +zwR|LtD1`bIJ&bfUR6^XHorwe%P+7vl_WRUp+p8f4X5@*EwA|?*$7>qV>JjW!m!@~7 +zX*NN&)COVNIUf4NCd@-*90nAk +zP-h}1%ZLw%lJ4DO6BWa+-rR?Zh$KqaBp05jBHCEV#93iy1TL_OM&yc+&Nw!`Nx||8 +z)e*6f1ONa4Lj<6Z!w|ZRR<|ee{#Fpo1TY1lB2&YN4P01AxPZflhlxKL8z#3p*63%3 +z=4I~V<+OX7p7+NXuHPDCRzR@{tg$9eSFKoTQx7Ns$%*aKW^FD}fC6~bXwC;(A_FGj +z08~`E-9vRSt#+atch_+jdMMQlYmJwdda{}cG++q4VZ+h^&48xaA+;2mqGz#G;Y(|P +zrn6k4<#uR;;1@)4NC$FwDBsk*5Yr-M1)|&WuPwk904*XAFbvYn0#wYTU)5q8$U<9v +zQR^my-;5xQa)a&VzM8JSQ;g58r3Qe@U)1I!;9v4>f@9hYxkAWB%v*VP7l39Z* +zAV`&nuj;vG&Gpq{E)sE<*MRj`WI!aW9>E;S-e0>R6(xn+=i`pOXF-~@?wQ>}Ww^Cv +z@i6cjeEv`6My}Qz>df{}P!Q}j&nX9rl@O57fq|!mW=)qV +z?!rQ@-NwK)5gPUeDOWHrVzqWF0KX8Rk1nwJreeJI6~u2@)`Bk4c5EC3XkL$C*0hr^ +zgOZIR&1xA2HW-{TMsKGcMlRD)o|TLQv6cgDg8ezEtrecq#4+`EzoQ2#@~J}_rw+Ms +zm}VaEs+rWMCREU2G|zjZch9pm8%2cCcNd*FJ_nnXr9s#iH@LHP#TSU5o-C#~F +zJya)~5=gZmsnWlXiQN%Z+A5}v)1olJU~+_H>m`81VE0A}40Fsoa}?zJ#Q8^!2R(1C +zs6nFARrHetj1oBtMd_&zoylnxL}GH|T1YTaSrh5ptjw@vHG>FrhcWPG3Z}gUu6TCB +zPinb9WWfPU=1w5la7E0awv{>y(`4E&V4>J*R3U+hSTSljjwxrPEuI%&We+PUh8c!? +zkU5YXs>i#HXhfnUQBt-gbuovA`P!m$nzc#lsa5v4qDbF{#ic@;3Md~YAM^?C#jeW9 +z&cJ%8+l+aol?y&=!9vyMomOircBzcY*T{L>dzG4Z`OYJ1k(q*31@DA$d80yB7M +zq{dX~C4#DJ{1iPRg>95m`DSHDzeff)BrP?lFOD5PhE_pbB-a}!U*SF52n+)Nft(m) +z`sXd%vip_V85|WUSJ!YuN*6hAsk(Cj^m%}P-JR;PHn}f4`3@UCJk`76gQPI-)hv{t +z5>b;2qsXsR9yqFd*`HyN2W%#;Kt16^3nU;6R94)Nv}>&C88g1jV(l~|qpQ0;9JN^t +zWw=}KFk)eWhD;aDO3~s(6B$hZK%)|&~>prUbB^1^clw})u(>#E5GNctr +z2`iBMOMi18r-2=B +zcEk3kIEC2@(iM2{tGwl?^orD+ato3D5$1ak;74C1w1;wz!Q5tJF5 +zT5Rlbx1=9xaBiIqC}C?Kp% +Qz%lkN ++ ++[extensions] ++# MQ extention needs for unit test of check history editing. ++hgext.mq = ++ ++# share = ++# hgext.convert = ++# hgext.graphlog = ++# extdiff = ++# hgext.hgk = ++# hgext.bookmarks = ++# rebase= ++# hgext.purge= ++ ++# hggit = ++# svn = ++ +diff --git a/test/fixtures/repositories/mercurial_repository.tar.gz b/test/fixtures/repositories/mercurial_repository.tar.gz +deleted file mode 100644 +Binary file test/fixtures/repositories/mercurial_repository.tar.gz has changed +diff --git a/test/functional/repositories_mercurial_controller_test.rb b/test/functional/repositories_mercurial_controller_test.rb +--- a/test/functional/repositories_mercurial_controller_test.rb ++++ b/test/functional/repositories_mercurial_controller_test.rb +@@ -32,9 +32,21 @@ + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil +- Repository::Mercurial.create(:project => Project.find(3), :url => REPOSITORY_PATH) ++ repository = Repository::Mercurial.create(:project => Project.find(3), :url => REPOSITORY_PATH) ++ ++ %x{hg -R #{REPOSITORY_PATH} update null} ++ %x{hg -R #{REPOSITORY_PATH} strip 0} ++ %x{hg -R #{REPOSITORY_PATH} verify} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r0.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch00.r1.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch01.r1.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle} ++ ++ repository.changesets.find(:all).each(&:destroy) ++ repository.fetch_changesets ++ repository.reload + end +- ++ + if File.directory?(REPOSITORY_PATH) + def test_show + get :show, :id => 3 +@@ -68,7 +80,7 @@ + end + + def test_show_at_given_revision +- get :show, :id => 3, :path => ['images'], :rev => 0 ++ get :show, :id => 3, :path => ['images'], :rev => '0885933ad4f6' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) +@@ -92,7 +104,7 @@ + :attributes => { :class => /line-num/ }, + :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ } + end +- ++ + def test_entry_download + get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw' + assert_response :success +@@ -100,6 +112,8 @@ + assert @response.body.include?('WITHOUT ANY WARRANTY') + end + ++ # This shows "default branch". ++ # git shows "master", but Mercurial shows "tip". + def test_directory_entry + get :entry, :id => 3, :path => ['sources'] + assert_response :success +@@ -107,30 +121,81 @@ + assert_not_nil assigns(:entry) + assert_equal 'sources', assigns(:entry).name + end +- ++ ++ def test_browse_branch ++ get :show, :id => 3, :rev => 'branch00' ++ assert_response :success ++ assert_template 'show' ++ assert_not_nil assigns(:entries) ++ assert_equal 4, assigns(:entries).size ++ assert assigns(:entries).detect {|e| e.name == 'branch00-dir' && e.kind == 'dir'} ++ assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} ++ assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} ++ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} ++ end ++ + def test_diff + # Full diff of changeset 4 +- get :diff, :id => 3, :rev => 4 ++ get :diff, :id => 3, :rev => 'def6d2f1254a' + assert_response :success + assert_template 'diff' + # Line 22 removed + assert_tag :tag => 'th', +- :content => /22/, +- :sibling => { :tag => 'td', ++ :content => '22', ++ :sibling => { ++ :tag => 'td', + :attributes => { :class => /diff_out/ }, +- :content => /def remove/ } ++ :content => /def remove/ ++ } + end +- ++ + def test_annotate + get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb'] + assert_response :success + assert_template 'annotate' +- # Line 23, revision 4 +- assert_tag :tag => 'th', :content => /23/, +- :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /4/ } }, +- :sibling => { :tag => 'td', :content => /jsmith/ }, ++ # Line 23, revision 4:def6d2f1254a ++ assert_tag :tag => 'th', ++ :content => '23', ++ :attributes => { :class => 'line-num' }, ++ :sibling => ++ { ++ :tag => 'td', ++ :attributes => { :class => 'revision' }, ++ # :child => { :tag => 'a', :content => '4' } ++ :child => { :tag => 'a', :content => /def6d2f1/ } ++ } ++ assert_tag :tag => 'th', ++ :content => '23', ++ :attributes => { :class => 'line-num' }, ++ :sibling => ++ { ++ :tag => 'td' , ++ :content => 'jsmith' , ++ :attributes => { :class => 'author' }, ++ ++ } ++ assert_tag :tag => 'th', ++ :content => '23', ++ :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /watcher =/ } + end ++ ++ def test_size_ext_1 ++ get :show, :id => 3, :rev => '0885933ad4f6' ++ assert_response :success ++ assert_template 'show' ++ assert_not_nil assigns(:entries) ++ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file' && e.size == 21 } ++ end ++ ++ def test_size_ext_2 ++ get :show, :id => 3, :rev => 'a76b95b2519c' ++ assert_response :success ++ assert_template 'show' ++ assert_not_nil assigns(:entries) ++ assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file' && e.size == 375 } ++ end ++ + else + puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end +diff --git a/test/functional/repositories_mercurial_controller_ver09x_test.rb b/test/functional/repositories_mercurial_controller_ver09x_test.rb +new file mode 100644 +--- /dev/null ++++ b/test/functional/repositories_mercurial_controller_ver09x_test.rb +@@ -0,0 +1,103 @@ ++# redMine - project management software ++# Copyright (C) 2006-2008 Jean-Philippe Lang ++# ++# This program is free software; you can redistribute it and/or ++# modify it under the terms of the GNU General Public License ++# as published by the Free Software Foundation; either version 2 ++# of the License, or (at your option) any later version. ++# ++# This program is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with this program; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ++ ++require File.dirname(__FILE__) + '/../test_helper' ++require 'repositories_controller' ++ ++# Re-raise errors caught by the controller. ++class RepositoriesController; def rescue_action(e) raise e end; end ++ ++class RepositoriesMercurialControllerVer09xTest < ActionController::TestCase ++ fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules, :changesets, :changes ++ ++ # No '..' in the repository path ++ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository' ++ ++ def setup ++ @controller = RepositoriesController.new ++ @request = ActionController::TestRequest.new ++ @response = ActionController::TestResponse.new ++ User.current = nil ++ assert project = Project.find(7) ++ assert repository = project.repository ++ ++ %x{hg -R #{REPOSITORY_PATH} update null} ++ %x{hg -R #{REPOSITORY_PATH} strip 0} ++ %x{hg -R #{REPOSITORY_PATH} verify} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r0.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch00.r1.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch01.r1.bundle} ++ ++ repository.changesets.find( ++ :all, ++ :conditions => [ "revision NOT IN (?)" , ["0","1","2"] ] ++ ).each(&:destroy) ++ repository.fetch_changesets ++ repository.reload ++ end ++ ++ if File.directory?(REPOSITORY_PATH) ++ ++ # called from blame screen. ++ # '96aec45e5255d2b29d992468cb4344ac73334d16' ++ def test_show_revision_with_shortid ++ get :revision, :id => 7, :rev => '96aec45e' ++ assert_response :success ++ assert_template 'revision' ++ assert_tag :tag =>'span', ++ :attributes => { :class =>'scmid' }, ++ :content => /96aec45e/ ++ end ++ ++ def test_show_directory_at_given_revision ++ get :show, :id => 7, :path => ['images'], :rev => '1' ++ assert_response :success ++ assert_template 'show' ++ assert_not_nil assigns(:entries) ++ assert_equal ['delete.png'], assigns(:entries).collect(&:name) ++ entry = assigns(:entries).detect {|e| e.name == 'delete.png'} ++ assert_not_nil entry ++ assert_equal 'file', entry.kind ++ assert_equal 'images/delete.png', entry.path ++ end ++ ++ def test_changes ++ get :changes, :id => 7, :path => ['branch00-dir', 'hg-log.txt'], ++ :rev => '1' ++ assert_response :success ++ assert_template 'changes' ++ assert_tag :tag => 'h2', :content => 'hg-log.txt' ++ end ++ ++ def test_entry_show ++ get :entry, :id => 7, :path => ['branch00-dir', 'hg-log.txt'], ++ :rev => '1' ++ assert_response :success ++ assert_template 'entry' ++ assert_tag :tag => 'th', ++ :content => '10', ++ :attributes => { :class => 'line-num' }, ++ :sibling => { :tag => 'td', :content => /summary: Changed user variable to watcher/ } ++ end ++ ++ ++ else ++ puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!" ++ def test_fake; assert true end ++ end ++end +diff --git a/test/unit/repository_mercurial_test.rb b/test/unit/repository_mercurial_test.rb +--- a/test/unit/repository_mercurial_test.rb ++++ b/test/unit/repository_mercurial_test.rb +@@ -26,46 +26,118 @@ + def setup + @project = Project.find(1) + assert @repository = Repository::Mercurial.create(:project => @project, :url => REPOSITORY_PATH) ++ ++ %x{hg -R #{REPOSITORY_PATH} update null} ++ %x{hg -R #{REPOSITORY_PATH} strip 0} ++ %x{hg -R #{REPOSITORY_PATH} verify} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r0.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch00.r1.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle} ++ ++ @repository.changesets.find(:all).each(&:destroy) ++ + end +- +- if File.directory?(REPOSITORY_PATH) ++ ++ if File.directory?(REPOSITORY_PATH) ++ + def test_fetch_changesets_from_scratch + @repository.fetch_changesets + @repository.reload +- +- assert_equal 6, @repository.changesets.count +- assert_equal 11, @repository.changes.count +- assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0').comments ++ ++ assert_equal 7, @repository.changesets.count ++ assert_equal 12, @repository.changes.count ++ assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0885933ad4f6').comments + end + + def test_fetch_changesets_incremental + @repository.fetch_changesets +- # Remove changesets with revision > 2 +- @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2} ++ ## Mercurial can set commit date, ++ # @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy) ++ @repository.changesets.find(:all, :order => 'scm_order DESC', :limit => 3).each(&:destroy) ++ + @repository.reload +- assert_equal 3, @repository.changesets.count +- ++ assert_equal 4, @repository.changesets.count ++ + @repository.fetch_changesets +- assert_equal 6, @repository.changesets.count ++ assert_equal 7, @repository.changesets.count + end + + def test_entries +- assert_equal 2, @repository.entries("sources", 2).size +- assert_equal 1, @repository.entries("sources", 3).size ++ @repository.fetch_changesets ++ @repository.reload ++ %x{hg -R #{REPOSITORY_PATH} up null} ++ assert_equal 2, @repository.entries("sources", '400bb8672109').size ++ assert_equal 1, @repository.entries("sources", 'b3a615152df8').size + end + + def test_locate_on_outdated_repository ++ @repository.fetch_changesets ++ @repository.reload + # Change the working dir state +- %x{hg -R #{REPOSITORY_PATH} up -r 0} +- assert_equal 1, @repository.entries("images", 0).size ++ %x{hg -R #{REPOSITORY_PATH} up null} ++ assert_equal 1, @repository.entries("images", '0885933ad4f6').size + assert_equal 2, @repository.entries("images").size +- assert_equal 2, @repository.entries("images", 2).size ++ assert_equal 2, @repository.entries("images", '400bb8672109').size + end + ++ def test_cat ++ @repository.fetch_changesets ++ @repository.reload ++ assert @repository.scm.cat("sources/welcome_controller.rb", '400bb8672109') ++ assert_nil @repository.scm.cat("sources/welcome_controller.rb") ++ end + +- def test_cat +- assert @repository.scm.cat("sources/welcome_controller.rb", 2) +- assert_nil @repository.scm.cat("sources/welcome_controller.rb") ++ def test_latest_changesets_tip ++ @repository.fetch_changesets ++ @repository.reload ++ assert_equal 7, @repository.latest_changesets("", 'tip').size ++ end ++ ++ def test_latest_changesets_branch00 ++ @repository.fetch_changesets ++ @repository.reload ++ assert_equal 1, @repository.latest_changesets("", 'branch00').size ++ end ++ ++ def test_simple_strip ++ %x{hg -R #{REPOSITORY_PATH} up null} ++ %x{hg -R #{REPOSITORY_PATH} strip def6d2f1254a} ++ @repository.fetch_changesets ++ @repository.reload ++ assert_equal 5, @repository.changesets.count ++ ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle} ++ @repository.fetch_changesets ++ @repository.reload ++ assert_equal 7, @repository.changesets.count ++ end ++ ++ def test_middle_rev_strip ++ %x{hg -R #{REPOSITORY_PATH} up null} ++ %x{hg -R #{REPOSITORY_PATH} strip 96aec45e5255} ++ @repository.fetch_changesets ++ @repository.reload ++ assert_equal 6, @repository.changesets.count ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch00.r1.bundle} ++ @repository.fetch_changesets ++ @repository.reload ++ assert_equal 7, @repository.changesets.count ++ end ++ ++ def test_size_ext ++ %x{hg -R #{REPOSITORY_PATH} up null} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch01.r1.bundle} ++ @repository.fetch_changesets ++ @repository.reload ++ assert_equal 21, @repository.scm.size_from_ext("README", '0885933ad4f6') ++ assert_equal 375, @repository.scm.size_from_ext("README", 'a76b95b2519c') ++ end ++ ++ def test_invalid_repo_url ++ @repository.url = REPOSITORY_PATH + "_invalid" ++ @repository.fetch_changesets ++ @repository.reload ++ assert_nil @repository.entries("") + end + + else +diff --git a/test/unit/repository_mercurial_ver09x_test.rb b/test/unit/repository_mercurial_ver09x_test.rb +new file mode 100644 +--- /dev/null ++++ b/test/unit/repository_mercurial_ver09x_test.rb +@@ -0,0 +1,71 @@ ++# redMine - project management software ++# Copyright (C) 2006-2007 Jean-Philippe Lang ++# ++# This program is free software; you can redistribute it and/or ++# modify it under the terms of the GNU General Public License ++# as published by the Free Software Foundation; either version 2 ++# of the License, or (at your option) any later version. ++# ++# This program is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with this program; if not, write to the Free Software ++# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ++ ++require File.dirname(__FILE__) + '/../test_helper' ++ ++class RepositoryMercurialVer09xTest < ActiveSupport::TestCase ++ fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules, :changesets, :changes ++ # fixtures :all ++ ++ # No '..' in the repository path ++ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository' ++ ++ def setup ++ assert @project = Project.find(7) ++ assert @repository = @project.repository ++ # assert_equal 3, @repository.changesets.count ++ ++ %x{hg -R #{REPOSITORY_PATH} update null} ++ %x{hg -R #{REPOSITORY_PATH} strip 0} ++ %x{hg -R #{REPOSITORY_PATH} verify} ++ ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r0.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle} ++ %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch00.r1.bundle} ++ # %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch01.r1.bundle} ++ ++ @repository.changesets.find( ++ :all, ++ :conditions => [ "revision NOT IN (?)" , ["0","1","2"] ] ++ ).each(&:destroy) ++ @repository.fetch_changesets ++ @repository.reload ++ ++ end ++ ++ if File.directory?(REPOSITORY_PATH) ++ ++ def test_entries ++ %x{hg -R #{REPOSITORY_PATH} up null} ++ assert_equal 1, @repository.entries("sources", '0').size ++ assert_equal 1, @repository.entries("images", '0').size ++ assert_equal 1, @repository.entries("sources", '1').size ++ assert_equal 1, @repository.entries("images", '1').size ++ assert_equal 1, @repository.entries("branch00-dir", '1').size ++ end ++ ++ def test_latest_changesets_rev1 ++ @repository.fetch_changesets ++ @repository.reload ++ assert_equal 7, @repository.latest_changesets("", '1').size ++ end ++ ++ else ++ puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!" ++ def test_fake; assert true end ++ end ++end diff -r 40f2607efd173437c7df45c958f5452fe0f6cb04 -r 7f692581605fef75ca01e31c87730e22e706cb77 mercurial_helper_hgrc_support.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial_helper_hgrc_support.diff Wed Mar 17 21:21:02 2010 +0100 @@ -0,0 +1,36 @@ +# HG changeset patch +# Parent b6819019edd69075d073e1832921fb111eb220d2 +Add .hgrc support inside repository settings views + +diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb +--- a/app/helpers/repositories_helper.rb ++++ b/app/helpers/repositories_helper.rb +@@ -160,6 +160,7 @@ + end + + def mercurial_field_tags(form, repository) ++ repository.load_hgrc + content_tag('p', form.text_field( + :url, + :label => 'Root directory', +@@ -168,7 +169,19 @@ + ## Mercurial repository is removable. + # :disabled => (repository && !repository.root_url.blank?) + :disabled => false +- )) ++ )) + ++ content_tag('p', form.text_field( ++ :hgrc_web_contact, ++ :label => 'Contacts')) + ++ content_tag('p', form.text_field( ++ :hgrc_web_description, ++ :label => 'Description')) + ++ content_tag('p', form.text_field( ++ :hgrc_web_style, ++ :label => 'Style')) + ++ content_tag('p', form.check_box( ++ :hgrc_hooks_issues_update, ++ :label => 'Update issues on push')) + end + + def git_field_tags(form, repository) diff -r 40f2607efd173437c7df45c958f5452fe0f6cb04 -r 7f692581605fef75ca01e31c87730e22e706cb77 mercurial_hgrc_support.diff --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial_hgrc_support.diff Wed Mar 17 21:21:02 2010 +0100 @@ -0,0 +1,70 @@ +# HG changeset patch +# Parent 167057abb32712da4fb72ab71650c6c598775b37 +Add .hgrc support to mercurial repos + +diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb +--- a/app/models/repository/mercurial.rb ++++ b/app/models/repository/mercurial.rb +@@ -16,6 +16,7 @@ + # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + require 'redmine/scm/adapters/mercurial_adapter' ++require 'ini' + + class Repository::Mercurial < Repository + attr_protected :root_url +@@ -24,6 +25,13 @@ + @@limit_check_strip = 100 + @@num_convert_redmine_0_9 = 20 + ++ attr_accessor :hgrc_hooks_issues_update ++ attr_accessor :hgrc_web_contact ++ attr_accessor :hgrc_web_description ++ attr_accessor :hgrc_web_style ++ ++ after_save :save_hgrc ++ + def scm_adapter + Redmine::Scm::Adapters::MercurialAdapter + end +@@ -378,4 +386,40 @@ + ret = cs + ret + end ++ ++ def load_hgrc ++ logger.debug("eseguo LOAD_HGRC") ++ ini = Ini.new(File.join(root_url, '.hg', 'hgrc')) ++ #web ++ ini['web'] = {} if ini['web'].nil? ++ @hgrc_web_contact = ini['web']['contact'] ++ @hgrc_web_description = ini['web']['description'] ++ @hgrc_web_style = ini['web']['style'] ++ #hooks ++ ini['hooks'] = {} if ini['hooks'].nil? ++ if !ini['hooks']['changegroup.redmine'].nil? && !ini['hooks']['changegroup.redmine'].empty? ++ @hgrc_hooks_issues_update = 1 ++ else ++ @hgrc_hooks_issues_update = 0 ++ end ++ end ++ ++ private ++ def save_hgrc ++ logger.debug("eseguo SAVE_HGRC #{@hgrc_hooks_issues_update}") ++ ini = Ini.new(File.join(root_url, '.hg', 'hgrc')) ++ #web ++ ini['web'] = {} if ini['web'].nil? ++ ini['web']['contact'] = @hgrc_web_contact ++ ini['web']['description'] = @hgrc_web_description ++ ini['web']['style'] = @hgrc_web_style ++ #hooks ++ ini['hooks'] = {} if ini['hooks'].nil? ++ if @hgrc_hooks_issues_update.to_i == 1 ++ ini['hooks']['changegroup.redmine'] = "cd #{RAILS_ROOT} && ruby script/runner \"Repository.fetch_changesets\" -e #{RAILS_ENV}" ++ end ++ ini.comment = "Generated with Redmine.\n" ++ #save ++ ini.update() ++ end + end diff -r 40f2607efd173437c7df45c958f5452fe0f6cb04 -r 7f692581605fef75ca01e31c87730e22e706cb77 series --- a/series Wed Mar 17 20:24:14 2010 +0100 +++ b/series Wed Mar 17 21:21:02 2010 +0100 @@ -1,1 +1,5 @@ # Placed by Bitbucket +marutosi.diff +add_ini_support.diff +mercurial_hgrc_support.diff +mercurial_helper_hgrc_support.diff