Chris@441
|
1 # Redmine - project management software
|
Chris@1295
|
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
|
Chris@441
|
3 #
|
Chris@441
|
4 # This program is free software; you can redistribute it and/or
|
Chris@441
|
5 # modify it under the terms of the GNU General Public License
|
Chris@441
|
6 # as published by the Free Software Foundation; either version 2
|
Chris@441
|
7 # of the License, or (at your option) any later version.
|
Chris@441
|
8 #
|
Chris@441
|
9 # This program is distributed in the hope that it will be useful,
|
Chris@441
|
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Chris@441
|
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Chris@441
|
12 # GNU General Public License for more details.
|
Chris@441
|
13 #
|
Chris@441
|
14 # You should have received a copy of the GNU General Public License
|
Chris@441
|
15 # along with this program; if not, write to the Free Software
|
Chris@441
|
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
Chris@441
|
17
|
Chris@441
|
18 require 'redmine/scm/adapters/abstract_adapter'
|
Chris@441
|
19 require 'uri'
|
Chris@441
|
20
|
Chris@441
|
21 module Redmine
|
Chris@441
|
22 module Scm
|
Chris@441
|
23 module Adapters
|
Chris@441
|
24 class SubversionAdapter < AbstractAdapter
|
Chris@441
|
25
|
Chris@441
|
26 # SVN executable name
|
Chris@441
|
27 SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
|
Chris@441
|
28
|
Chris@441
|
29 class << self
|
Chris@441
|
30 def client_command
|
Chris@441
|
31 @@bin ||= SVN_BIN
|
Chris@441
|
32 end
|
Chris@441
|
33
|
Chris@441
|
34 def sq_bin
|
Chris@909
|
35 @@sq_bin ||= shell_quote_command
|
Chris@441
|
36 end
|
Chris@441
|
37
|
Chris@441
|
38 def client_version
|
Chris@441
|
39 @@client_version ||= (svn_binary_version || [])
|
Chris@441
|
40 end
|
Chris@441
|
41
|
Chris@441
|
42 def client_available
|
Chris@441
|
43 # --xml options are introduced in 1.3.
|
Chris@441
|
44 # http://subversion.apache.org/docs/release-notes/1.3.html
|
Chris@441
|
45 client_version_above?([1, 3])
|
Chris@441
|
46 end
|
Chris@441
|
47
|
Chris@441
|
48 def svn_binary_version
|
Chris@441
|
49 scm_version = scm_version_from_command_line.dup
|
Chris@441
|
50 if scm_version.respond_to?(:force_encoding)
|
Chris@441
|
51 scm_version.force_encoding('ASCII-8BIT')
|
Chris@441
|
52 end
|
Chris@441
|
53 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
|
Chris@441
|
54 m[2].scan(%r{\d+}).collect(&:to_i)
|
Chris@441
|
55 end
|
Chris@441
|
56 end
|
Chris@441
|
57
|
Chris@441
|
58 def scm_version_from_command_line
|
Chris@441
|
59 shellout("#{sq_bin} --version") { |io| io.read }.to_s
|
Chris@441
|
60 end
|
Chris@441
|
61 end
|
Chris@441
|
62
|
Chris@441
|
63 # Get info about the svn repository
|
Chris@441
|
64 def info
|
Chris@441
|
65 cmd = "#{self.class.sq_bin} info --xml #{target}"
|
Chris@441
|
66 cmd << credentials_string
|
Chris@441
|
67 info = nil
|
Chris@441
|
68 shellout(cmd) do |io|
|
Chris@441
|
69 output = io.read
|
Chris@441
|
70 if output.respond_to?(:force_encoding)
|
Chris@441
|
71 output.force_encoding('UTF-8')
|
Chris@441
|
72 end
|
Chris@441
|
73 begin
|
Chris@1115
|
74 doc = parse_xml(output)
|
Chris@441
|
75 # root_url = doc.elements["info/entry/repository/root"].text
|
Chris@441
|
76 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
|
Chris@441
|
77 :lastrev => Revision.new({
|
Chris@441
|
78 :identifier => doc['info']['entry']['commit']['revision'],
|
Chris@441
|
79 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
|
Chris@441
|
80 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
|
Chris@441
|
81 })
|
Chris@441
|
82 })
|
Chris@441
|
83 rescue
|
Chris@441
|
84 end
|
Chris@441
|
85 end
|
Chris@441
|
86 return nil if $? && $?.exitstatus != 0
|
Chris@441
|
87 info
|
Chris@441
|
88 rescue CommandFailed
|
Chris@441
|
89 return nil
|
Chris@441
|
90 end
|
Chris@441
|
91
|
Chris@441
|
92 # Returns an Entries collection
|
Chris@441
|
93 # or nil if the given path doesn't exist in the repository
|
Chris@441
|
94 def entries(path=nil, identifier=nil, options={})
|
Chris@441
|
95 path ||= ''
|
Chris@441
|
96 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
Chris@441
|
97 entries = Entries.new
|
Chris@441
|
98 cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
|
Chris@441
|
99 cmd << credentials_string
|
Chris@441
|
100 shellout(cmd) do |io|
|
Chris@441
|
101 output = io.read
|
Chris@441
|
102 if output.respond_to?(:force_encoding)
|
Chris@441
|
103 output.force_encoding('UTF-8')
|
Chris@441
|
104 end
|
Chris@441
|
105 begin
|
Chris@1115
|
106 doc = parse_xml(output)
|
Chris@441
|
107 each_xml_element(doc['lists']['list'], 'entry') do |entry|
|
Chris@441
|
108 commit = entry['commit']
|
Chris@441
|
109 commit_date = commit['date']
|
Chris@441
|
110 # Skip directory if there is no commit date (usually that
|
Chris@441
|
111 # means that we don't have read access to it)
|
Chris@441
|
112 next if entry['kind'] == 'dir' && commit_date.nil?
|
Chris@441
|
113 name = entry['name']['__content__']
|
Chris@441
|
114 entries << Entry.new({:name => URI.unescape(name),
|
Chris@441
|
115 :path => ((path.empty? ? "" : "#{path}/") + name),
|
Chris@441
|
116 :kind => entry['kind'],
|
Chris@441
|
117 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
|
Chris@441
|
118 :lastrev => Revision.new({
|
Chris@441
|
119 :identifier => commit['revision'],
|
Chris@441
|
120 :time => Time.parse(commit_date['__content__'].to_s).localtime,
|
Chris@441
|
121 :author => ((a = commit['author']) ? a['__content__'] : nil)
|
Chris@441
|
122 })
|
Chris@441
|
123 })
|
Chris@441
|
124 end
|
Chris@441
|
125 rescue Exception => e
|
Chris@441
|
126 logger.error("Error parsing svn output: #{e.message}")
|
Chris@441
|
127 logger.error("Output was:\n #{output}")
|
Chris@441
|
128 end
|
Chris@441
|
129 end
|
Chris@441
|
130 return nil if $? && $?.exitstatus != 0
|
Chris@441
|
131 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
|
Chris@441
|
132 entries.sort_by_name
|
Chris@441
|
133 end
|
Chris@441
|
134
|
Chris@441
|
135 def properties(path, identifier=nil)
|
Chris@441
|
136 # proplist xml output supported in svn 1.5.0 and higher
|
Chris@441
|
137 return nil unless self.class.client_version_above?([1, 5, 0])
|
Chris@441
|
138
|
Chris@441
|
139 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
Chris@441
|
140 cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
|
Chris@441
|
141 cmd << credentials_string
|
Chris@441
|
142 properties = {}
|
Chris@441
|
143 shellout(cmd) do |io|
|
Chris@441
|
144 output = io.read
|
Chris@441
|
145 if output.respond_to?(:force_encoding)
|
Chris@441
|
146 output.force_encoding('UTF-8')
|
Chris@441
|
147 end
|
Chris@441
|
148 begin
|
Chris@1115
|
149 doc = parse_xml(output)
|
Chris@441
|
150 each_xml_element(doc['properties']['target'], 'property') do |property|
|
Chris@441
|
151 properties[ property['name'] ] = property['__content__'].to_s
|
Chris@441
|
152 end
|
Chris@441
|
153 rescue
|
Chris@441
|
154 end
|
Chris@441
|
155 end
|
Chris@441
|
156 return nil if $? && $?.exitstatus != 0
|
Chris@441
|
157 properties
|
Chris@441
|
158 end
|
Chris@441
|
159
|
Chris@441
|
160 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
|
Chris@441
|
161 path ||= ''
|
Chris@441
|
162 identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
|
Chris@441
|
163 identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
|
Chris@441
|
164 revisions = Revisions.new
|
Chris@441
|
165 cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
|
Chris@441
|
166 cmd << credentials_string
|
Chris@441
|
167 cmd << " --verbose " if options[:with_paths]
|
Chris@441
|
168 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
|
Chris@441
|
169 cmd << ' ' + target(path)
|
Chris@441
|
170 shellout(cmd) do |io|
|
Chris@441
|
171 output = io.read
|
Chris@441
|
172 if output.respond_to?(:force_encoding)
|
Chris@441
|
173 output.force_encoding('UTF-8')
|
Chris@441
|
174 end
|
Chris@441
|
175 begin
|
Chris@1115
|
176 doc = parse_xml(output)
|
Chris@441
|
177 each_xml_element(doc['log'], 'logentry') do |logentry|
|
Chris@441
|
178 paths = []
|
Chris@441
|
179 each_xml_element(logentry['paths'], 'path') do |path|
|
Chris@441
|
180 paths << {:action => path['action'],
|
Chris@441
|
181 :path => path['__content__'],
|
Chris@441
|
182 :from_path => path['copyfrom-path'],
|
Chris@441
|
183 :from_revision => path['copyfrom-rev']
|
Chris@441
|
184 }
|
Chris@441
|
185 end if logentry['paths'] && logentry['paths']['path']
|
Chris@441
|
186 paths.sort! { |x,y| x[:path] <=> y[:path] }
|
Chris@441
|
187
|
Chris@441
|
188 revisions << Revision.new({:identifier => logentry['revision'],
|
Chris@441
|
189 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
|
Chris@441
|
190 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
|
Chris@441
|
191 :message => logentry['msg']['__content__'],
|
Chris@441
|
192 :paths => paths
|
Chris@441
|
193 })
|
Chris@441
|
194 end
|
Chris@441
|
195 rescue
|
Chris@441
|
196 end
|
Chris@441
|
197 end
|
Chris@441
|
198 return nil if $? && $?.exitstatus != 0
|
Chris@441
|
199 revisions
|
Chris@441
|
200 end
|
Chris@441
|
201
|
Chris@1115
|
202 def diff(path, identifier_from, identifier_to=nil)
|
Chris@441
|
203 path ||= ''
|
Chris@441
|
204 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
|
Chris@441
|
205
|
Chris@441
|
206 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
|
Chris@441
|
207
|
Chris@441
|
208 cmd = "#{self.class.sq_bin} diff -r "
|
Chris@441
|
209 cmd << "#{identifier_to}:"
|
Chris@441
|
210 cmd << "#{identifier_from}"
|
Chris@441
|
211 cmd << " #{target(path)}@#{identifier_from}"
|
Chris@441
|
212 cmd << credentials_string
|
Chris@441
|
213 diff = []
|
Chris@441
|
214 shellout(cmd) do |io|
|
Chris@441
|
215 io.each_line do |line|
|
Chris@441
|
216 diff << line
|
Chris@441
|
217 end
|
Chris@441
|
218 end
|
Chris@441
|
219 return nil if $? && $?.exitstatus != 0
|
Chris@441
|
220 diff
|
Chris@441
|
221 end
|
Chris@441
|
222
|
Chris@441
|
223 def cat(path, identifier=nil)
|
Chris@441
|
224 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
Chris@441
|
225 cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
|
Chris@441
|
226 cmd << credentials_string
|
Chris@441
|
227 cat = nil
|
Chris@441
|
228 shellout(cmd) do |io|
|
Chris@441
|
229 io.binmode
|
Chris@441
|
230 cat = io.read
|
Chris@441
|
231 end
|
Chris@441
|
232 return nil if $? && $?.exitstatus != 0
|
Chris@441
|
233 cat
|
Chris@441
|
234 end
|
Chris@441
|
235
|
Chris@441
|
236 def annotate(path, identifier=nil)
|
Chris@441
|
237 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
Chris@441
|
238 cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
|
Chris@441
|
239 cmd << credentials_string
|
Chris@441
|
240 blame = Annotate.new
|
Chris@441
|
241 shellout(cmd) do |io|
|
Chris@441
|
242 io.each_line do |line|
|
Chris@441
|
243 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
|
Chris@441
|
244 rev = $1
|
Chris@441
|
245 blame.add_line($3.rstrip,
|
Chris@441
|
246 Revision.new(
|
Chris@441
|
247 :identifier => rev,
|
Chris@441
|
248 :revision => rev,
|
Chris@441
|
249 :author => $2.strip
|
Chris@441
|
250 ))
|
Chris@441
|
251 end
|
Chris@441
|
252 end
|
Chris@441
|
253 return nil if $? && $?.exitstatus != 0
|
Chris@441
|
254 blame
|
Chris@441
|
255 end
|
Chris@441
|
256
|
Chris@441
|
257 private
|
Chris@441
|
258
|
Chris@441
|
259 def credentials_string
|
Chris@441
|
260 str = ''
|
Chris@441
|
261 str << " --username #{shell_quote(@login)}" unless @login.blank?
|
Chris@441
|
262 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
|
Chris@441
|
263 str << " --no-auth-cache --non-interactive"
|
Chris@441
|
264 str
|
Chris@441
|
265 end
|
Chris@441
|
266
|
Chris@441
|
267 # Helper that iterates over the child elements of a xml node
|
Chris@441
|
268 # MiniXml returns a hash when a single child is found
|
Chris@441
|
269 # or an array of hashes for multiple children
|
Chris@441
|
270 def each_xml_element(node, name)
|
Chris@441
|
271 if node && node[name]
|
Chris@441
|
272 if node[name].is_a?(Hash)
|
Chris@441
|
273 yield node[name]
|
Chris@441
|
274 else
|
Chris@441
|
275 node[name].each do |element|
|
Chris@441
|
276 yield element
|
Chris@441
|
277 end
|
Chris@441
|
278 end
|
Chris@441
|
279 end
|
Chris@441
|
280 end
|
Chris@441
|
281
|
Chris@441
|
282 def target(path = '')
|
Chris@441
|
283 base = path.match(/^\//) ? root_url : url
|
Chris@441
|
284 uri = "#{base}/#{path}"
|
Chris@441
|
285 uri = URI.escape(URI.escape(uri), '[]')
|
Chris@441
|
286 shell_quote(uri.gsub(/[?<>\*]/, ''))
|
Chris@441
|
287 end
|
Chris@441
|
288 end
|
Chris@441
|
289 end
|
Chris@441
|
290 end
|
Chris@441
|
291 end
|