##// END OF EJS Templates
scm: use shell quote for scm command at adapter level (#7517, #4273)....
Toshi MARUYAMA -
r4701:8b98c05879a2
parent child
Show More
@@ -1,126 +1,126
1 1 # = Redmine configuration file
2 2 #
3 3 # Each environment has it's own configuration options. If you are only
4 4 # running in production, only the production block needs to be configured.
5 5 # Environment specific configuration options override the default ones.
6 6 #
7 7 # Note that this file needs to be a valid YAML file.
8 8 #
9 9 # == Outgoing email settings (email_delivery setting)
10 10 #
11 11 # === Common configurations
12 12 #
13 13 # ==== Sendmail command
14 14 #
15 15 # production:
16 16 # email_delivery:
17 17 # delivery_method: :sendmail
18 18 #
19 19 # ==== Simple SMTP server at localhost
20 20 #
21 21 # production:
22 22 # email_delivery:
23 23 # delivery_method: :smtp
24 24 # smtp_settings:
25 25 # address: "localhost"
26 26 # port: 25
27 27 #
28 28 # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com
29 29 #
30 30 # production:
31 31 # email_delivery:
32 32 # delivery_method: :smtp
33 33 # smtp_settings:
34 34 # address: "example.com"
35 35 # port: 25
36 36 # authentication: :login
37 37 # domain: 'foo.com'
38 38 # user_name: 'myaccount'
39 39 # password: 'password'
40 40 #
41 41 # ==== SMTP server at example.com using PLAIN authentication
42 42 #
43 43 # production:
44 44 # email_delivery:
45 45 # delivery_method: :smtp
46 46 # smtp_settings:
47 47 # address: "example.com"
48 48 # port: 25
49 49 # authentication: :plain
50 50 # domain: 'example.com'
51 51 # user_name: 'myaccount'
52 52 # password: 'password'
53 53 #
54 54 # ==== SMTP server at using TLS (GMail)
55 55 #
56 56 # This requires some additional configuration. See the article at:
57 57 # http://redmineblog.com/articles/setup-redmine-to-send-email-using-gmail/
58 58 #
59 59 # production:
60 60 # email_delivery:
61 61 # delivery_method: :smtp
62 62 # smtp_settings:
63 63 # tls: true
64 64 # address: "smtp.gmail.com"
65 65 # port: 587
66 66 # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
67 67 # authentication: :plain
68 68 # user_name: "your_email@gmail.com"
69 69 # password: "your_password"
70 70 #
71 71 #
72 72 # === More configuration options
73 73 #
74 74 # See the "Configuration options" at the following website for a list of the
75 75 # full options allowed:
76 76 #
77 77 # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer
78 78
79 79
80 80 # default configuration options for all environments
81 81 default:
82 82 # Outgoing emails configuration (see examples above)
83 83 email_delivery:
84 84 delivery_method: :smtp
85 85 smtp_settings:
86 86 address: smtp.example.net
87 87 port: 25
88 88 domain: example.net
89 89 authentication: :login
90 90 user_name: "redmine@example.net"
91 91 password: "redmine"
92 92
93 93 # Absolute path to the directory where attachments are stored.
94 94 # The default is the 'files' directory in your Redmine instance.
95 95 # Your Redmine instance needs to have write permission on this
96 96 # directory.
97 97 # Examples:
98 98 # attachments_storage_path: /var/redmine/files
99 99 # attachments_storage_path: D:/redmine/files
100 100 attachments_storage_path:
101 101
102 102 # Configuration of the autologin cookie.
103 103 # autologin_cookie_name: the name of the cookie (default: autologin)
104 104 # autologin_cookie_path: the cookie path (default: /)
105 105 # autologin_cookie_secure: true sets the cookie secure flag (default: false)
106 106 autologin_cookie_name:
107 107 autologin_cookie_path:
108 108 autologin_cookie_secure:
109 109
110 110 # Configuration of SCM executable command.
111 111 # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe)
112 112 # On Windows, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work.
113 113 scm_subversion_command: svn # (default: svn)
114 scm_mercurial_command: "\"C:\Program Files\TortoiseHg\hg.exe\"" # (default: hg)
114 scm_mercurial_command: C:\Program Files\TortoiseHg\hg.exe # (default: hg)
115 115 scm_git_command: /usr/local/bin/git # (default: git)
116 116 scm_cvs_command: cvs # (default: cvs)
117 117 scm_bazaar_command: bzr.exe # (default: bzr)
118 118 scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs)
119 119
120 120 # specific configuration options for production environment
121 121 # that overrides the default ones
122 122 production:
123 123
124 124 # specific configuration options for development environment
125 125 # that overrides the default ones
126 126 development:
@@ -1,344 +1,356
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 class << self
28 def client_command
29 ""
30 end
31
28 32 # Returns the version of the scm client
29 33 # Eg: [1, 5, 0] or [] if unknown
30 34 def client_version
31 35 []
32 36 end
33 37
34 38 # Returns the version string of the scm client
35 39 # Eg: '1.5.0' or 'Unknown version' if unknown
36 40 def client_version_string
37 41 v = client_version || 'Unknown version'
38 42 v.is_a?(Array) ? v.join('.') : v.to_s
39 43 end
40 44
41 45 # Returns true if the current client version is above
42 46 # or equals the given one
43 47 # If option is :unknown is set to true, it will return
44 48 # true if the client version is unknown
45 49 def client_version_above?(v, options={})
46 50 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
47 51 end
52
53 def client_available
54 true
55 end
56
57 def shell_quote(str)
58 if Redmine::Platform.mswin?
59 '"' + str.gsub(/"/, '\\"') + '"'
60 else
61 "'" + str.gsub(/'/, "'\"'\"'") + "'"
62 end
63 end
48 64 end
49
65
50 66 def initialize(url, root_url=nil, login=nil, password=nil)
51 67 @url = url
52 68 @login = login if login && !login.empty?
53 69 @password = (password || "") if @login
54 70 @root_url = root_url.blank? ? retrieve_root_url : root_url
55 71 end
56 72
57 73 def adapter_name
58 74 'Abstract'
59 75 end
60 76
61 77 def supports_cat?
62 78 true
63 79 end
64 80
65 81 def supports_annotate?
66 82 respond_to?('annotate')
67 83 end
68 84
69 85 def root_url
70 86 @root_url
71 87 end
72 88
73 89 def url
74 90 @url
75 91 end
76 92
77 93 # get info about the svn repository
78 94 def info
79 95 return nil
80 96 end
81 97
82 98 # Returns the entry identified by path and revision identifier
83 99 # or nil if entry doesn't exist in the repository
84 100 def entry(path=nil, identifier=nil)
85 101 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
86 102 search_path = parts[0..-2].join('/')
87 103 search_name = parts[-1]
88 104 if search_path.blank? && search_name.blank?
89 105 # Root entry
90 106 Entry.new(:path => '', :kind => 'dir')
91 107 else
92 108 # Search for the entry in the parent directory
93 109 es = entries(search_path, identifier)
94 110 es ? es.detect {|e| e.name == search_name} : nil
95 111 end
96 112 end
97 113
98 114 # Returns an Entries collection
99 115 # or nil if the given path doesn't exist in the repository
100 116 def entries(path=nil, identifier=nil)
101 117 return nil
102 118 end
103 119
104 120 def branches
105 121 return nil
106 122 end
107 123
108 124 def tags
109 125 return nil
110 126 end
111 127
112 128 def default_branch
113 129 return nil
114 130 end
115 131
116 132 def properties(path, identifier=nil)
117 133 return nil
118 134 end
119 135
120 136 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
121 137 return nil
122 138 end
123 139
124 140 def diff(path, identifier_from, identifier_to=nil)
125 141 return nil
126 142 end
127 143
128 144 def cat(path, identifier=nil)
129 145 return nil
130 146 end
131 147
132 148 def with_leading_slash(path)
133 149 path ||= ''
134 150 (path[0,1]!="/") ? "/#{path}" : path
135 151 end
136 152
137 153 def with_trailling_slash(path)
138 154 path ||= ''
139 155 (path[-1,1] == "/") ? path : "#{path}/"
140 156 end
141
157
142 158 def without_leading_slash(path)
143 159 path ||= ''
144 160 path.gsub(%r{^/+}, '')
145 161 end
146 162
147 163 def without_trailling_slash(path)
148 164 path ||= ''
149 165 (path[-1,1] == "/") ? path[0..-2] : path
150 166 end
151
167
152 168 def shell_quote(str)
153 if Redmine::Platform.mswin?
154 '"' + str.gsub(/"/, '\\"') + '"'
155 else
156 "'" + str.gsub(/'/, "'\"'\"'") + "'"
157 end
169 self.class.shell_quote(str)
158 170 end
159 171
160 172 private
161 173 def retrieve_root_url
162 174 info = self.info
163 175 info ? info.root_url : nil
164 176 end
165 177
166 178 def target(path)
167 179 path ||= ''
168 180 base = path.match(/^\//) ? root_url : url
169 181 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
170 182 end
171
183
172 184 def logger
173 185 self.class.logger
174 186 end
175
187
176 188 def shellout(cmd, &block)
177 189 self.class.shellout(cmd, &block)
178 190 end
179 191
180 192 def self.logger
181 193 RAILS_DEFAULT_LOGGER
182 194 end
183 195
184 196 def self.shellout(cmd, &block)
185 197 logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug?
186 198 if Rails.env == 'development'
187 199 # Capture stderr when running in dev environment
188 200 cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
189 201 end
190 202 begin
191 203 IO.popen(cmd, "r+") do |io|
192 204 io.close_write
193 205 block.call(io) if block_given?
194 206 end
195 207 rescue Errno::ENOENT => e
196 208 msg = strip_credential(e.message)
197 209 # The command failed, log it and re-raise
198 210 logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
199 211 raise CommandFailed.new(msg)
200 212 end
201 213 end
202 214
203 215 # Hides username/password in a given command
204 216 def self.strip_credential(cmd)
205 217 q = (Redmine::Platform.mswin? ? '"' : "'")
206 218 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
207 219 end
208 220
209 221 def strip_credential(cmd)
210 222 self.class.strip_credential(cmd)
211 223 end
212 224 end
213 225
214 226 class Entries < Array
215 227 def sort_by_name
216 228 sort {|x,y|
217 229 if x.kind == y.kind
218 230 x.name.to_s <=> y.name.to_s
219 231 else
220 232 x.kind <=> y.kind
221 233 end
222 234 }
223 235 end
224 236
225 237 def revisions
226 238 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
227 239 end
228 240 end
229 241
230 242 class Info
231 243 attr_accessor :root_url, :lastrev
232 244 def initialize(attributes={})
233 245 self.root_url = attributes[:root_url] if attributes[:root_url]
234 246 self.lastrev = attributes[:lastrev]
235 247 end
236 248 end
237 249
238 250 class Entry
239 251 attr_accessor :name, :path, :kind, :size, :lastrev
240 252 def initialize(attributes={})
241 253 self.name = attributes[:name] if attributes[:name]
242 254 self.path = attributes[:path] if attributes[:path]
243 255 self.kind = attributes[:kind] if attributes[:kind]
244 256 self.size = attributes[:size].to_i if attributes[:size]
245 257 self.lastrev = attributes[:lastrev]
246 258 end
247 259
248 260 def is_file?
249 261 'file' == self.kind
250 262 end
251 263
252 264 def is_dir?
253 265 'dir' == self.kind
254 266 end
255 267
256 268 def is_text?
257 269 Redmine::MimeType.is_type?('text', name)
258 270 end
259 271 end
260 272
261 273 class Revisions < Array
262 274 def latest
263 275 sort {|x,y|
264 276 unless x.time.nil? or y.time.nil?
265 277 x.time <=> y.time
266 278 else
267 279 0
268 280 end
269 281 }.last
270 282 end
271 283 end
272 284
273 285 class Revision
274 286 attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
275 287 attr_writer :identifier
276 288
277 289 def initialize(attributes={})
278 290 self.identifier = attributes[:identifier]
279 291 self.scmid = attributes[:scmid]
280 292 self.name = attributes[:name] || self.identifier
281 293 self.author = attributes[:author]
282 294 self.time = attributes[:time]
283 295 self.message = attributes[:message] || ""
284 296 self.paths = attributes[:paths]
285 297 self.revision = attributes[:revision]
286 298 self.branch = attributes[:branch]
287 299 end
288 300
289 301 # Returns the identifier of this revision; see also Changeset model
290 302 def identifier
291 303 (@identifier || revision).to_s
292 304 end
293 305
294 306 # Returns the readable identifier.
295 307 def format_identifier
296 308 identifier
297 309 end
298 310
299 311 def save(repo)
300 312 Changeset.transaction do
301 313 changeset = Changeset.new(
302 314 :repository => repo,
303 315 :revision => identifier,
304 316 :scmid => scmid,
305 317 :committer => author,
306 318 :committed_on => time,
307 319 :comments => message)
308 320
309 321 if changeset.save
310 322 paths.each do |file|
311 323 Change.create(
312 324 :changeset => changeset,
313 325 :action => file[:action],
314 326 :path => file[:path])
315 327 end
316 328 end
317 329 end
318 330 end
319 331 end
320 332
321 333 class Annotate
322 334 attr_reader :lines, :revisions
323 335
324 336 def initialize
325 337 @lines = []
326 338 @revisions = []
327 339 end
328 340
329 341 def add_line(line, revision)
330 342 @lines << line
331 343 @revisions << revision
332 344 end
333 345
334 346 def content
335 347 content = lines.join("\n")
336 348 end
337 349
338 350 def empty?
339 351 lines.empty?
340 352 end
341 353 end
342 354 end
343 355 end
344 356 end
@@ -1,189 +1,199
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 module Adapters
22 module Adapters
23 23 class BazaarAdapter < AbstractAdapter
24
24
25 25 # Bazaar executable name
26 26 BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
27
27
28 class << self
29 def client_command
30 @@bin ||= BZR_BIN
31 end
32
33 def sq_bin
34 @@sq_bin ||= shell_quote(BZR_BIN)
35 end
36 end
37
28 38 # Get info about the repository
29 39 def info
30 cmd = "#{BZR_BIN} revno #{target('')}"
40 cmd = "#{self.class.sq_bin} revno #{target('')}"
31 41 info = nil
32 42 shellout(cmd) do |io|
33 43 if io.read =~ %r{^(\d+)\r?$}
34 44 info = Info.new({:root_url => url,
35 45 :lastrev => Revision.new({
36 46 :identifier => $1
37 47 })
38 48 })
39 49 end
40 50 end
41 51 return nil if $? && $?.exitstatus != 0
42 52 info
43 53 rescue CommandFailed
44 54 return nil
45 55 end
46
56
47 57 # Returns an Entries collection
48 58 # or nil if the given path doesn't exist in the repository
49 59 def entries(path=nil, identifier=nil)
50 60 path ||= ''
51 61 entries = Entries.new
52 cmd = "#{BZR_BIN} ls -v --show-ids"
62 cmd = "#{self.class.sq_bin} ls -v --show-ids"
53 63 identifier = -1 unless identifier && identifier.to_i > 0
54 64 cmd << " -r#{identifier.to_i}"
55 65 cmd << " #{target(path)}"
56 66 shellout(cmd) do |io|
57 67 prefix = "#{url}/#{path}".gsub('\\', '/')
58 68 logger.debug "PREFIX: #{prefix}"
59 69 re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
60 70 io.each_line do |line|
61 71 next unless line =~ re
62 72 entries << Entry.new({:name => $3.strip,
63 73 :path => ((path.empty? ? "" : "#{path}/") + $3.strip),
64 74 :kind => ($4.blank? ? 'file' : 'dir'),
65 75 :size => nil,
66 76 :lastrev => Revision.new(:revision => $5.strip)
67 77 })
68 78 end
69 79 end
70 80 return nil if $? && $?.exitstatus != 0
71 81 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
72 82 entries.sort_by_name
73 83 end
74
84
75 85 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
76 86 path ||= ''
77 87 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
78 88 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
79 89 revisions = Revisions.new
80 cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to}..#{identifier_from} #{target(path)}"
90 cmd = "#{self.class.sq_bin} log -v --show-ids -r#{identifier_to}..#{identifier_from} #{target(path)}"
81 91 shellout(cmd) do |io|
82 92 revision = nil
83 93 parsing = nil
84 94 io.each_line do |line|
85 95 if line =~ /^----/
86 96 revisions << revision if revision
87 97 revision = Revision.new(:paths => [], :message => '')
88 98 parsing = nil
89 99 else
90 100 next unless revision
91 101
92 102 if line =~ /^revno: (\d+)($|\s\[merge\]$)/
93 103 revision.identifier = $1.to_i
94 104 elsif line =~ /^committer: (.+)$/
95 105 revision.author = $1.strip
96 106 elsif line =~ /^revision-id:(.+)$/
97 107 revision.scmid = $1.strip
98 108 elsif line =~ /^timestamp: (.+)$/
99 109 revision.time = Time.parse($1).localtime
100 110 elsif line =~ /^ -----/
101 111 # partial revisions
102 112 parsing = nil unless parsing == 'message'
103 113 elsif line =~ /^(message|added|modified|removed|renamed):/
104 114 parsing = $1
105 115 elsif line =~ /^ (.*)$/
106 116 if parsing == 'message'
107 117 revision.message << "#{$1}\n"
108 118 else
109 119 if $1 =~ /^(.*)\s+(\S+)$/
110 120 path = $1.strip
111 121 revid = $2
112 122 case parsing
113 123 when 'added'
114 124 revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
115 125 when 'modified'
116 126 revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
117 127 when 'removed'
118 128 revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
119 129 when 'renamed'
120 130 new_path = path.split('=>').last
121 131 revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
122 132 end
123 133 end
124 134 end
125 135 else
126 136 parsing = nil
127 137 end
128 138 end
129 139 end
130 140 revisions << revision if revision
131 141 end
132 142 return nil if $? && $?.exitstatus != 0
133 143 revisions
134 144 end
135
145
136 146 def diff(path, identifier_from, identifier_to=nil)
137 147 path ||= ''
138 148 if identifier_to
139 149 identifier_to = identifier_to.to_i
140 150 else
141 151 identifier_to = identifier_from.to_i - 1
142 152 end
143 153 if identifier_from
144 154 identifier_from = identifier_from.to_i
145 155 end
146 cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
156 cmd = "#{self.class.sq_bin} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
147 157 diff = []
148 158 shellout(cmd) do |io|
149 159 io.each_line do |line|
150 160 diff << line
151 161 end
152 162 end
153 163 #return nil if $? && $?.exitstatus != 0
154 164 diff
155 165 end
156
166
157 167 def cat(path, identifier=nil)
158 cmd = "#{BZR_BIN} cat"
168 cmd = "#{self.class.sq_bin} cat"
159 169 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
160 170 cmd << " #{target(path)}"
161 171 cat = nil
162 172 shellout(cmd) do |io|
163 173 io.binmode
164 174 cat = io.read
165 175 end
166 176 return nil if $? && $?.exitstatus != 0
167 177 cat
168 178 end
169
179
170 180 def annotate(path, identifier=nil)
171 cmd = "#{BZR_BIN} annotate --all"
181 cmd = "#{self.class.sq_bin} annotate --all"
172 182 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
173 183 cmd << " #{target(path)}"
174 184 blame = Annotate.new
175 185 shellout(cmd) do |io|
176 186 author = nil
177 187 identifier = nil
178 188 io.each_line do |line|
179 189 next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
180 190 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
181 191 end
182 192 end
183 193 return nil if $? && $?.exitstatus != 0
184 194 blame
185 195 end
186 196 end
187 197 end
188 198 end
189 199 end
@@ -1,371 +1,381
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 CvsAdapter < AbstractAdapter
24 24
25 25 # CVS executable name
26 26 CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
27
27
28 class << self
29 def client_command
30 @@bin ||= CVS_BIN
31 end
32
33 def sq_bin
34 @@sq_bin ||= shell_quote(CVS_BIN)
35 end
36 end
37
28 38 # Guidelines for the input:
29 39 # url -> the project-path, relative to the cvsroot (eg. module name)
30 40 # root_url -> the good old, sometimes damned, CVSROOT
31 41 # login -> unnecessary
32 42 # password -> unnecessary too
33 43 def initialize(url, root_url=nil, login=nil, password=nil)
34 44 @url = url
35 45 @login = login if login && !login.empty?
36 46 @password = (password || "") if @login
37 47 #TODO: better Exception here (IllegalArgumentException)
38 48 raise CommandFailed if root_url.blank?
39 49 @root_url = root_url
40 50 end
41
51
42 52 def root_url
43 53 @root_url
44 54 end
45
55
46 56 def url
47 57 @url
48 58 end
49
59
50 60 def info
51 61 logger.debug "<cvs> info"
52 62 Info.new({:root_url => @root_url, :lastrev => nil})
53 63 end
54
64
55 65 def get_previous_revision(revision)
56 66 CvsRevisionHelper.new(revision).prevRev
57 67 end
58 68
59 69 # Returns an Entries collection
60 70 # or nil if the given path doesn't exist in the repository
61 71 # this method is used by the repository-browser (aka LIST)
62 72 def entries(path=nil, identifier=nil)
63 73 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
64 74 path_with_project="#{url}#{with_leading_slash(path)}"
65 75 entries = Entries.new
66 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rls -e"
76 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rls -e"
67 77 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
68 78 cmd << " #{shell_quote path_with_project}"
69 79 shellout(cmd) do |io|
70 80 io.each_line(){|line|
71 81 fields=line.chop.split('/',-1)
72 82 logger.debug(">>InspectLine #{fields.inspect}")
73 83
74 84 if fields[0]!="D"
75 85 entries << Entry.new({:name => fields[-5],
76 86 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
77 87 :path => "#{path}/#{fields[-5]}",
78 88 :kind => 'file',
79 89 :size => nil,
80 90 :lastrev => Revision.new({
81 91 :revision => fields[-4],
82 92 :name => fields[-4],
83 93 :time => Time.parse(fields[-3]),
84 94 :author => ''
85 95 })
86 96 })
87 97 else
88 98 entries << Entry.new({:name => fields[1],
89 99 :path => "#{path}/#{fields[1]}",
90 100 :kind => 'dir',
91 101 :size => nil,
92 102 :lastrev => nil
93 103 })
94 104 end
95 105 }
96 106 end
97 107 return nil if $? && $?.exitstatus != 0
98 108 entries.sort_by_name
99 109 end
100 110
101 111 STARTLOG="----------------------------"
102 112 ENDLOG ="============================================================================="
103 113
104 114 # Returns all revisions found between identifier_from and identifier_to
105 115 # in the repository. both identifier have to be dates or nil.
106 116 # these method returns nothing but yield every result in block
107 117 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
108 118 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
109 119
110 120 path_with_project="#{url}#{with_leading_slash(path)}"
111 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rlog"
121 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rlog"
112 122 cmd << " -d\">#{time_to_cvstime_rlog(identifier_from)}\"" if identifier_from
113 123 cmd << " #{shell_quote path_with_project}"
114 124 shellout(cmd) do |io|
115 125 state="entry_start"
116 126
117 127 commit_log=String.new
118 128 revision=nil
119 129 date=nil
120 130 author=nil
121 131 entry_path=nil
122 132 entry_name=nil
123 133 file_state=nil
124 134 branch_map=nil
125 135
126 136 io.each_line() do |line|
127 137
128 138 if state!="revision" && /^#{ENDLOG}/ =~ line
129 139 commit_log=String.new
130 140 revision=nil
131 141 state="entry_start"
132 142 end
133 143
134 144 if state=="entry_start"
135 145 branch_map=Hash.new
136 146 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
137 147 entry_path = normalize_cvs_path($1)
138 148 entry_name = normalize_path(File.basename($1))
139 149 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
140 150 elsif /^head: (.+)$/ =~ line
141 151 entry_headRev = $1 #unless entry.nil?
142 152 elsif /^symbolic names:/ =~ line
143 153 state="symbolic" #unless entry.nil?
144 154 elsif /^#{STARTLOG}/ =~ line
145 155 commit_log=String.new
146 156 state="revision"
147 157 end
148 158 next
149 159 elsif state=="symbolic"
150 160 if /^(.*):\s(.*)/ =~ (line.strip)
151 161 branch_map[$1]=$2
152 162 else
153 163 state="tags"
154 164 next
155 165 end
156 166 elsif state=="tags"
157 167 if /^#{STARTLOG}/ =~ line
158 168 commit_log = ""
159 169 state="revision"
160 170 elsif /^#{ENDLOG}/ =~ line
161 171 state="head"
162 172 end
163 173 next
164 174 elsif state=="revision"
165 175 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
166 176 if revision
167 177
168 178 revHelper=CvsRevisionHelper.new(revision)
169 179 revBranch="HEAD"
170 180
171 181 branch_map.each() do |branch_name,branch_point|
172 182 if revHelper.is_in_branch_with_symbol(branch_point)
173 183 revBranch=branch_name
174 184 end
175 185 end
176 186
177 187 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
178 188
179 189 yield Revision.new({
180 190 :time => date,
181 191 :author => author,
182 192 :message=>commit_log.chomp,
183 193 :paths => [{
184 194 :revision => revision,
185 195 :branch=> revBranch,
186 196 :path=>entry_path,
187 197 :name=>entry_name,
188 198 :kind=>'file',
189 199 :action=>file_state
190 200 }]
191 201 })
192 202 end
193 203
194 204 commit_log=String.new
195 205 revision=nil
196 206
197 207 if /^#{ENDLOG}/ =~ line
198 208 state="entry_start"
199 209 end
200 210 next
201 211 end
202 212
203 213 if /^branches: (.+)$/ =~ line
204 214 #TODO: version.branch = $1
205 215 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
206 216 revision = $1
207 217 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
208 218 date = Time.parse($1)
209 219 author = /author: ([^;]+)/.match(line)[1]
210 220 file_state = /state: ([^;]+)/.match(line)[1]
211 221 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
212 222 # useful for stats or something else
213 223 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
214 224 # unless linechanges.nil?
215 225 # version.line_plus = linechanges[1]
216 226 # version.line_minus = linechanges[2]
217 227 # else
218 228 # version.line_plus = 0
219 229 # version.line_minus = 0
220 230 # end
221 231 else
222 232 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
223 233 end
224 234 end
225 235 end
226 236 end
227 237 end
228 238
229 239 def diff(path, identifier_from, identifier_to=nil)
230 240 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
231 241 path_with_project="#{url}#{with_leading_slash(path)}"
232 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
242 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
233 243 diff = []
234 244 shellout(cmd) do |io|
235 245 io.each_line do |line|
236 246 diff << line
237 247 end
238 248 end
239 249 return nil if $? && $?.exitstatus != 0
240 250 diff
241 251 end
242 252
243 253 def cat(path, identifier=nil)
244 254 identifier = (identifier) ? identifier : "HEAD"
245 255 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
246 256 path_with_project="#{url}#{with_leading_slash(path)}"
247 cmd = "#{CVS_BIN} -d #{shell_quote root_url} co"
257 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} co"
248 258 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
249 259 cmd << " -p #{shell_quote path_with_project}"
250 260 cat = nil
251 261 shellout(cmd) do |io|
252 262 io.binmode
253 263 cat = io.read
254 264 end
255 265 return nil if $? && $?.exitstatus != 0
256 266 cat
257 267 end
258 268
259 269 def annotate(path, identifier=nil)
260 270 identifier = (identifier) ? identifier.to_i : "HEAD"
261 271 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
262 272 path_with_project="#{url}#{with_leading_slash(path)}"
263 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
273 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
264 274 blame = Annotate.new
265 275 shellout(cmd) do |io|
266 276 io.each_line do |line|
267 277 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
268 278 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
269 279 end
270 280 end
271 281 return nil if $? && $?.exitstatus != 0
272 282 blame
273 283 end
274 284
275 285 private
276 286
277 287 # Returns the root url without the connexion string
278 288 # :pserver:anonymous@foo.bar:/path => /path
279 289 # :ext:cvsservername:/path => /path
280 290 def root_url_path
281 291 root_url.to_s.gsub(/^:.+:\d*/, '')
282 292 end
283 293
284 294 # convert a date/time into the CVS-format
285 295 def time_to_cvstime(time)
286 296 return nil if time.nil?
287 297 return Time.now if time == 'HEAD'
288 298
289 299 unless time.kind_of? Time
290 300 time = Time.parse(time)
291 301 end
292 302 return time.strftime("%Y-%m-%d %H:%M:%S")
293 303 end
294 304
295 305 def time_to_cvstime_rlog(time)
296 306 return nil if time.nil?
297 307 t1 = time.clone.localtime
298 308 return t1.strftime("%Y-%m-%d %H:%M:%S")
299 309 end
300 310
301 311 def normalize_cvs_path(path)
302 312 normalize_path(path.gsub(/Attic\//,''))
303 313 end
304 314
305 315 def normalize_path(path)
306 316 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
307 317 end
308 318 end
309 319
310 320 class CvsRevisionHelper
311 321 attr_accessor :complete_rev, :revision, :base, :branchid
312 322
313 323 def initialize(complete_rev)
314 324 @complete_rev = complete_rev
315 325 parseRevision()
316 326 end
317 327
318 328 def branchPoint
319 329 return @base
320 330 end
321 331
322 332 def branchVersion
323 333 if isBranchRevision
324 334 return @base+"."+@branchid
325 335 end
326 336 return @base
327 337 end
328 338
329 339 def isBranchRevision
330 340 !@branchid.nil?
331 341 end
332 342
333 343 def prevRev
334 344 unless @revision==0
335 345 return buildRevision(@revision-1)
336 346 end
337 347 return buildRevision(@revision)
338 348 end
339 349
340 350 def is_in_branch_with_symbol(branch_symbol)
341 351 bpieces=branch_symbol.split(".")
342 352 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
343 353 return (branchVersion==branch_start)
344 354 end
345 355
346 356 private
347 357 def buildRevision(rev)
348 358 if rev== 0
349 359 @base
350 360 elsif @branchid.nil?
351 361 @base+"."+rev.to_s
352 362 else
353 363 @base+"."+@branchid+"."+rev.to_s
354 364 end
355 365 end
356 366
357 367 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
358 368 def parseRevision()
359 369 pieces=@complete_rev.split(".")
360 370 @revision=pieces.last.to_i
361 371 baseSize=1
362 372 baseSize+=(pieces.size/2)
363 373 @base=pieces[0..-baseSize].join(".")
364 374 if baseSize > 2
365 375 @branchid=pieces[-2]
366 376 end
367 377 end
368 378 end
369 379 end
370 380 end
371 381 end
@@ -1,225 +1,233
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 require 'rexml/document'
20 20
21 21 module Redmine
22 22 module Scm
23 module Adapters
23 module Adapters
24 24 class DarcsAdapter < AbstractAdapter
25 25 # Darcs executable name
26 26 DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
27
27
28 28 class << self
29 def client_command
30 @@bin ||= DARCS_BIN
31 end
32
33 def sq_bin
34 @@sq_bin ||= shell_quote(DARCS_BIN)
35 end
36
29 37 def client_version
30 38 @@client_version ||= (darcs_binary_version || [])
31 39 end
32
40
33 41 def darcs_binary_version
34 42 darcsversion = darcs_binary_version_from_command_line
35 43 if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
36 44 m[2].scan(%r{\d+}).collect(&:to_i)
37 45 end
38 46 end
39 47
40 48 def darcs_binary_version_from_command_line
41 shellout("#{DARCS_BIN} --version") { |io| io.read }.to_s
49 shellout("#{sq_bin} --version") { |io| io.read }.to_s
42 50 end
43 51 end
44 52
45 53 def initialize(url, root_url=nil, login=nil, password=nil)
46 54 @url = url
47 55 @root_url = url
48 56 end
49 57
50 58 def supports_cat?
51 59 # cat supported in darcs 2.0.0 and higher
52 60 self.class.client_version_above?([2, 0, 0])
53 61 end
54 62
55 63 # Get info about the darcs repository
56 64 def info
57 65 rev = revisions(nil,nil,nil,{:limit => 1})
58 66 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
59 67 end
60
68
61 69 # Returns an Entries collection
62 70 # or nil if the given path doesn't exist in the repository
63 71 def entries(path=nil, identifier=nil)
64 72 path_prefix = (path.blank? ? '' : "#{path}/")
65 73 if path.blank?
66 74 path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
67 75 end
68 76 entries = Entries.new
69 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --xml-output"
77 cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
70 78 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
71 79 cmd << " #{shell_quote path}"
72 80 shellout(cmd) do |io|
73 81 begin
74 82 doc = REXML::Document.new(io)
75 83 if doc.root.name == 'directory'
76 84 doc.elements.each('directory/*') do |element|
77 85 next unless ['file', 'directory'].include? element.name
78 86 entries << entry_from_xml(element, path_prefix)
79 87 end
80 88 elsif doc.root.name == 'file'
81 89 entries << entry_from_xml(doc.root, path_prefix)
82 90 end
83 91 rescue
84 92 end
85 93 end
86 94 return nil if $? && $?.exitstatus != 0
87 95 entries.compact.sort_by_name
88 96 end
89
97
90 98 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
91 99 path = '.' if path.blank?
92 100 revisions = Revisions.new
93 cmd = "#{DARCS_BIN} changes --repodir #{shell_quote @url} --xml-output"
101 cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
94 102 cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
95 103 cmd << " --last #{options[:limit].to_i}" if options[:limit]
96 104 shellout(cmd) do |io|
97 105 begin
98 106 doc = REXML::Document.new(io)
99 107 doc.elements.each("changelog/patch") do |patch|
100 108 message = patch.elements['name'].text
101 109 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
102 110 revisions << Revision.new({:identifier => nil,
103 111 :author => patch.attributes['author'],
104 112 :scmid => patch.attributes['hash'],
105 113 :time => Time.parse(patch.attributes['local_date']),
106 114 :message => message,
107 115 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
108 116 })
109 117 end
110 118 rescue
111 119 end
112 120 end
113 121 return nil if $? && $?.exitstatus != 0
114 122 revisions
115 123 end
116
124
117 125 def diff(path, identifier_from, identifier_to=nil)
118 126 path = '*' if path.blank?
119 cmd = "#{DARCS_BIN} diff --repodir #{shell_quote @url}"
127 cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
120 128 if identifier_to.nil?
121 129 cmd << " --match #{shell_quote("hash #{identifier_from}")}"
122 130 else
123 131 cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
124 132 cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
125 133 end
126 134 cmd << " -u #{shell_quote path}"
127 135 diff = []
128 136 shellout(cmd) do |io|
129 137 io.each_line do |line|
130 138 diff << line
131 139 end
132 140 end
133 141 return nil if $? && $?.exitstatus != 0
134 142 diff
135 143 end
136
144
137 145 def cat(path, identifier=nil)
138 cmd = "#{DARCS_BIN} show content --repodir #{shell_quote @url}"
146 cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
139 147 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
140 148 cmd << " #{shell_quote path}"
141 149 cat = nil
142 150 shellout(cmd) do |io|
143 151 io.binmode
144 152 cat = io.read
145 153 end
146 154 return nil if $? && $?.exitstatus != 0
147 155 cat
148 156 end
149 157
150 158 private
151
159
152 160 # Returns an Entry from the given XML element
153 161 # or nil if the entry was deleted
154 162 def entry_from_xml(element, path_prefix)
155 163 modified_element = element.elements['modified']
156 164 if modified_element.elements['modified_how'].text.match(/removed/)
157 165 return nil
158 166 end
159 167
160 168 Entry.new({:name => element.attributes['name'],
161 169 :path => path_prefix + element.attributes['name'],
162 170 :kind => element.name == 'file' ? 'file' : 'dir',
163 171 :size => nil,
164 172 :lastrev => Revision.new({
165 173 :identifier => nil,
166 174 :scmid => modified_element.elements['patch'].attributes['hash']
167 175 })
168 176 })
169 177 end
170 178
171 179 def get_paths_for_patch(hash)
172 180 paths = get_paths_for_patch_raw(hash)
173 181 if self.class.client_version_above?([2, 4])
174 182 orig_paths = paths
175 183 paths = []
176 184 add_paths = []
177 185 add_paths_name = []
178 186 mod_paths = []
179 187 other_paths = []
180 188 orig_paths.each do |path|
181 189 if path[:action] == 'A'
182 190 add_paths << path
183 191 add_paths_name << path[:path]
184 192 elsif path[:action] == 'M'
185 193 mod_paths << path
186 194 else
187 195 other_paths << path
188 196 end
189 197 end
190 198 add_paths_name.each do |add_path|
191 199 mod_paths.delete_if { |m| m[:path] == add_path }
192 200 end
193 201 paths.concat add_paths
194 202 paths.concat mod_paths
195 203 paths.concat other_paths
196 204 end
197 205 paths
198 206 end
199
207
200 208 # Retrieve changed paths for a single patch
201 209 def get_paths_for_patch_raw(hash)
202 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --summary --xml-output"
210 cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
203 211 cmd << " --match #{shell_quote("hash #{hash}")} "
204 212 paths = []
205 213 shellout(cmd) do |io|
206 214 begin
207 215 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
208 216 # A root element is added so that REXML doesn't raise an error
209 217 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
210 218 doc.elements.each('fake_root/summary/*') do |modif|
211 219 paths << {:action => modif.name[0,1].upcase,
212 220 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
213 221 }
214 222 end
215 223 rescue
216 224 end
217 225 end
218 226 paths
219 227 rescue CommandFailed
220 228 paths
221 229 end
222 230 end
223 231 end
224 232 end
225 233 end
@@ -1,93 +1,98
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # FileSystem adapter
5 5 # File written by Paul Rivier, at Demotera.
6 6 #
7 7 # This program is free software; you can redistribute it and/or
8 8 # modify it under the terms of the GNU General Public License
9 9 # as published by the Free Software Foundation; either version 2
10 10 # of the License, or (at your option) any later version.
11 11 #
12 12 # This program is distributed in the hope that it will be useful,
13 13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 15 # GNU General Public License for more details.
16 16 #
17 17 # You should have received a copy of the GNU General Public License
18 18 # along with this program; if not, write to the Free Software
19 19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 20
21 21 require 'redmine/scm/adapters/abstract_adapter'
22 22 require 'find'
23 23
24 24 module Redmine
25 25 module Scm
26 26 module Adapters
27 27 class FilesystemAdapter < AbstractAdapter
28
28
29 class << self
30 def client_available
31 true
32 end
33 end
29 34
30 35 def initialize(url, root_url=nil, login=nil, password=nil)
31 36 @url = with_trailling_slash(url)
32 37 end
33 38
34 39 def format_path_ends(path, leading=true, trailling=true)
35 40 path = leading ? with_leading_slash(path) :
36 41 without_leading_slash(path)
37 42 trailling ? with_trailling_slash(path) :
38 43 without_trailling_slash(path)
39 44 end
40 45
41 46 def info
42 47 info = Info.new({:root_url => target(),
43 48 :lastrev => nil
44 49 })
45 50 info
46 51 rescue CommandFailed
47 52 return nil
48 53 end
49 54
50 55 def entries(path="", identifier=nil)
51 56 entries = Entries.new
52 57 Dir.new(target(path)).each do |e|
53 58 relative_path = format_path_ends((format_path_ends(path,
54 59 false,
55 60 true) + e),
56 61 false,false)
57 62 target = target(relative_path)
58 63 entries <<
59 64 Entry.new({ :name => File.basename(e),
60 65 # below : list unreadable files, but dont link them.
61 66 :path => File.readable?(target) ? relative_path : "",
62 67 :kind => (File.directory?(target) ? 'dir' : 'file'),
63 68 :size => (File.directory?(target) ? nil : [File.size(target)].pack('l').unpack('L').first),
64 69 :lastrev =>
65 70 Revision.new({:time => (File.mtime(target)).localtime,
66 71 })
67 72 }) if File.exist?(target) and # paranoid test
68 73 %w{file directory}.include?(File.ftype(target)) and # avoid special types
69 74 not File.basename(e).match(/^\.+$/) # avoid . and ..
70 75 end
71 76 entries.sort_by_name
72 77 end
73 78
74 79 def cat(path, identifier=nil)
75 80 File.new(target(path), "rb").read
76 81 end
77 82
78 83 private
79 84
80 85 # AbstractAdapter::target is implicitly made to quote paths.
81 86 # Here we do not shell-out, so we do not want quotes.
82 87 def target(path=nil)
83 88 #Prevent the use of ..
84 89 if path and !path.match(/(^|\/)\.\.(\/|$)/)
85 90 return "#{self.url}#{without_leading_slash(path)}"
86 91 end
87 92 return self.url
88 93 end
89 94
90 95 end
91 96 end
92 97 end
93 98 end
@@ -1,277 +1,291
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 module Adapters
22 module Adapters
23 23 class GitAdapter < AbstractAdapter
24 24 # Git executable name
25 25 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
26 26
27 class << self
28 def client_command
29 @@bin ||= GIT_BIN
30 end
31
32 def sq_bin
33 @@sq_bin ||= shell_quote(GIT_BIN)
34 end
35
36 def client_available
37 !client_version.empty?
38 end
39 end
40
27 41 def info
28 42 begin
29 43 Info.new(:root_url => url, :lastrev => lastrev('',nil))
30 44 rescue
31 45 nil
32 46 end
33 47 end
34 48
35 49 def branches
36 50 return @branches if @branches
37 51 @branches = []
38 cmd = "#{GIT_BIN} --git-dir #{target('')} branch --no-color"
52 cmd = "#{self.class.sq_bin} --git-dir #{target('')} branch --no-color"
39 53 shellout(cmd) do |io|
40 54 io.each_line do |line|
41 55 @branches << line.match('\s*\*?\s*(.*)$')[1]
42 56 end
43 57 end
44 58 @branches.sort!
45 59 end
46 60
47 61 def tags
48 62 return @tags if @tags
49 cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
63 cmd = "#{self.class.sq_bin} --git-dir #{target('')} tag"
50 64 shellout(cmd) do |io|
51 65 @tags = io.readlines.sort!.map{|t| t.strip}
52 66 end
53 67 end
54 68
55 69 def default_branch
56 70 branches.include?('master') ? 'master' : branches.first
57 71 end
58 72
59 73 def entries(path=nil, identifier=nil)
60 74 path ||= ''
61 75 entries = Entries.new
62 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
76 cmd = "#{self.class.sq_bin} --git-dir #{target('')} ls-tree -l "
63 77 cmd << shell_quote("HEAD:" + path) if identifier.nil?
64 78 cmd << shell_quote(identifier + ":" + path) if identifier
65 79 shellout(cmd) do |io|
66 80 io.each_line do |line|
67 81 e = line.chomp.to_s
68 82 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
69 83 type = $1
70 84 sha = $2
71 85 size = $3
72 86 name = $4
73 87 full_path = path.empty? ? name : "#{path}/#{name}"
74 88 entries << Entry.new({:name => name,
75 89 :path => full_path,
76 90 :kind => (type == "tree") ? 'dir' : 'file',
77 91 :size => (type == "tree") ? nil : size,
78 92 :lastrev => lastrev(full_path,identifier)
79 93 }) unless entries.detect{|entry| entry.name == name}
80 94 end
81 95 end
82 96 end
83 97 return nil if $? && $?.exitstatus != 0
84 98 entries.sort_by_name
85 99 end
86 100
87 101 def lastrev(path,rev)
88 102 return nil if path.nil?
89 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
103 cmd = "#{self.class.sq_bin} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
90 104 cmd << " #{shell_quote rev} " if rev
91 105 cmd << "-- #{shell_quote path} " unless path.empty?
92 106 lines = []
93 107 shellout(cmd) { |io| lines = io.readlines }
94 108 return nil if $? && $?.exitstatus != 0
95 109 begin
96 110 id = lines[0].split[1]
97 111 author = lines[1].match('Author:\s+(.*)$')[1]
98 112 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1]).localtime
99 113
100 114 Revision.new({
101 115 :identifier => id,
102 116 :scmid => id,
103 117 :author => author,
104 118 :time => time,
105 119 :message => nil,
106 120 :paths => nil
107 121 })
108 122 rescue NoMethodError => e
109 123 logger.error("The revision '#{path}' has a wrong format")
110 124 return nil
111 125 end
112 126 end
113 127
114 128 def revisions(path, identifier_from, identifier_to, options={})
115 129 revisions = Revisions.new
116 130
117 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --raw --date=iso --pretty=fuller "
131 cmd = "#{self.class.sq_bin} --git-dir #{target('')} log --no-color --raw --date=iso --pretty=fuller "
118 132 cmd << " --reverse " if options[:reverse]
119 133 cmd << " --all " if options[:all]
120 134 cmd << " -n #{options[:limit].to_i} " if options[:limit]
121 135 cmd << "#{shell_quote(identifier_from + '..')}" if identifier_from
122 136 cmd << "#{shell_quote identifier_to}" if identifier_to
123 137 cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since]
124 138 cmd << " -- #{shell_quote path}" if path && !path.empty?
125 139
126 140 shellout(cmd) do |io|
127 141 files=[]
128 142 changeset = {}
129 143 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
130 144 revno = 1
131 145
132 146 io.each_line do |line|
133 147 if line =~ /^commit ([0-9a-f]{40})$/
134 148 key = "commit"
135 149 value = $1
136 150 if (parsing_descr == 1 || parsing_descr == 2)
137 151 parsing_descr = 0
138 152 revision = Revision.new({
139 153 :identifier => changeset[:commit],
140 154 :scmid => changeset[:commit],
141 155 :author => changeset[:author],
142 156 :time => Time.parse(changeset[:date]),
143 157 :message => changeset[:description],
144 158 :paths => files
145 159 })
146 160 if block_given?
147 161 yield revision
148 162 else
149 163 revisions << revision
150 164 end
151 165 changeset = {}
152 166 files = []
153 167 revno = revno + 1
154 168 end
155 169 changeset[:commit] = $1
156 170 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
157 171 key = $1
158 172 value = $2
159 173 if key == "Author"
160 174 changeset[:author] = value
161 175 elsif key == "CommitDate"
162 176 changeset[:date] = value
163 177 end
164 178 elsif (parsing_descr == 0) && line.chomp.to_s == ""
165 179 parsing_descr = 1
166 180 changeset[:description] = ""
167 181 elsif (parsing_descr == 1 || parsing_descr == 2) \
168 182 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
169 183 parsing_descr = 2
170 184 fileaction = $1
171 185 filepath = $2
172 186 files << {:action => fileaction, :path => filepath}
173 187 elsif (parsing_descr == 1 || parsing_descr == 2) \
174 188 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
175 189 parsing_descr = 2
176 190 fileaction = $1
177 191 filepath = $3
178 192 files << {:action => fileaction, :path => filepath}
179 193 elsif (parsing_descr == 1) && line.chomp.to_s == ""
180 194 parsing_descr = 2
181 195 elsif (parsing_descr == 1)
182 196 changeset[:description] << line[4..-1]
183 197 end
184 198 end
185 199
186 200 if changeset[:commit]
187 201 revision = Revision.new({
188 202 :identifier => changeset[:commit],
189 203 :scmid => changeset[:commit],
190 204 :author => changeset[:author],
191 205 :time => Time.parse(changeset[:date]),
192 206 :message => changeset[:description],
193 207 :paths => files
194 208 })
195 209
196 210 if block_given?
197 211 yield revision
198 212 else
199 213 revisions << revision
200 214 end
201 215 end
202 216 end
203 217
204 218 return nil if $? && $?.exitstatus != 0
205 219 revisions
206 220 end
207 221
208 222 def diff(path, identifier_from, identifier_to=nil)
209 223 path ||= ''
210 224
211 225 if identifier_to
212 cmd = "#{GIT_BIN} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
226 cmd = "#{self.class.sq_bin} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
213 227 else
214 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
228 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
215 229 end
216 230
217 231 cmd << " -- #{shell_quote path}" unless path.empty?
218 232 diff = []
219 233 shellout(cmd) do |io|
220 234 io.each_line do |line|
221 235 diff << line
222 236 end
223 237 end
224 238 return nil if $? && $?.exitstatus != 0
225 239 diff
226 240 end
227 241
228 242 def annotate(path, identifier=nil)
229 243 identifier = 'HEAD' if identifier.blank?
230 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
244 cmd = "#{self.class.sq_bin} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
231 245 blame = Annotate.new
232 246 content = nil
233 247 shellout(cmd) { |io| io.binmode; content = io.read }
234 248 return nil if $? && $?.exitstatus != 0
235 249 # git annotates binary files
236 250 return nil if content.is_binary_data?
237 251 identifier = ''
238 252 # git shows commit author on the first occurrence only
239 253 authors_by_commit = {}
240 254 content.split("\n").each do |line|
241 255 if line =~ /^([0-9a-f]{39,40})\s.*/
242 256 identifier = $1
243 257 elsif line =~ /^author (.+)/
244 258 authors_by_commit[identifier] = $1.strip
245 259 elsif line =~ /^\t(.*)/
246 260 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
247 261 identifier = ''
248 262 author = ''
249 263 end
250 264 end
251 265 blame
252 266 end
253 267
254 268 def cat(path, identifier=nil)
255 269 if identifier.nil?
256 270 identifier = 'HEAD'
257 271 end
258 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
272 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
259 273 cat = nil
260 274 shellout(cmd) do |io|
261 275 io.binmode
262 276 cat = io.read
263 277 end
264 278 return nil if $? && $?.exitstatus != 0
265 279 cat
266 280 end
267 281
268 282 class Revision < Redmine::Scm::Adapters::Revision
269 283 # Returns the readable identifier
270 284 def format_identifier
271 285 identifier[0,8]
272 286 end
273 287 end
274 288 end
275 289 end
276 290 end
277 291 end
@@ -1,225 +1,237
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 require 'cgi'
20 20
21 21 module Redmine
22 22 module Scm
23 module Adapters
23 module Adapters
24 24 class MercurialAdapter < AbstractAdapter
25 25
26 26 # Mercurial executable name
27 27 HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
28 28 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
29 29 TEMPLATE_NAME = "hg-template"
30 30 TEMPLATE_EXTENSION = "tmpl"
31 31
32 32 class << self
33 def client_command
34 @@bin ||= HG_BIN
35 end
36
37 def sq_bin
38 @@sq_bin ||= shell_quote(HG_BIN)
39 end
40
33 41 def client_version
34 42 @@client_version ||= (hgversion || [])
35 43 end
36 44
37 def hgversion
45 def client_available
46 !client_version.empty?
47 end
48
49 def hgversion
38 50 # The hg version is expressed either as a
39 51 # release number (eg 0.9.5 or 1.0) or as a revision
40 52 # id composed of 12 hexa characters.
41 53 theversion = hgversion_from_command_line
42 54 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
43 55 m[2].scan(%r{\d+}).collect(&:to_i)
44 56 end
45 57 end
46 58
47 59 def hgversion_from_command_line
48 shellout("#{HG_BIN} --version") { |io| io.read }.to_s
60 shellout("#{sq_bin} --version") { |io| io.read }.to_s
49 61 end
50 62
51 63 def template_path
52 64 @@template_path ||= template_path_for(client_version)
53 65 end
54 66
55 67 def template_path_for(version)
56 68 if ((version <=> [0,9,5]) > 0) || version.empty?
57 69 ver = "1.0"
58 70 else
59 71 ver = "0.9.5"
60 72 end
61 73 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
62 74 end
63 75 end
64 76
65 77 def info
66 cmd = "#{HG_BIN} -R #{target('')} root"
78 cmd = "#{self.class.sq_bin} -R #{target('')} root"
67 79 root_url = nil
68 80 shellout(cmd) do |io|
69 81 root_url = io.read
70 82 end
71 83 return nil if $? && $?.exitstatus != 0
72 84 info = Info.new({:root_url => root_url.chomp,
73 85 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
74 86 })
75 87 info
76 88 rescue CommandFailed
77 89 return nil
78 90 end
79 91
80 92 def entries(path=nil, identifier=nil)
81 93 path ||= ''
82 94 entries = Entries.new
83 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
95 cmd = "#{self.class.sq_bin} -R #{target('')} --cwd #{target('')} locate"
84 96 cmd << " -r #{hgrev(identifier)}"
85 97 cmd << " " + shell_quote("path:#{path}") unless path.empty?
86 98 shellout(cmd) do |io|
87 99 io.each_line do |line|
88 100 # HG uses antislashs as separator on Windows
89 101 line = line.gsub(/\\/, "/")
90 102 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
91 103 e ||= line
92 104 e = e.chomp.split(%r{[\/\\]})
93 105 entries << Entry.new({:name => e.first,
94 106 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
95 107 :kind => (e.size > 1 ? 'dir' : 'file'),
96 108 :lastrev => Revision.new
97 109 }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
98 110 end
99 111 end
100 112 end
101 113 return nil if $? && $?.exitstatus != 0
102 114 entries.sort_by_name
103 115 end
104 116
105 117 # Fetch the revisions by using a template file that
106 118 # makes Mercurial produce a xml output.
107 119 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
108 120 revisions = Revisions.new
109 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
121 cmd = "#{self.class.sq_bin} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
110 122 if identifier_from && identifier_to
111 123 cmd << " -r #{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
112 124 elsif identifier_from
113 125 cmd << " -r #{hgrev(identifier_from)}:"
114 126 end
115 127 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
116 128 cmd << " #{shell_quote path}" unless path.blank?
117 129 shellout(cmd) do |io|
118 130 begin
119 131 # HG doesn't close the XML Document...
120 132 doc = REXML::Document.new(io.read << "</log>")
121 133 doc.elements.each("log/logentry") do |logentry|
122 134 paths = []
123 135 copies = logentry.get_elements('paths/path-copied')
124 136 logentry.elements.each("paths/path") do |path|
125 137 # Detect if the added file is a copy
126 138 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
127 139 from_path = c.attributes['copyfrom-path']
128 140 from_rev = logentry.attributes['revision']
129 141 end
130 142 paths << {:action => path.attributes['action'],
131 143 :path => "/#{CGI.unescape(path.text)}",
132 144 :from_path => from_path ? "/#{CGI.unescape(from_path)}" : nil,
133 145 :from_revision => from_rev ? from_rev : nil
134 146 }
135 147 end
136 148 paths.sort! { |x,y| x[:path] <=> y[:path] }
137 149
138 150 revisions << Revision.new({:identifier => logentry.attributes['revision'],
139 151 :scmid => logentry.attributes['node'],
140 152 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
141 153 :time => Time.parse(logentry.elements['date'].text).localtime,
142 154 :message => logentry.elements['msg'].text,
143 155 :paths => paths
144 156 })
145 157 end
146 158 rescue
147 159 logger.debug($!)
148 160 end
149 161 end
150 162 return nil if $? && $?.exitstatus != 0
151 163 revisions
152 164 end
153 165
154 166 def diff(path, identifier_from, identifier_to=nil)
155 167 path ||= ''
156 168 diff_args = ''
157 169 diff = []
158 170 if identifier_to
159 171 diff_args = "-r #{hgrev(identifier_to)} -r #{hgrev(identifier_from)}"
160 172 else
161 173 if self.class.client_version_above?([1, 2])
162 174 diff_args = "-c #{hgrev(identifier_from)}"
163 175 else
164 176 return []
165 177 end
166 178 end
167 cmd = "#{HG_BIN} -R #{target('')} --config diff.git=false diff --nodates #{diff_args}"
179 cmd = "#{self.class.sq_bin} -R #{target('')} --config diff.git=false diff --nodates #{diff_args}"
168 180 cmd << " -I #{target(path)}" unless path.empty?
169 181 shellout(cmd) do |io|
170 182 io.each_line do |line|
171 183 diff << line
172 184 end
173 185 end
174 186 return nil if $? && $?.exitstatus != 0
175 187 diff
176 188 end
177 189
178 190 def cat(path, identifier=nil)
179 cmd = "#{HG_BIN} -R #{target('')} cat"
191 cmd = "#{self.class.sq_bin} -R #{target('')} cat"
180 192 cmd << " -r #{hgrev(identifier)}"
181 193 cmd << " #{target(path)}"
182 194 cat = nil
183 195 shellout(cmd) do |io|
184 196 io.binmode
185 197 cat = io.read
186 198 end
187 199 return nil if $? && $?.exitstatus != 0
188 200 cat
189 201 end
190 202
191 203 def annotate(path, identifier=nil)
192 204 path ||= ''
193 cmd = "#{HG_BIN} -R #{target('')}"
205 cmd = "#{self.class.sq_bin} -R #{target('')}"
194 206 cmd << " annotate -ncu"
195 207 cmd << " -r #{hgrev(identifier)}"
196 208 cmd << " #{target(path)}"
197 209 blame = Annotate.new
198 210 shellout(cmd) do |io|
199 211 io.each_line do |line|
200 212 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
201 213 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
202 214 :identifier => $3)
203 215 blame.add_line($4.rstrip, r)
204 216 end
205 217 end
206 218 return nil if $? && $?.exitstatus != 0
207 219 blame
208 220 end
209 221
210 222 class Revision < Redmine::Scm::Adapters::Revision
211 223 # Returns the readable identifier
212 224 def format_identifier
213 225 "#{revision}:#{scmid}"
214 226 end
215 227 end
216 228
217 229 # Returns correct revision identifier
218 230 def hgrev(identifier)
219 231 shell_quote(identifier.blank? ? 'tip' : identifier.to_s)
220 232 end
221 233 private :hgrev
222 234 end
223 235 end
224 236 end
225 237 end
@@ -1,256 +1,264
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 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 require 'uri'
20 20
21 21 module Redmine
22 22 module Scm
23 module Adapters
23 module Adapters
24 24 class SubversionAdapter < AbstractAdapter
25
25
26 26 # SVN executable name
27 27 SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
28
28
29 29 class << self
30 def client_command
31 @@bin ||= SVN_BIN
32 end
33
34 def sq_bin
35 @@sq_bin ||= shell_quote(SVN_BIN)
36 end
37
30 38 def client_version
31 39 @@client_version ||= (svn_binary_version || [])
32 40 end
33
41
34 42 def svn_binary_version
35 cmd = "#{SVN_BIN} --version"
43 cmd = "#{sq_bin} --version"
36 44 version = nil
37 45 shellout(cmd) do |io|
38 46 # Read svn version in first returned line
39 47 if m = io.read.to_s.match(%r{\A(.*?)((\d+\.)+\d+)})
40 48 version = m[2].scan(%r{\d+}).collect(&:to_i)
41 49 end
42 50 end
43 51 return nil if $? && $?.exitstatus != 0
44 52 version
45 53 end
46 54 end
47
55
48 56 # Get info about the svn repository
49 57 def info
50 cmd = "#{SVN_BIN} info --xml #{target}"
58 cmd = "#{self.class.sq_bin} info --xml #{target}"
51 59 cmd << credentials_string
52 60 info = nil
53 61 shellout(cmd) do |io|
54 62 output = io.read
55 63 begin
56 64 doc = ActiveSupport::XmlMini.parse(output)
57 65 #root_url = doc.elements["info/entry/repository/root"].text
58 66 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
59 67 :lastrev => Revision.new({
60 68 :identifier => doc['info']['entry']['commit']['revision'],
61 69 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
62 70 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
63 71 })
64 72 })
65 73 rescue
66 74 end
67 75 end
68 76 return nil if $? && $?.exitstatus != 0
69 77 info
70 78 rescue CommandFailed
71 79 return nil
72 80 end
73
81
74 82 # Returns an Entries collection
75 83 # or nil if the given path doesn't exist in the repository
76 84 def entries(path=nil, identifier=nil)
77 85 path ||= ''
78 86 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
79 87 entries = Entries.new
80 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
88 cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
81 89 cmd << credentials_string
82 90 shellout(cmd) do |io|
83 91 output = io.read
84 92 begin
85 93 doc = ActiveSupport::XmlMini.parse(output)
86 94 each_xml_element(doc['lists']['list'], 'entry') do |entry|
87 95 commit = entry['commit']
88 96 commit_date = commit['date']
89 97 # Skip directory if there is no commit date (usually that
90 98 # means that we don't have read access to it)
91 99 next if entry['kind'] == 'dir' && commit_date.nil?
92 100 name = entry['name']['__content__']
93 101 entries << Entry.new({:name => URI.unescape(name),
94 102 :path => ((path.empty? ? "" : "#{path}/") + name),
95 103 :kind => entry['kind'],
96 104 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
97 105 :lastrev => Revision.new({
98 106 :identifier => commit['revision'],
99 107 :time => Time.parse(commit_date['__content__'].to_s).localtime,
100 108 :author => ((a = commit['author']) ? a['__content__'] : nil)
101 109 })
102 110 })
103 111 end
104 112 rescue Exception => e
105 113 logger.error("Error parsing svn output: #{e.message}")
106 114 logger.error("Output was:\n #{output}")
107 115 end
108 116 end
109 117 return nil if $? && $?.exitstatus != 0
110 118 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
111 119 entries.sort_by_name
112 120 end
113
121
114 122 def properties(path, identifier=nil)
115 123 # proplist xml output supported in svn 1.5.0 and higher
116 124 return nil unless self.class.client_version_above?([1, 5, 0])
117 125
118 126 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
119 cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
127 cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
120 128 cmd << credentials_string
121 129 properties = {}
122 130 shellout(cmd) do |io|
123 131 output = io.read
124 132 begin
125 133 doc = ActiveSupport::XmlMini.parse(output)
126 134 each_xml_element(doc['properties']['target'], 'property') do |property|
127 135 properties[ property['name'] ] = property['__content__'].to_s
128 136 end
129 137 rescue
130 138 end
131 139 end
132 140 return nil if $? && $?.exitstatus != 0
133 141 properties
134 142 end
135
143
136 144 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
137 145 path ||= ''
138 146 identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
139 147 identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
140 148 revisions = Revisions.new
141 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
149 cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
142 150 cmd << credentials_string
143 151 cmd << " --verbose " if options[:with_paths]
144 152 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
145 153 cmd << ' ' + target(path)
146 154 shellout(cmd) do |io|
147 155 output = io.read
148 156 begin
149 157 doc = ActiveSupport::XmlMini.parse(output)
150 158 each_xml_element(doc['log'], 'logentry') do |logentry|
151 159 paths = []
152 160 each_xml_element(logentry['paths'], 'path') do |path|
153 161 paths << {:action => path['action'],
154 162 :path => path['__content__'],
155 163 :from_path => path['copyfrom-path'],
156 164 :from_revision => path['copyfrom-rev']
157 165 }
158 166 end if logentry['paths'] && logentry['paths']['path']
159 167 paths.sort! { |x,y| x[:path] <=> y[:path] }
160 168
161 169 revisions << Revision.new({:identifier => logentry['revision'],
162 170 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
163 171 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
164 172 :message => logentry['msg']['__content__'],
165 173 :paths => paths
166 174 })
167 175 end
168 176 rescue
169 177 end
170 178 end
171 179 return nil if $? && $?.exitstatus != 0
172 180 revisions
173 181 end
174
182
175 183 def diff(path, identifier_from, identifier_to=nil, type="inline")
176 184 path ||= ''
177 185 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
178 186 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
179
180 cmd = "#{SVN_BIN} diff -r "
187
188 cmd = "#{self.class.sq_bin} diff -r "
181 189 cmd << "#{identifier_to}:"
182 190 cmd << "#{identifier_from}"
183 191 cmd << " #{target(path)}@#{identifier_from}"
184 192 cmd << credentials_string
185 193 diff = []
186 194 shellout(cmd) do |io|
187 195 io.each_line do |line|
188 196 diff << line
189 197 end
190 198 end
191 199 return nil if $? && $?.exitstatus != 0
192 200 diff
193 201 end
194
202
195 203 def cat(path, identifier=nil)
196 204 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
197 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
205 cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
198 206 cmd << credentials_string
199 207 cat = nil
200 208 shellout(cmd) do |io|
201 209 io.binmode
202 210 cat = io.read
203 211 end
204 212 return nil if $? && $?.exitstatus != 0
205 213 cat
206 214 end
207
215
208 216 def annotate(path, identifier=nil)
209 217 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
210 cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
218 cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
211 219 cmd << credentials_string
212 220 blame = Annotate.new
213 221 shellout(cmd) do |io|
214 222 io.each_line do |line|
215 223 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
216 224 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
217 225 end
218 226 end
219 227 return nil if $? && $?.exitstatus != 0
220 228 blame
221 229 end
222 230
223 231 private
224 232
225 233 def credentials_string
226 234 str = ''
227 235 str << " --username #{shell_quote(@login)}" unless @login.blank?
228 236 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
229 237 str << " --no-auth-cache --non-interactive"
230 238 str
231 239 end
232 240
233 241 # Helper that iterates over the child elements of a xml node
234 242 # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
235 243 def each_xml_element(node, name)
236 244 if node && node[name]
237 245 if node[name].is_a?(Hash)
238 246 yield node[name]
239 247 else
240 248 node[name].each do |element|
241 249 yield element
242 250 end
243 251 end
244 252 end
245 253 end
246 254
247 255 def target(path = '')
248 256 base = path.match(/^\//) ? root_url : url
249 257 uri = "#{base}/#{path}"
250 258 uri = URI.escape(URI.escape(uri), '[]')
251 259 shell_quote(uri.gsub(/[?<>\*]/, ''))
252 260 end
253 261 end
254 262 end
255 263 end
256 264 end
General Comments 0
You need to be logged in to leave comments. Login now