To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / lib / redmine / scm / adapters / abstract_adapter.rb @ 1298:4f746d8966dd

History | View | Annotate | Download (12.8 KB)

1 441:cbce1fd3b1b7 Chris
# Redmine - project management software
2 1295:622f24f53b42 Chris
# Copyright (C) 2006-2013  Jean-Philippe Lang
3 0:513646585e45 Chris
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8 441:cbce1fd3b1b7 Chris
#
9 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13 441:cbce1fd3b1b7 Chris
#
14 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
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.
17
18
require 'cgi'
19
20 1295:622f24f53b42 Chris
if RUBY_VERSION < '1.9'
21
  require 'iconv'
22
end
23
24 0:513646585e45 Chris
module Redmine
25
  module Scm
26 245:051f544170fe Chris
    module Adapters
27 0:513646585e45 Chris
      class CommandFailed < StandardError #:nodoc:
28
      end
29 245:051f544170fe Chris
30 0:513646585e45 Chris
      class AbstractAdapter #:nodoc:
31 909:cbb26bc654de Chris
32
        # raised if scm command exited with error, e.g. unknown revision.
33
        class ScmCommandAborted < CommandFailed; end
34
35 0:513646585e45 Chris
        class << self
36 245:051f544170fe Chris
          def client_command
37
            ""
38
          end
39
40 909:cbb26bc654de Chris
          def shell_quote_command
41
            if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
42
              client_command
43
            else
44
              shell_quote(client_command)
45
            end
46
          end
47
48 0:513646585e45 Chris
          # Returns the version of the scm client
49
          # Eg: [1, 5, 0] or [] if unknown
50
          def client_version
51
            []
52
          end
53 245:051f544170fe Chris
54 0:513646585e45 Chris
          # Returns the version string of the scm client
55
          # Eg: '1.5.0' or 'Unknown version' if unknown
56
          def client_version_string
57
            v = client_version || 'Unknown version'
58
            v.is_a?(Array) ? v.join('.') : v.to_s
59
          end
60 245:051f544170fe Chris
61 0:513646585e45 Chris
          # Returns true if the current client version is above
62
          # or equals the given one
63
          # If option is :unknown is set to true, it will return
64
          # true if the client version is unknown
65
          def client_version_above?(v, options={})
66
            ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
67
          end
68 245:051f544170fe Chris
69
          def client_available
70
            true
71
          end
72
73
          def shell_quote(str)
74
            if Redmine::Platform.mswin?
75
              '"' + str.gsub(/"/, '\\"') + '"'
76
            else
77
              "'" + str.gsub(/'/, "'\"'\"'") + "'"
78
            end
79
          end
80 0:513646585e45 Chris
        end
81 245:051f544170fe Chris
82
        def initialize(url, root_url=nil, login=nil, password=nil,
83
                       path_encoding=nil)
84 0:513646585e45 Chris
          @url = url
85
          @login = login if login && !login.empty?
86
          @password = (password || "") if @login
87
          @root_url = root_url.blank? ? retrieve_root_url : root_url
88
        end
89 245:051f544170fe Chris
90 0:513646585e45 Chris
        def adapter_name
91
          'Abstract'
92
        end
93 245:051f544170fe Chris
94 0:513646585e45 Chris
        def supports_cat?
95
          true
96
        end
97
98
        def supports_annotate?
99
          respond_to?('annotate')
100
        end
101 245:051f544170fe Chris
102 0:513646585e45 Chris
        def root_url
103
          @root_url
104
        end
105 245:051f544170fe Chris
106 0:513646585e45 Chris
        def url
107
          @url
108
        end
109 441:cbce1fd3b1b7 Chris
110
        def path_encoding
111
          nil
112
        end
113
114 0:513646585e45 Chris
        # get info about the svn repository
115
        def info
116
          return nil
117
        end
118 441:cbce1fd3b1b7 Chris
119 0:513646585e45 Chris
        # Returns the entry identified by path and revision identifier
120
        # or nil if entry doesn't exist in the repository
121
        def entry(path=nil, identifier=nil)
122
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
123
          search_path = parts[0..-2].join('/')
124
          search_name = parts[-1]
125
          if search_path.blank? && search_name.blank?
126
            # Root entry
127
            Entry.new(:path => '', :kind => 'dir')
128
          else
129
            # Search for the entry in the parent directory
130
            es = entries(search_path, identifier)
131
            es ? es.detect {|e| e.name == search_name} : nil
132
          end
133
        end
134 441:cbce1fd3b1b7 Chris
135 0:513646585e45 Chris
        # Returns an Entries collection
136
        # or nil if the given path doesn't exist in the repository
137 441:cbce1fd3b1b7 Chris
        def entries(path=nil, identifier=nil, options={})
138 0:513646585e45 Chris
          return nil
139
        end
140
141
        def branches
142
          return nil
143
        end
144
145 441:cbce1fd3b1b7 Chris
        def tags
146 0:513646585e45 Chris
          return nil
147
        end
148
149
        def default_branch
150
          return nil
151
        end
152 441:cbce1fd3b1b7 Chris
153 0:513646585e45 Chris
        def properties(path, identifier=nil)
154
          return nil
155
        end
156 441:cbce1fd3b1b7 Chris
157 0:513646585e45 Chris
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
158
          return nil
159
        end
160 441:cbce1fd3b1b7 Chris
161 0:513646585e45 Chris
        def diff(path, identifier_from, identifier_to=nil)
162
          return nil
163
        end
164 441:cbce1fd3b1b7 Chris
165 0:513646585e45 Chris
        def cat(path, identifier=nil)
166
          return nil
167
        end
168 441:cbce1fd3b1b7 Chris
169 0:513646585e45 Chris
        def with_leading_slash(path)
170
          path ||= ''
171
          (path[0,1]!="/") ? "/#{path}" : path
172
        end
173
174
        def with_trailling_slash(path)
175
          path ||= ''
176
          (path[-1,1] == "/") ? path : "#{path}/"
177
        end
178 245:051f544170fe Chris
179 0:513646585e45 Chris
        def without_leading_slash(path)
180
          path ||= ''
181
          path.gsub(%r{^/+}, '')
182
        end
183
184
        def without_trailling_slash(path)
185
          path ||= ''
186
          (path[-1,1] == "/") ? path[0..-2] : path
187
         end
188 245:051f544170fe Chris
189 0:513646585e45 Chris
        def shell_quote(str)
190 245:051f544170fe Chris
          self.class.shell_quote(str)
191 0:513646585e45 Chris
        end
192
193
      private
194
        def retrieve_root_url
195
          info = self.info
196
          info ? info.root_url : nil
197
        end
198 441:cbce1fd3b1b7 Chris
199 909:cbb26bc654de Chris
        def target(path, sq=true)
200 0:513646585e45 Chris
          path ||= ''
201
          base = path.match(/^\//) ? root_url : url
202 909:cbb26bc654de Chris
          str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
203
          if sq
204
            str = shell_quote(str)
205
          end
206
          str
207 0:513646585e45 Chris
        end
208 245:051f544170fe Chris
209 0:513646585e45 Chris
        def logger
210
          self.class.logger
211
        end
212 245:051f544170fe Chris
213 1115:433d4f72a19b Chris
        def shellout(cmd, options = {}, &block)
214
          self.class.shellout(cmd, options, &block)
215 0:513646585e45 Chris
        end
216 245:051f544170fe Chris
217 0:513646585e45 Chris
        def self.logger
218 909:cbb26bc654de Chris
          Rails.logger
219 0:513646585e45 Chris
        end
220 245:051f544170fe Chris
221 1295:622f24f53b42 Chris
        # Path to the file where scm stderr output is logged
222
        # Returns nil if the log file is not writable
223
        def self.stderr_log_file
224
          if @stderr_log_file.nil?
225
            writable = false
226
            path = Redmine::Configuration['scm_stderr_log_file'].presence
227
            path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
228
            if File.exists?(path)
229
              if File.file?(path) && File.writable?(path)
230
                writable = true
231
              else
232
                logger.warn("SCM log file (#{path}) is not writable")
233
              end
234
            else
235
              begin
236
                File.open(path, "w") {}
237
                writable = true
238
              rescue => e
239
                logger.warn("SCM log file (#{path}) cannot be created: #{e.message}")
240
              end
241
            end
242
            @stderr_log_file = writable ? path : false
243
          end
244
          @stderr_log_file || nil
245
        end
246
247 1115:433d4f72a19b Chris
        def self.shellout(cmd, options = {}, &block)
248 507:0c939c159af4 Chris
          if logger && logger.debug?
249
            logger.debug "Shelling out: #{strip_credential(cmd)}"
250 1295:622f24f53b42 Chris
            # Capture stderr in a log file
251
            if stderr_log_file
252
              cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
253
            end
254 0:513646585e45 Chris
          end
255
          begin
256 1115:433d4f72a19b Chris
            mode = "r+"
257 245:051f544170fe Chris
            IO.popen(cmd, mode) do |io|
258 1115:433d4f72a19b Chris
              io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
259
              io.close_write unless options[:write_stdin]
260 0:513646585e45 Chris
              block.call(io) if block_given?
261
            end
262 909:cbb26bc654de Chris
          ## If scm command does not exist,
263
          ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
264
          ## in production environment.
265
          # rescue Errno::ENOENT => e
266
          rescue Exception => e
267 0:513646585e45 Chris
            msg = strip_credential(e.message)
268
            # The command failed, log it and re-raise
269 507:0c939c159af4 Chris
            logmsg = "SCM command failed, "
270
            logmsg += "make sure that your SCM command (e.g. svn) is "
271
            logmsg += "in PATH (#{ENV['PATH']})\n"
272
            logmsg += "You can configure your scm commands in config/configuration.yml.\n"
273
            logmsg += "#{strip_credential(cmd)}\n"
274
            logmsg += "with: #{msg}"
275
            logger.error(logmsg)
276 0:513646585e45 Chris
            raise CommandFailed.new(msg)
277
          end
278 245:051f544170fe Chris
        end
279
280 0:513646585e45 Chris
        # Hides username/password in a given command
281
        def self.strip_credential(cmd)
282
          q = (Redmine::Platform.mswin? ? '"' : "'")
283
          cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
284
        end
285 441:cbce1fd3b1b7 Chris
286 0:513646585e45 Chris
        def strip_credential(cmd)
287
          self.class.strip_credential(cmd)
288
        end
289 245:051f544170fe Chris
290
        def scm_iconv(to, from, str)
291
          return nil if str.nil?
292 922:ad295b270cd4 Chris
          # bug 446: non-utf8 paths in repositories blow up repo viewer and reposman
293
          # -- Remove this short-circuit: we want the conversion to
294
          #    happen always, so we can trap the error here if the
295
          #    source text happens not to be in the advertised
296
          #    encoding (instead of having the database blow up later)
297
#          return str if to == from
298 1115:433d4f72a19b Chris
          if str.respond_to?(:force_encoding)
299
            str.force_encoding(from)
300
            begin
301
              str.encode(to)
302
            rescue Exception => err
303
              logger.error("failed to convert from #{from} to #{to}. #{err}")
304
              nil
305
            end
306
          else
307
            begin
308
              Iconv.conv(to, from, str)
309
            rescue Iconv::Failure => err
310
              logger.error("failed to convert from #{from} to #{to}. #{err}")
311
              nil
312
            end
313 245:051f544170fe Chris
          end
314
        end
315 1115:433d4f72a19b Chris
316
        def parse_xml(xml)
317
          if RUBY_PLATFORM == 'java'
318
            xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
319
          end
320
          ActiveSupport::XmlMini.parse(xml)
321
        end
322 0:513646585e45 Chris
      end
323 245:051f544170fe Chris
324 0:513646585e45 Chris
      class Entries < Array
325
        def sort_by_name
326 1115:433d4f72a19b Chris
          dup.sort! {|x,y|
327 0:513646585e45 Chris
            if x.kind == y.kind
328
              x.name.to_s <=> y.name.to_s
329
            else
330
              x.kind <=> y.kind
331
            end
332 245:051f544170fe Chris
          }
333 0:513646585e45 Chris
        end
334 441:cbce1fd3b1b7 Chris
335 0:513646585e45 Chris
        def revisions
336
          revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
337
        end
338
      end
339 441:cbce1fd3b1b7 Chris
340 0:513646585e45 Chris
      class Info
341
        attr_accessor :root_url, :lastrev
342
        def initialize(attributes={})
343
          self.root_url = attributes[:root_url] if attributes[:root_url]
344
          self.lastrev = attributes[:lastrev]
345
        end
346
      end
347 441:cbce1fd3b1b7 Chris
348 0:513646585e45 Chris
      class Entry
349 1115:433d4f72a19b Chris
        attr_accessor :name, :path, :kind, :size, :lastrev, :changeset
350
351 0:513646585e45 Chris
        def initialize(attributes={})
352
          self.name = attributes[:name] if attributes[:name]
353
          self.path = attributes[:path] if attributes[:path]
354
          self.kind = attributes[:kind] if attributes[:kind]
355
          self.size = attributes[:size].to_i if attributes[:size]
356
          self.lastrev = attributes[:lastrev]
357
        end
358 441:cbce1fd3b1b7 Chris
359 0:513646585e45 Chris
        def is_file?
360
          'file' == self.kind
361
        end
362 441:cbce1fd3b1b7 Chris
363 0:513646585e45 Chris
        def is_dir?
364
          'dir' == self.kind
365
        end
366 441:cbce1fd3b1b7 Chris
367 0:513646585e45 Chris
        def is_text?
368
          Redmine::MimeType.is_type?('text', name)
369
        end
370 1115:433d4f72a19b Chris
371
        def author
372
          if changeset
373
            changeset.author.to_s
374
          elsif lastrev
375
            Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
376
          end
377
        end
378 0:513646585e45 Chris
      end
379 441:cbce1fd3b1b7 Chris
380 0:513646585e45 Chris
      class Revisions < Array
381
        def latest
382
          sort {|x,y|
383
            unless x.time.nil? or y.time.nil?
384
              x.time <=> y.time
385
            else
386
              0
387
            end
388
          }.last
389 441:cbce1fd3b1b7 Chris
        end
390 0:513646585e45 Chris
      end
391 441:cbce1fd3b1b7 Chris
392 0:513646585e45 Chris
      class Revision
393 441:cbce1fd3b1b7 Chris
        attr_accessor :scmid, :name, :author, :time, :message,
394 909:cbb26bc654de Chris
                      :paths, :revision, :branch, :identifier,
395
                      :parents
396 0:513646585e45 Chris
397
        def initialize(attributes={})
398
          self.identifier = attributes[:identifier]
399 441:cbce1fd3b1b7 Chris
          self.scmid      = attributes[:scmid]
400
          self.name       = attributes[:name] || self.identifier
401
          self.author     = attributes[:author]
402
          self.time       = attributes[:time]
403
          self.message    = attributes[:message] || ""
404
          self.paths      = attributes[:paths]
405
          self.revision   = attributes[:revision]
406
          self.branch     = attributes[:branch]
407 909:cbb26bc654de Chris
          self.parents    = attributes[:parents]
408 117:af80e5618e9b Chris
        end
409
410
        # Returns the readable identifier.
411
        def format_identifier
412 441:cbce1fd3b1b7 Chris
          self.identifier.to_s
413 117:af80e5618e9b Chris
        end
414 1115:433d4f72a19b Chris
415
        def ==(other)
416
          if other.nil?
417
            false
418
          elsif scmid.present?
419
            scmid == other.scmid
420
          elsif identifier.present?
421
            identifier == other.identifier
422
          elsif revision.present?
423
            revision == other.revision
424
          end
425
        end
426 245:051f544170fe Chris
      end
427 117:af80e5618e9b Chris
428 0:513646585e45 Chris
      class Annotate
429
        attr_reader :lines, :revisions
430 441:cbce1fd3b1b7 Chris
431 0:513646585e45 Chris
        def initialize
432
          @lines = []
433
          @revisions = []
434
        end
435 441:cbce1fd3b1b7 Chris
436 0:513646585e45 Chris
        def add_line(line, revision)
437
          @lines << line
438
          @revisions << revision
439
        end
440 441:cbce1fd3b1b7 Chris
441 0:513646585e45 Chris
        def content
442
          content = lines.join("\n")
443
        end
444 441:cbce1fd3b1b7 Chris
445 0:513646585e45 Chris
        def empty?
446
          lines.empty?
447
        end
448
      end
449 909:cbb26bc654de Chris
450
      class Branch < String
451
        attr_accessor :revision, :scmid
452
      end
453 0:513646585e45 Chris
    end
454
  end
455
end