##// 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
@@ -95,6 +95,11 module Redmine
95 (path[0,1]!="/") ? "/#{path}" : path
95 (path[0,1]!="/") ? "/#{path}" : path
96 end
96 end
97
97
98 def with_trailling_slash(path)
99 path ||= ''
100 (path[-1,1] == "/") ? path : "#{path}/"
101 end
102
98 def shell_quote(str)
103 def shell_quote(str)
99 if RUBY_PLATFORM =~ /mswin/
104 if RUBY_PLATFORM =~ /mswin/
100 '"' + str.gsub(/"/, '\\"') + '"'
105 '"' + str.gsub(/"/, '\\"') + '"'
@@ -24,6 +24,9 module Redmine
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"
@@ -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
55 line = line.gsub(/\\/, "/")
56 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
57 e ||= line
58 e = e.chomp.split(%r{[\/\\]})
52 entries << Entry.new({:name => e.first,
59 entries << Entry.new({:name => e.first,
53 :path => (path.empty? ? e.first : "#{path}/#{e.first}"),
60 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
54 :kind => (e.size > 1 ? 'dir' : 'file'),
61 :kind => (e.size > 1 ? 'dir' : 'file'),
55 :lastrev => Revision.new
62 :lastrev => Revision.new
56 }) unless entries.detect{|entry| entry.name == e.first}
63 }) unless entries.detect{|entry| entry.name == e.first}
57 end
64 end
58 end
65 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
71 # Fetch the revisions by using a template file that
72 # makes Mercurial produce a xml output.
63 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
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>")
87 doc.elements.each("log/logentry") do |logentry|
88 paths = []
89 copies = logentry.get_elements('paths/path-copied')
90 logentry.elements.each("paths/path") do |path|
91 # Detect if the added file is a copy
92 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
93 from_path = c.attributes['copyfrom-path']
94 from_rev = logentry.attributes['revision']
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 }
101 end
102 paths.sort! { |x,y| x[:path] <=> y[:path] }
76
103
77 io.each_line do |line|
104 revisions << Revision.new({:identifier => logentry.attributes['revision'],
78 if line =~ /^(\w+):\s*(.*)$/
105 :scmid => logentry.attributes['node'],
79 key = $1
106 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
80 value = $2
107 :time => Time.parse(logentry.elements['date'].text).localtime,
81 if parsing_descr && line_feeds > 1
108 :message => logentry.elements['msg'].text,
82 parsing_descr = false
109 :paths => paths
83 revisions << build_revision_from_changeset(changeset)
110 })
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
93 end
94 end
95 if parsing_descr
96 changeset[:description] << line
97 line_feeds += 1 if line.chomp.empty?
98 end
111 end
112 rescue
113 logger.debug($!)
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
157
171 # release number (eg 0.9.5 or 1.0) or as a revision
158 # Builds a revision objet from the changeset returned by hg command
172 # id composed of 12 hexa characters.
159 def build_revision_from_changeset(changeset)
173 def hgversion
160 rev_id = changeset[:changeset].to_s.split(':').first.to_i
174 theversion = hgversion_from_command_line
161
175 if theversion.match(/^\d+(\.\d+)+/)
162 # Changes
176 theversion.split(".").collect(&:to_i)
163 paths = (rev_id == 0) ?
177 # elsif match = theversion.match(/[[:xdigit:]]{12}/)
164 # Can't get changes for revision 0 with hg status
178 # match[0]
165 changeset[:files].to_s.split.collect{|path| {:action => 'A', :path => "/#{path}"}} :
179 else
166 status(rev_id)
180 "Unknown version"
167
181 end
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
182 end
176
183
177 # Returns the file changes for a given revision
184 def template_path
178 def status(rev_id)
185 @template ||= begin
179 cmd = "#{HG_BIN} -R #{target('')} status --rev #{rev_id.to_i - 1}:#{rev_id.to_i}"
186 if hgversion.is_a?(String) or ((hgversion <=> [0,9,5]) > 0)
180 result = []
187 ver = "1.0"
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
188 else
190 result << { :action => action, :path => "/#{file}" }
189 ver = "0.9.5"
191 end
190 end
191 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
192 end
192 end
193 end
193 end
194 result
194
195 private
196
197 def hgversion_from_command_line
198 @hgversion ||= %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
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