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