##// 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
@@ -1,405 +1,410
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'cgi'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class CommandFailed < StandardError #:nodoc:
24 24 end
25 25
26 26 class AbstractAdapter #:nodoc:
27 27 def initialize(url, root_url=nil, login=nil, password=nil)
28 28 @url = url
29 29 @login = login if login && !login.empty?
30 30 @password = (password || "") if @login
31 31 @root_url = root_url.blank? ? retrieve_root_url : root_url
32 32 end
33 33
34 34 def adapter_name
35 35 'Abstract'
36 36 end
37 37
38 38 def supports_cat?
39 39 true
40 40 end
41 41
42 42 def supports_annotate?
43 43 respond_to?('annotate')
44 44 end
45 45
46 46 def root_url
47 47 @root_url
48 48 end
49 49
50 50 def url
51 51 @url
52 52 end
53 53
54 54 # get info about the svn repository
55 55 def info
56 56 return nil
57 57 end
58 58
59 59 # Returns the entry identified by path and revision identifier
60 60 # or nil if entry doesn't exist in the repository
61 61 def entry(path=nil, identifier=nil)
62 62 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
63 63 search_path = parts[0..-2].join('/')
64 64 search_name = parts[-1]
65 65 if search_path.blank? && search_name.blank?
66 66 # Root entry
67 67 Entry.new(:path => '', :kind => 'dir')
68 68 else
69 69 # Search for the entry in the parent directory
70 70 es = entries(search_path, identifier)
71 71 es ? es.detect {|e| e.name == search_name} : nil
72 72 end
73 73 end
74 74
75 75 # Returns an Entries collection
76 76 # or nil if the given path doesn't exist in the repository
77 77 def entries(path=nil, identifier=nil)
78 78 return nil
79 79 end
80 80
81 81 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
82 82 return nil
83 83 end
84 84
85 85 def diff(path, identifier_from, identifier_to=nil, type="inline")
86 86 return nil
87 87 end
88 88
89 89 def cat(path, identifier=nil)
90 90 return nil
91 91 end
92 92
93 93 def with_leading_slash(path)
94 94 path ||= ''
95 95 (path[0,1]!="/") ? "/#{path}" : path
96 96 end
97
98 def with_trailling_slash(path)
99 path ||= ''
100 (path[-1,1] == "/") ? path : "#{path}/"
101 end
97 102
98 103 def shell_quote(str)
99 104 if RUBY_PLATFORM =~ /mswin/
100 105 '"' + str.gsub(/"/, '\\"') + '"'
101 106 else
102 107 "'" + str.gsub(/'/, "'\"'\"'") + "'"
103 108 end
104 109 end
105
110
106 111 private
107 112 def retrieve_root_url
108 113 info = self.info
109 114 info ? info.root_url : nil
110 115 end
111 116
112 117 def target(path)
113 118 path ||= ''
114 119 base = path.match(/^\//) ? root_url : url
115 120 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
116 121 end
117 122
118 123 def logger
119 124 RAILS_DEFAULT_LOGGER
120 125 end
121 126
122 127 def shellout(cmd, &block)
123 128 logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
124 129 begin
125 130 IO.popen(cmd, "r+") do |io|
126 131 io.close_write
127 132 block.call(io) if block_given?
128 133 end
129 134 rescue Errno::ENOENT => e
130 135 msg = strip_credential(e.message)
131 136 # The command failed, log it and re-raise
132 137 logger.error("SCM command failed: #{strip_credential(cmd)}\n with: #{msg}")
133 138 raise CommandFailed.new(msg)
134 139 end
135 140 end
136 141
137 142 # Hides username/password in a given command
138 143 def self.hide_credential(cmd)
139 144 q = (RUBY_PLATFORM =~ /mswin/ ? '"' : "'")
140 145 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
141 146 end
142 147
143 148 def strip_credential(cmd)
144 149 self.class.hide_credential(cmd)
145 150 end
146 151 end
147 152
148 153 class Entries < Array
149 154 def sort_by_name
150 155 sort {|x,y|
151 156 if x.kind == y.kind
152 157 x.name <=> y.name
153 158 else
154 159 x.kind <=> y.kind
155 160 end
156 161 }
157 162 end
158 163
159 164 def revisions
160 165 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
161 166 end
162 167 end
163 168
164 169 class Info
165 170 attr_accessor :root_url, :lastrev
166 171 def initialize(attributes={})
167 172 self.root_url = attributes[:root_url] if attributes[:root_url]
168 173 self.lastrev = attributes[:lastrev]
169 174 end
170 175 end
171 176
172 177 class Entry
173 178 attr_accessor :name, :path, :kind, :size, :lastrev
174 179 def initialize(attributes={})
175 180 self.name = attributes[:name] if attributes[:name]
176 181 self.path = attributes[:path] if attributes[:path]
177 182 self.kind = attributes[:kind] if attributes[:kind]
178 183 self.size = attributes[:size].to_i if attributes[:size]
179 184 self.lastrev = attributes[:lastrev]
180 185 end
181 186
182 187 def is_file?
183 188 'file' == self.kind
184 189 end
185 190
186 191 def is_dir?
187 192 'dir' == self.kind
188 193 end
189 194
190 195 def is_text?
191 196 Redmine::MimeType.is_type?('text', name)
192 197 end
193 198 end
194 199
195 200 class Revisions < Array
196 201 def latest
197 202 sort {|x,y|
198 203 unless x.time.nil? or y.time.nil?
199 204 x.time <=> y.time
200 205 else
201 206 0
202 207 end
203 208 }.last
204 209 end
205 210 end
206 211
207 212 class Revision
208 213 attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
209 214 def initialize(attributes={})
210 215 self.identifier = attributes[:identifier]
211 216 self.scmid = attributes[:scmid]
212 217 self.name = attributes[:name] || self.identifier
213 218 self.author = attributes[:author]
214 219 self.time = attributes[:time]
215 220 self.message = attributes[:message] || ""
216 221 self.paths = attributes[:paths]
217 222 self.revision = attributes[:revision]
218 223 self.branch = attributes[:branch]
219 224 end
220 225
221 226 end
222 227
223 228 # A line of Diff
224 229 class Diff
225 230 attr_accessor :nb_line_left
226 231 attr_accessor :line_left
227 232 attr_accessor :nb_line_right
228 233 attr_accessor :line_right
229 234 attr_accessor :type_diff_right
230 235 attr_accessor :type_diff_left
231 236
232 237 def initialize ()
233 238 self.nb_line_left = ''
234 239 self.nb_line_right = ''
235 240 self.line_left = ''
236 241 self.line_right = ''
237 242 self.type_diff_right = ''
238 243 self.type_diff_left = ''
239 244 end
240 245
241 246 def inspect
242 247 puts '### Start Line Diff ###'
243 248 puts self.nb_line_left
244 249 puts self.line_left
245 250 puts self.nb_line_right
246 251 puts self.line_right
247 252 end
248 253 end
249 254
250 255 class DiffTableList < Array
251 256 def initialize (diff, type="inline")
252 257 diff_table = DiffTable.new type
253 258 diff.each do |line|
254 259 if line =~ /^(---|\+\+\+) (.*)$/
255 260 self << diff_table if diff_table.length > 1
256 261 diff_table = DiffTable.new type
257 262 end
258 263 a = diff_table.add_line line
259 264 end
260 265 self << diff_table unless diff_table.empty?
261 266 self
262 267 end
263 268 end
264 269
265 270 # Class for create a Diff
266 271 class DiffTable < Hash
267 272 attr_reader :file_name, :line_num_l, :line_num_r
268 273
269 274 # Initialize with a Diff file and the type of Diff View
270 275 # The type view must be inline or sbs (side_by_side)
271 276 def initialize(type="inline")
272 277 @parsing = false
273 278 @nb_line = 1
274 279 @start = false
275 280 @before = 'same'
276 281 @second = true
277 282 @type = type
278 283 end
279 284
280 285 # Function for add a line of this Diff
281 286 def add_line(line)
282 287 unless @parsing
283 288 if line =~ /^(---|\+\+\+) (.*)$/
284 289 @file_name = $2
285 290 return false
286 291 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
287 292 @line_num_l = $5.to_i
288 293 @line_num_r = $2.to_i
289 294 @parsing = true
290 295 end
291 296 else
292 297 if line =~ /^[^\+\-\s@\\]/
293 298 @parsing = false
294 299 return false
295 300 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
296 301 @line_num_l = $5.to_i
297 302 @line_num_r = $2.to_i
298 303 else
299 304 @nb_line += 1 if parse_line(line, @type)
300 305 end
301 306 end
302 307 return true
303 308 end
304 309
305 310 def inspect
306 311 puts '### DIFF TABLE ###'
307 312 puts "file : #{file_name}"
308 313 self.each do |d|
309 314 d.inspect
310 315 end
311 316 end
312 317
313 318 private
314 319 # Test if is a Side By Side type
315 320 def sbs?(type, func)
316 321 if @start and type == "sbs"
317 322 if @before == func and @second
318 323 tmp_nb_line = @nb_line
319 324 self[tmp_nb_line] = Diff.new
320 325 else
321 326 @second = false
322 327 tmp_nb_line = @start
323 328 @start += 1
324 329 @nb_line -= 1
325 330 end
326 331 else
327 332 tmp_nb_line = @nb_line
328 333 @start = @nb_line
329 334 self[tmp_nb_line] = Diff.new
330 335 @second = true
331 336 end
332 337 unless self[tmp_nb_line]
333 338 @nb_line += 1
334 339 self[tmp_nb_line] = Diff.new
335 340 else
336 341 self[tmp_nb_line]
337 342 end
338 343 end
339 344
340 345 # Escape the HTML for the diff
341 346 def escapeHTML(line)
342 347 CGI.escapeHTML(line)
343 348 end
344 349
345 350 def parse_line(line, type="inline")
346 351 if line[0, 1] == "+"
347 352 diff = sbs? type, 'add'
348 353 @before = 'add'
349 354 diff.line_left = escapeHTML line[1..-1]
350 355 diff.nb_line_left = @line_num_l
351 356 diff.type_diff_left = 'diff_in'
352 357 @line_num_l += 1
353 358 true
354 359 elsif line[0, 1] == "-"
355 360 diff = sbs? type, 'remove'
356 361 @before = 'remove'
357 362 diff.line_right = escapeHTML line[1..-1]
358 363 diff.nb_line_right = @line_num_r
359 364 diff.type_diff_right = 'diff_out'
360 365 @line_num_r += 1
361 366 true
362 367 elsif line[0, 1] =~ /\s/
363 368 @before = 'same'
364 369 @start = false
365 370 diff = Diff.new
366 371 diff.line_right = escapeHTML line[1..-1]
367 372 diff.nb_line_right = @line_num_r
368 373 diff.line_left = escapeHTML line[1..-1]
369 374 diff.nb_line_left = @line_num_l
370 375 self[@nb_line] = diff
371 376 @line_num_l += 1
372 377 @line_num_r += 1
373 378 true
374 379 elsif line[0, 1] = "\\"
375 380 true
376 381 else
377 382 false
378 383 end
379 384 end
380 385 end
381 386
382 387 class Annotate
383 388 attr_reader :lines, :revisions
384 389
385 390 def initialize
386 391 @lines = []
387 392 @revisions = []
388 393 end
389 394
390 395 def add_line(line, revision)
391 396 @lines << line
392 397 @revisions << revision
393 398 end
394 399
395 400 def content
396 401 content = lines.join("\n")
397 402 end
398 403
399 404 def empty?
400 405 lines.empty?
401 406 end
402 407 end
403 408 end
404 409 end
405 410 end
@@ -1,199 +1,203
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/abstract_adapter'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class MercurialAdapter < AbstractAdapter
24
24
25 25 # Mercurial executable name
26 26 HG_BIN = "hg"
27 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 TEMPLATE_NAME = "hg-template"
29 TEMPLATE_EXTENSION = "tmpl"
27 30
28 31 def info
29 32 cmd = "#{HG_BIN} -R #{target('')} root"
30 33 root_url = nil
31 34 shellout(cmd) do |io|
32 35 root_url = io.gets
33 36 end
34 37 return nil if $? && $?.exitstatus != 0
35 38 info = Info.new({:root_url => root_url.chomp,
36 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
37 })
39 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
40 })
38 41 info
39 42 rescue CommandFailed
40 43 return nil
41 44 end
42 45
43 46 def entries(path=nil, identifier=nil)
44 47 path ||= ''
45 48 entries = Entries.new
46 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target(path)} locate"
47 cmd << " -r #{identifier.to_i}" if identifier
48 cmd << " " + shell_quote('glob:**')
49 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
50 cmd << " -r " + (identifier ? identifier.to_s : "tip")
51 cmd << " " + shell_quote("path:#{path}") unless path.empty?
49 52 shellout(cmd) do |io|
50 53 io.each_line do |line|
51 e = line.chomp.split(%r{[\/\\]})
52 entries << Entry.new({:name => e.first,
53 :path => (path.empty? ? e.first : "#{path}/#{e.first}"),
54 :kind => (e.size > 1 ? 'dir' : 'file'),
55 :lastrev => Revision.new
56 }) unless entries.detect{|entry| entry.name == e.first}
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{[\/\\]})
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 65 end
58 66 end
59 67 return nil if $? && $?.exitstatus != 0
60 68 entries.sort_by_name
61 69 end
62
63 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
70
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 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 76 if identifier_from && identifier_to
67 77 cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
68 78 elsif identifier_from
69 79 cmd << " -r #{identifier_from.to_i}:"
70 80 end
71 81 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
82 cmd << " #{path}" if path
72 83 shellout(cmd) do |io|
73 changeset = {}
74 parsing_descr = false
75 line_feeds = 0
76
77 io.each_line do |line|
78 if line =~ /^(\w+):\s*(.*)$/
79 key = $1
80 value = $2
81 if parsing_descr && line_feeds > 1
82 parsing_descr = false
83 revisions << build_revision_from_changeset(changeset)
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
84 begin
85 # HG doesn't close the XML Document...
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']
92 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 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 111 end
95 if parsing_descr
96 changeset[:description] << line
97 line_feeds += 1 if line.chomp.empty?
98 end
112 rescue
113 logger.debug($!)
99 114 end
100 # Add the last changeset if there is one left
101 revisions << build_revision_from_changeset(changeset) if changeset[:date]
102 115 end
103 116 return nil if $? && $?.exitstatus != 0
104 117 revisions
105 118 end
106 119
107 120 def diff(path, identifier_from, identifier_to=nil, type="inline")
108 121 path ||= ''
109 122 if identifier_to
110 123 identifier_to = identifier_to.to_i
111 124 else
112 125 identifier_to = identifier_from.to_i - 1
113 126 end
114 127 cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
115 128 cmd << " -I #{target(path)}" unless path.empty?
116 129 diff = []
117 130 shellout(cmd) do |io|
118 131 io.each_line do |line|
119 132 diff << line
120 133 end
121 134 end
122 135 return nil if $? && $?.exitstatus != 0
123 136 DiffTableList.new diff, type
124 137 end
125 138
126 139 def cat(path, identifier=nil)
127 140 cmd = "#{HG_BIN} -R #{target('')} cat"
128 cmd << " -r #{identifier.to_i}" if identifier
141 cmd << " -r " + (identifier ? identifier.to_s : "tip")
129 142 cmd << " #{target(path)}"
130 143 cat = nil
131 144 shellout(cmd) do |io|
132 145 io.binmode
133 146 cat = io.read
134 147 end
135 148 return nil if $? && $?.exitstatus != 0
136 149 cat
137 150 end
138 151
139 152 def annotate(path, identifier=nil)
140 153 path ||= ''
141 154 cmd = "#{HG_BIN} -R #{target('')}"
142 155 cmd << " annotate -n -u"
156 cmd << " -r " + (identifier ? identifier.to_s : "tip")
143 157 cmd << " -r #{identifier.to_i}" if identifier
144 158 cmd << " #{target(path)}"
145 159 blame = Annotate.new
146 160 shellout(cmd) do |io|
147 161 io.each_line do |line|
148 162 next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
149 163 blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
150 164 end
151 165 end
152 166 return nil if $? && $?.exitstatus != 0
153 167 blame
154 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
159 def build_revision_from_changeset(changeset)
160 rev_id = changeset[:changeset].to_s.split(':').first.to_i
161
162 # Changes
163 paths = (rev_id == 0) ?
164 # Can't get changes for revision 0 with hg status
165 changeset[:files].to_s.split.collect{|path| {:action => 'A', :path => "/#{path}"}} :
166 status(rev_id)
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 })
184 def template_path
185 @template ||= begin
186 if hgversion.is_a?(String) or ((hgversion <=> [0,9,5]) > 0)
187 ver = "1.0"
188 else
189 ver = "0.9.5"
190 end
191 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
192 end
175 193 end
176 194
177 # Returns the file changes for a given revision
178 def status(rev_id)
179 cmd = "#{HG_BIN} -R #{target('')} status --rev #{rev_id.to_i - 1}:#{rev_id.to_i}"
180 result = []
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 private
196
197 def hgversion_from_command_line
198 @hgversion ||= %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
195 199 end
196 200 end
197 201 end
198 202 end
199 203 end
@@ -1,55 +1,75
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class RepositoryMercurialTest < Test::Unit::TestCase
21 21 fixtures :projects
22 22
23 23 # No '..' in the repository path
24 24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository'
25 25
26 26 def setup
27 27 @project = Project.find(1)
28 28 assert @repository = Repository::Mercurial.create(:project => @project, :url => REPOSITORY_PATH)
29 29 end
30 30
31 31 if File.directory?(REPOSITORY_PATH)
32 32 def test_fetch_changesets_from_scratch
33 33 @repository.fetch_changesets
34 34 @repository.reload
35 35
36 36 assert_equal 6, @repository.changesets.count
37 37 assert_equal 11, @repository.changes.count
38 38 assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0').comments
39 39 end
40 40
41 41 def test_fetch_changesets_incremental
42 42 @repository.fetch_changesets
43 43 # Remove changesets with revision > 2
44 44 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2}
45 45 @repository.reload
46 46 assert_equal 3, @repository.changesets.count
47 47
48 48 @repository.fetch_changesets
49 49 assert_equal 6, @repository.changesets.count
50 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 71 else
52 72 puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!"
53 73 def test_fake; assert true end
54 74 end
55 75 end
General Comments 0
You need to be logged in to leave comments. Login now