##// END OF EJS Templates
Mercurial adapter improvements (patch #1199 by Pierre Paysant-Le Roux)....
Jean-Philippe Lang -
r1485:aa9d04a4a7ec
parent child
Show More
@@ -0,0 +1,12
1 changeset = 'This template must be used with --debug option\n'
2 changeset_quiet = 'This template must be used with --debug option\n'
3 changeset_verbose = 'This template must be used with --debug option\n'
4 changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5
6 file = '<path action="M">{file|escape}</path>\n'
7 file_add = '<path action="A">{file_add|escape}</path>\n'
8 file_del = '<path action="D">{file_del|escape}</path>\n'
9 file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
10 tag = '<tag>{tag|escape}</tag>\n'
11 header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
12 # footer="</log>" No newline at end of file
@@ -0,0 +1,12
1 changeset = 'This template must be used with --debug option\n'
2 changeset_quiet = 'This template must be used with --debug option\n'
3 changeset_verbose = 'This template must be used with --debug option\n'
4 changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5
6 file_mod = '<path action="M">{file_mod|escape}</path>\n'
7 file_add = '<path action="A">{file_add|escape}</path>\n'
8 file_del = '<path action="D">{file_del|escape}</path>\n'
9 file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
10 tag = '<tag>{tag|escape}</tag>\n'
11 header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
12 # footer="</log>"
@@ -0,0 +1,49
1 require File.dirname(__FILE__) + '/../test_helper'
2 begin
3 require 'mocha'
4
5 class MercurialAdapterTest < Test::Unit::TestCase
6
7 TEMPLATES_DIR = "#{RAILS_ROOT}/extra/mercurial"
8 TEMPLATE_NAME = "hg-template"
9 TEMPLATE_EXTENSION = "tmpl"
10
11 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository'
12
13
14 def test_version_template_0_9_5
15 # 0.9.5
16 test_version_template_for("0.9.5", [0,9,5], "0.9.5")
17 end
18
19 def test_version_template_1_0
20 # 1.0
21 test_version_template_for("1.0", [1,0], "1.0")
22 end
23
24 def test_version_template_1_0_win
25 test_version_template_for("1e4ddc9ac9f7+20080325", "Unknown version", "1.0")
26 end
27
28 def test_version_template_1_0_1_win
29 test_version_template_for("1.0.1+20080525", [1,0,1], "1.0")
30 end
31
32 def test_version_template_changeset_id
33 test_version_template_for("1916e629a29d", "Unknown version", "1.0")
34 end
35
36 private
37
38 def test_version_template_for(hgversion, version, templateversion)
39 Redmine::Scm::Adapters::MercurialAdapter.any_instance.stubs(:hgversion_from_command_line).returns(hgversion)
40 adapter = Redmine::Scm::Adapters::MercurialAdapter.new(REPOSITORY_PATH)
41 assert_equal version, adapter.hgversion
42 assert_equal "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{templateversion}.#{TEMPLATE_EXTENSION}", adapter.template_path
43 assert File.exist?(adapter.template_path)
44 end
45 end
46
47 rescue LoadError
48 def test_fake; assert(false, "Requires mocha to run those tests") end
49 end
@@ -94,6 +94,11 module Redmine
94 path ||= ''
94 path ||= ''
95 (path[0,1]!="/") ? "/#{path}" : path
95 (path[0,1]!="/") ? "/#{path}" : path
96 end
96 end
97
98 def with_trailling_slash(path)
99 path ||= ''
100 (path[-1,1] == "/") ? path : "#{path}/"
101 end
97
102
98 def shell_quote(str)
103 def shell_quote(str)
99 if RUBY_PLATFORM =~ /mswin/
104 if RUBY_PLATFORM =~ /mswin/
@@ -102,7 +107,7 module Redmine
102 "'" + str.gsub(/'/, "'\"'\"'") + "'"
107 "'" + str.gsub(/'/, "'\"'\"'") + "'"
103 end
108 end
104 end
109 end
105
110
106 private
111 private
107 def retrieve_root_url
112 def retrieve_root_url
108 info = self.info
113 info = self.info
@@ -21,9 +21,12 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class MercurialAdapter < AbstractAdapter
23 class MercurialAdapter < AbstractAdapter
24
24
25 # Mercurial executable name
25 # Mercurial executable name
26 HG_BIN = "hg"
26 HG_BIN = "hg"
27 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 TEMPLATE_NAME = "hg-template"
29 TEMPLATE_EXTENSION = "tmpl"
27
30
28 def info
31 def info
29 cmd = "#{HG_BIN} -R #{target('')} root"
32 cmd = "#{HG_BIN} -R #{target('')} root"
@@ -33,8 +36,8 module Redmine
33 end
36 end
34 return nil if $? && $?.exitstatus != 0
37 return nil if $? && $?.exitstatus != 0
35 info = Info.new({:root_url => root_url.chomp,
38 info = Info.new({:root_url => root_url.chomp,
36 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
39 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
37 })
40 })
38 info
41 info
39 rescue CommandFailed
42 rescue CommandFailed
40 return nil
43 return nil
@@ -43,62 +46,72 module Redmine
43 def entries(path=nil, identifier=nil)
46 def entries(path=nil, identifier=nil)
44 path ||= ''
47 path ||= ''
45 entries = Entries.new
48 entries = Entries.new
46 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target(path)} locate"
49 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
47 cmd << " -r #{identifier.to_i}" if identifier
50 cmd << " -r " + (identifier ? identifier.to_s : "tip")
48 cmd << " " + shell_quote('glob:**')
51 cmd << " " + shell_quote("path:#{path}") unless path.empty?
49 shellout(cmd) do |io|
52 shellout(cmd) do |io|
50 io.each_line do |line|
53 io.each_line do |line|
51 e = line.chomp.split(%r{[\/\\]})
54 # HG uses antislashs as separator on Windows
52 entries << Entry.new({:name => e.first,
55 line = line.gsub(/\\/, "/")
53 :path => (path.empty? ? e.first : "#{path}/#{e.first}"),
56 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
54 :kind => (e.size > 1 ? 'dir' : 'file'),
57 e ||= line
55 :lastrev => Revision.new
58 e = e.chomp.split(%r{[\/\\]})
56 }) unless entries.detect{|entry| entry.name == e.first}
59 entries << Entry.new({:name => e.first,
60 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
61 :kind => (e.size > 1 ? 'dir' : 'file'),
62 :lastrev => Revision.new
63 }) unless entries.detect{|entry| entry.name == e.first}
64 end
57 end
65 end
58 end
66 end
59 return nil if $? && $?.exitstatus != 0
67 return nil if $? && $?.exitstatus != 0
60 entries.sort_by_name
68 entries.sort_by_name
61 end
69 end
62
70
63 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
71 # Fetch the revisions by using a template file that
72 # makes Mercurial produce a xml output.
73 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
64 revisions = Revisions.new
74 revisions = Revisions.new
65 cmd = "#{HG_BIN} -v --encoding utf8 -R #{target('')} log"
75 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{self.template_path}"
66 if identifier_from && identifier_to
76 if identifier_from && identifier_to
67 cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
77 cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
68 elsif identifier_from
78 elsif identifier_from
69 cmd << " -r #{identifier_from.to_i}:"
79 cmd << " -r #{identifier_from.to_i}:"
70 end
80 end
71 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
81 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
82 cmd << " #{path}" if path
72 shellout(cmd) do |io|
83 shellout(cmd) do |io|
73 changeset = {}
84 begin
74 parsing_descr = false
85 # HG doesn't close the XML Document...
75 line_feeds = 0
86 doc = REXML::Document.new(io.read << "</log>")
76
87 doc.elements.each("log/logentry") do |logentry|
77 io.each_line do |line|
88 paths = []
78 if line =~ /^(\w+):\s*(.*)$/
89 copies = logentry.get_elements('paths/path-copied')
79 key = $1
90 logentry.elements.each("paths/path") do |path|
80 value = $2
91 # Detect if the added file is a copy
81 if parsing_descr && line_feeds > 1
92 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
82 parsing_descr = false
93 from_path = c.attributes['copyfrom-path']
83 revisions << build_revision_from_changeset(changeset)
94 from_rev = logentry.attributes['revision']
84 changeset = {}
85 end
86 if !parsing_descr
87 changeset.store key.to_sym, value
88 if $1 == "description"
89 parsing_descr = true
90 line_feeds = 0
91 next
92 end
95 end
96 paths << {:action => path.attributes['action'],
97 :path => "/#{path.text}",
98 :from_path => from_path ? "/#{from_path}" : nil,
99 :from_revision => from_rev ? from_rev : nil
100 }
93 end
101 end
102 paths.sort! { |x,y| x[:path] <=> y[:path] }
103
104 revisions << Revision.new({:identifier => logentry.attributes['revision'],
105 :scmid => logentry.attributes['node'],
106 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
107 :time => Time.parse(logentry.elements['date'].text).localtime,
108 :message => logentry.elements['msg'].text,
109 :paths => paths
110 })
94 end
111 end
95 if parsing_descr
112 rescue
96 changeset[:description] << line
113 logger.debug($!)
97 line_feeds += 1 if line.chomp.empty?
98 end
99 end
114 end
100 # Add the last changeset if there is one left
101 revisions << build_revision_from_changeset(changeset) if changeset[:date]
102 end
115 end
103 return nil if $? && $?.exitstatus != 0
116 return nil if $? && $?.exitstatus != 0
104 revisions
117 revisions
@@ -125,7 +138,7 module Redmine
125
138
126 def cat(path, identifier=nil)
139 def cat(path, identifier=nil)
127 cmd = "#{HG_BIN} -R #{target('')} cat"
140 cmd = "#{HG_BIN} -R #{target('')} cat"
128 cmd << " -r #{identifier.to_i}" if identifier
141 cmd << " -r " + (identifier ? identifier.to_s : "tip")
129 cmd << " #{target(path)}"
142 cmd << " #{target(path)}"
130 cat = nil
143 cat = nil
131 shellout(cmd) do |io|
144 shellout(cmd) do |io|
@@ -140,6 +153,7 module Redmine
140 path ||= ''
153 path ||= ''
141 cmd = "#{HG_BIN} -R #{target('')}"
154 cmd = "#{HG_BIN} -R #{target('')}"
142 cmd << " annotate -n -u"
155 cmd << " annotate -n -u"
156 cmd << " -r " + (identifier ? identifier.to_s : "tip")
143 cmd << " -r #{identifier.to_i}" if identifier
157 cmd << " -r #{identifier.to_i}" if identifier
144 cmd << " #{target(path)}"
158 cmd << " #{target(path)}"
145 blame = Annotate.new
159 blame = Annotate.new
@@ -153,45 +167,35 module Redmine
153 blame
167 blame
154 end
168 end
155
169
156 private
170 # The hg version version is expressed either as a
171 # release number (eg 0.9.5 or 1.0) or as a revision
172 # id composed of 12 hexa characters.
173 def hgversion
174 theversion = hgversion_from_command_line
175 if theversion.match(/^\d+(\.\d+)+/)
176 theversion.split(".").collect(&:to_i)
177 # elsif match = theversion.match(/[[:xdigit:]]{12}/)
178 # match[0]
179 else
180 "Unknown version"
181 end
182 end
157
183
158 # Builds a revision objet from the changeset returned by hg command
184 def template_path
159 def build_revision_from_changeset(changeset)
185 @template ||= begin
160 rev_id = changeset[:changeset].to_s.split(':').first.to_i
186 if hgversion.is_a?(String) or ((hgversion <=> [0,9,5]) > 0)
161
187 ver = "1.0"
162 # Changes
188 else
163 paths = (rev_id == 0) ?
189 ver = "0.9.5"
164 # Can't get changes for revision 0 with hg status
190 end
165 changeset[:files].to_s.split.collect{|path| {:action => 'A', :path => "/#{path}"}} :
191 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
166 status(rev_id)
192 end
167
168 Revision.new({:identifier => rev_id,
169 :scmid => changeset[:changeset].to_s.split(':').last,
170 :author => changeset[:user],
171 :time => Time.parse(changeset[:date]),
172 :message => changeset[:description],
173 :paths => paths
174 })
175 end
193 end
176
194
177 # Returns the file changes for a given revision
195 private
178 def status(rev_id)
196
179 cmd = "#{HG_BIN} -R #{target('')} status --rev #{rev_id.to_i - 1}:#{rev_id.to_i}"
197 def hgversion_from_command_line
180 result = []
198 @hgversion ||= %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
181 shellout(cmd) do |io|
182 io.each_line do |line|
183 action, file = line.chomp.split
184 next unless action && file
185 file.gsub!("\\", "/")
186 case action
187 when 'R'
188 result << { :action => 'D' , :path => "/#{file}" }
189 else
190 result << { :action => action, :path => "/#{file}" }
191 end
192 end
193 end
194 result
195 end
199 end
196 end
200 end
197 end
201 end
@@ -48,6 +48,26 class RepositoryMercurialTest < Test::Unit::TestCase
48 @repository.fetch_changesets
48 @repository.fetch_changesets
49 assert_equal 6, @repository.changesets.count
49 assert_equal 6, @repository.changesets.count
50 end
50 end
51
52 def test_entries
53 assert_equal 2, @repository.entries("sources", 2).size
54 assert_equal 1, @repository.entries("sources", 3).size
55 end
56
57 def test_locate_on_outdated_repository
58 # Change the working dir state
59 %x{hg -R #{REPOSITORY_PATH} up -r 0}
60 assert_equal 1, @repository.entries("images", 0).size
61 assert_equal 2, @repository.entries("images").size
62 assert_equal 2, @repository.entries("images", 2).size
63 end
64
65
66 def test_cat
67 assert @repository.scm.cat("sources/welcome_controller.rb", 2)
68 assert_nil @repository.scm.cat("sources/welcome_controller.rb")
69 end
70
51 else
71 else
52 puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!"
72 puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!"
53 def test_fake; assert true end
73 def test_fake; assert true end
General Comments 0
You need to be logged in to leave comments. Login now