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
|
ActiveSupport::XmlMini.backend = 'LibXML'
|
152
|
doc = ActiveSupport::XmlMini.parse(output)
|
153
|
each_xml_element(doc['log'], 'logentry') do |logentry|
|
154
|
|
155
|
logger.info ('reading revision ' + logentry['revision'])
|
156
|
|
157
|
paths = []
|
158
|
each_xml_element(logentry['paths'], 'path') do |path|
|
159
|
|
160
|
logger.info ('reading path ' + path['__content__'])
|
161
|
|
162
|
paths << {:action => path['action'],
|
163
|
:path => path['__content__'],
|
164
|
:from_path => path['copyfrom-path'],
|
165
|
:from_revision => path['copyfrom-rev']
|
166
|
}
|
167
|
end if logentry['paths'] && logentry['paths']['path']
|
168
|
|
169
|
logger.info 'before paths array sorting'
|
170
|
|
171
|
paths.sort! { |x,y| x[:path] <=> y[:path] }
|
172
|
|
173
|
revisions << Revision.new({:identifier => logentry['revision'],
|
174
|
:author => (logentry['author'] ? logentry['author']['__content__'] : ""),
|
175
|
:time => Time.parse(logentry['date']['__content__'].to_s).localtime,
|
176
|
:message => logentry['msg']['__content__'],
|
177
|
:paths => paths
|
178
|
})
|
179
|
end
|
180
|
rescue => e
|
181
|
logger.info 'exception thrown : ' + e.inspect
|
182
|
end
|
183
|
end
|
184
|
logger.info 'end of fetching'
|
185
|
return nil if $? && $?.exitstatus != 0
|
186
|
revisions
|
187
|
end
|
188
|
|
189
|
def diff(path, identifier_from, identifier_to=nil, type="inline")
|
190
|
path ||= ''
|
191
|
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
|
192
|
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
|
193
|
|
194
|
cmd = "#{SVN_BIN} diff -r "
|
195
|
cmd << "#{identifier_to}:"
|
196
|
cmd << "#{identifier_from}"
|
197
|
cmd << " #{target(path)}@#{identifier_from}"
|
198
|
cmd << credentials_string
|
199
|
diff = []
|
200
|
shellout(cmd) do |io|
|
201
|
io.each_line do |line|
|
202
|
diff << line
|
203
|
end
|
204
|
end
|
205
|
return nil if $? && $?.exitstatus != 0
|
206
|
diff
|
207
|
end
|
208
|
|
209
|
def cat(path, identifier=nil)
|
210
|
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
211
|
cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
|
212
|
cmd << credentials_string
|
213
|
cat = nil
|
214
|
shellout(cmd) do |io|
|
215
|
io.binmode
|
216
|
cat = io.read
|
217
|
end
|
218
|
return nil if $? && $?.exitstatus != 0
|
219
|
cat
|
220
|
end
|
221
|
|
222
|
def annotate(path, identifier=nil)
|
223
|
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
|
224
|
cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
|
225
|
cmd << credentials_string
|
226
|
blame = Annotate.new
|
227
|
shellout(cmd) do |io|
|
228
|
io.each_line do |line|
|
229
|
next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
|
230
|
blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
|
231
|
end
|
232
|
end
|
233
|
return nil if $? && $?.exitstatus != 0
|
234
|
blame
|
235
|
end
|
236
|
|
237
|
private
|
238
|
|
239
|
def credentials_string
|
240
|
str = ''
|
241
|
str << " --username #{shell_quote(@login)}" unless @login.blank?
|
242
|
str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
|
243
|
str << " --no-auth-cache --non-interactive"
|
244
|
str
|
245
|
end
|
246
|
|
247
|
# Helper that iterates over the child elements of a xml node
|
248
|
# MiniXml returns a hash when a single child is found or an array of hashes for multiple children
|
249
|
def each_xml_element(node, name)
|
250
|
if node && node[name]
|
251
|
if node[name].is_a?(Hash)
|
252
|
yield node[name]
|
253
|
else
|
254
|
node[name].each do |element|
|
255
|
yield element
|
256
|
end
|
257
|
end
|
258
|
end
|
259
|
end
|
260
|
|
261
|
def target(path = '')
|
262
|
base = path.match(/^\//) ? root_url : url
|
263
|
uri = "#{base}/#{path}"
|
264
|
uri = URI.escape(URI.escape(uri), '[]')
|
265
|
shell_quote(uri.gsub(/[?<>\*]/, ''))
|
266
|
end
|
267
|
end
|
268
|
end
|
269
|
end
|
270
|
end
|