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