subversion_adapter.rb

Etienne Massip, 2011-03-17 19:47

Download (10.9 KB)

 
1
# Redmine - project management software
2
# Copyright (C) 2006-2010  Jean-Philippe Lang
3
#
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
# 
9
# 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
# 
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

    
18
require 'redmine/scm/adapters/abstract_adapter'
19
require 'uri'
20

    
21
module Redmine
22
  module Scm
23
    module Adapters    
24
      class SubversionAdapter < AbstractAdapter
25
      
26
        # SVN executable name
27
        SVN_BIN = "svn"
28
        
29
        class << self
30
          def client_version
31
            @@client_version ||= (svn_binary_version || [])
32
          end
33
          
34
          def svn_binary_version
35
            cmd = "#{SVN_BIN} --version"
36
            version = nil
37
            shellout(cmd) do |io|
38
              # Read svn version in first returned line
39
              if m = io.read.to_s.match(%r{\A(.*?)((\d+\.)+\d+)})
40
                version = m[2].scan(%r{\d+}).collect(&:to_i)
41
              end
42
            end
43
            return nil if $? && $?.exitstatus != 0
44
            version
45
          end
46
        end
47
        
48
        # Get info about the svn repository
49
        def info
50
          cmd = "#{SVN_BIN} info --xml #{target}"
51
          cmd << credentials_string
52
          info = nil
53
          shellout(cmd) do |io|
54
            output = io.read
55
            begin
56
              doc = ActiveSupport::XmlMini.parse(output)
57
              #root_url = doc.elements["info/entry/repository/root"].text          
58
              info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
59
                               :lastrev => Revision.new({
60
                                 :identifier => doc['info']['entry']['commit']['revision'],
61
                                 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
62
                                 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
63
                               })
64
                             })
65
            rescue
66
            end
67
          end
68
          return nil if $? && $?.exitstatus != 0
69
          info
70
        rescue CommandFailed
71
          return nil
72
        end
73
        
74
        # Returns an Entries collection
75
        # or nil if the given path doesn't exist in the repository
76
        def entries(path=nil, identifier=nil)
77
          path ||= ''
78
          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
79
          entries = Entries.new
80
          cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
81
          cmd << credentials_string
82
          shellout(cmd) do |io|
83
            output = io.read
84
            begin
85
              doc = ActiveSupport::XmlMini.parse(output)
86
              each_xml_element(doc['lists']['list'], 'entry') do |entry|
87
                commit = entry['commit']
88
                commit_date = commit['date']
89
                # Skip directory if there is no commit date (usually that
90
                # means that we don't have read access to it)
91
                next if entry['kind'] == 'dir' && commit_date.nil?
92
                name = entry['name']['__content__']
93
                entries << Entry.new({:name => URI.unescape(name),
94
                            :path => ((path.empty? ? "" : "#{path}/") + name),
95
                            :kind => entry['kind'],
96
                            :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
97
                            :lastrev => Revision.new({
98
                              :identifier => commit['revision'],
99
                              :time => Time.parse(commit_date['__content__'].to_s).localtime,
100
                              :author => ((a = commit['author']) ? a['__content__'] : nil)
101
                              })
102
                            })
103
              end
104
            rescue Exception => e
105
              logger.error("Error parsing svn output: #{e.message}")
106
              logger.error("Output was:\n #{output}")
107
            end
108
          end
109
          return nil if $? && $?.exitstatus != 0
110
          logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
111
          entries.sort_by_name
112
        end
113
        
114
        def properties(path, identifier=nil)
115
          # proplist xml output supported in svn 1.5.0 and higher
116
          return nil unless self.class.client_version_above?([1, 5, 0])
117
          
118
          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
119
          cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
120
          cmd << credentials_string
121
          properties = {}
122
          shellout(cmd) do |io|
123
            output = io.read
124
            begin
125
              doc = ActiveSupport::XmlMini.parse(output)
126
              each_xml_element(doc['properties']['target'], 'property') do |property|
127
                properties[ property['name'] ] = property['__content__'].to_s
128
              end
129
            rescue
130
            end
131
          end
132
          return nil if $? && $?.exitstatus != 0
133
          properties
134
        end
135
        
136
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
137
          path ||= ''
138
          identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
139
          identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
140
          revisions = Revisions.new
141
          cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
142
          cmd << credentials_string
143
          cmd << " --verbose " if  options[:with_paths]
144
          cmd << " --limit #{options[:limit].to_i}" if options[:limit]
145
          cmd << ' ' + target(path)
146
          logger.info 'before svn command'
147
          shellout(cmd) do |io|
148
            output = io.read
149
            begin
150
              logger.info 'before XML parsing'
151
              doc = ActiveSupport::XmlMini.with_backend('LibXML').parse(output)
152
              each_xml_element(doc['log'], 'logentry') do |logentry|
153

    
154
               logger.info ('reading revision ' + logentry['revision'])
155

    
156
                paths = []
157
                each_xml_element(logentry['paths'], 'path') do |path|
158

    
159
                  logger.info ('reading path ' + path['__content__'])
160

    
161
                  paths << {:action => path['action'],
162
                            :path => path['__content__'],
163
                            :from_path => path['copyfrom-path'],
164
                            :from_revision => path['copyfrom-rev']
165
                            }
166
                end if logentry['paths'] && logentry['paths']['path']
167

    
168
                logger.info 'before paths array sorting'
169

    
170
                paths.sort! { |x,y| x[:path] <=> y[:path] }
171
                
172
                revisions << Revision.new({:identifier => logentry['revision'],
173
                              :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
174
                              :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
175
                              :message => logentry['msg']['__content__'],
176
                              :paths => paths
177
                            })
178
              end
179
            rescue
180
             logger.info 'exception thrown'
181
            end
182
          end
183
          logger.info 'end of fetching'
184
          return nil if $? && $?.exitstatus != 0
185
          revisions
186
        end
187
        
188
        def diff(path, identifier_from, identifier_to=nil, type="inline")
189
          path ||= ''
190
          identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
191
          identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
192
          
193
          cmd = "#{SVN_BIN} diff -r "
194
          cmd << "#{identifier_to}:"
195
          cmd << "#{identifier_from}"
196
          cmd << " #{target(path)}@#{identifier_from}"
197
          cmd << credentials_string
198
          diff = []
199
          shellout(cmd) do |io|
200
            io.each_line do |line|
201
              diff << line
202
            end
203
          end
204
          return nil if $? && $?.exitstatus != 0
205
          diff
206
        end
207
        
208
        def cat(path, identifier=nil)
209
          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
210
          cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
211
          cmd << credentials_string
212
          cat = nil
213
          shellout(cmd) do |io|
214
            io.binmode
215
            cat = io.read
216
          end
217
          return nil if $? && $?.exitstatus != 0
218
          cat
219
        end
220
        
221
        def annotate(path, identifier=nil)
222
          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
223
          cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
224
          cmd << credentials_string
225
          blame = Annotate.new
226
          shellout(cmd) do |io|
227
            io.each_line do |line|
228
              next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
229
              blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
230
            end
231
          end
232
          return nil if $? && $?.exitstatus != 0
233
          blame
234
        end
235
        
236
        private
237
        
238
        def credentials_string
239
          str = ''
240
          str << " --username #{shell_quote(@login)}" unless @login.blank?
241
          str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
242
          str << " --no-auth-cache --non-interactive"
243
          str
244
        end
245
        
246
        # Helper that iterates over the child elements of a xml node
247
        # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
248
        def each_xml_element(node, name)
249
          if node && node[name]
250
            if node[name].is_a?(Hash)
251
              yield node[name]
252
            else
253
              node[name].each do |element|
254
                yield element
255
              end
256
            end
257
          end
258
        end
259

    
260
        def target(path = '')
261
          base = path.match(/^\//) ? root_url : url
262
          uri = "#{base}/#{path}"
263
          uri = URI.escape(URI.escape(uri), '[]')
264
          shell_quote(uri.gsub(/[?<>\*]/, ''))
265
        end
266
      end
267
    end
268
  end
269
end