##// END OF EJS Templates
scm: use shell quote for scm command at adapter level (#7517, #4273)....
Toshi MARUYAMA -
r4701:8b98c05879a2
parent child
Show More
@@ -1,126 +1,126
1 # = Redmine configuration file
1 # = Redmine configuration file
2 #
2 #
3 # Each environment has it's own configuration options. If you are only
3 # Each environment has it's own configuration options. If you are only
4 # running in production, only the production block needs to be configured.
4 # running in production, only the production block needs to be configured.
5 # Environment specific configuration options override the default ones.
5 # Environment specific configuration options override the default ones.
6 #
6 #
7 # Note that this file needs to be a valid YAML file.
7 # Note that this file needs to be a valid YAML file.
8 #
8 #
9 # == Outgoing email settings (email_delivery setting)
9 # == Outgoing email settings (email_delivery setting)
10 #
10 #
11 # === Common configurations
11 # === Common configurations
12 #
12 #
13 # ==== Sendmail command
13 # ==== Sendmail command
14 #
14 #
15 # production:
15 # production:
16 # email_delivery:
16 # email_delivery:
17 # delivery_method: :sendmail
17 # delivery_method: :sendmail
18 #
18 #
19 # ==== Simple SMTP server at localhost
19 # ==== Simple SMTP server at localhost
20 #
20 #
21 # production:
21 # production:
22 # email_delivery:
22 # email_delivery:
23 # delivery_method: :smtp
23 # delivery_method: :smtp
24 # smtp_settings:
24 # smtp_settings:
25 # address: "localhost"
25 # address: "localhost"
26 # port: 25
26 # port: 25
27 #
27 #
28 # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com
28 # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com
29 #
29 #
30 # production:
30 # production:
31 # email_delivery:
31 # email_delivery:
32 # delivery_method: :smtp
32 # delivery_method: :smtp
33 # smtp_settings:
33 # smtp_settings:
34 # address: "example.com"
34 # address: "example.com"
35 # port: 25
35 # port: 25
36 # authentication: :login
36 # authentication: :login
37 # domain: 'foo.com'
37 # domain: 'foo.com'
38 # user_name: 'myaccount'
38 # user_name: 'myaccount'
39 # password: 'password'
39 # password: 'password'
40 #
40 #
41 # ==== SMTP server at example.com using PLAIN authentication
41 # ==== SMTP server at example.com using PLAIN authentication
42 #
42 #
43 # production:
43 # production:
44 # email_delivery:
44 # email_delivery:
45 # delivery_method: :smtp
45 # delivery_method: :smtp
46 # smtp_settings:
46 # smtp_settings:
47 # address: "example.com"
47 # address: "example.com"
48 # port: 25
48 # port: 25
49 # authentication: :plain
49 # authentication: :plain
50 # domain: 'example.com'
50 # domain: 'example.com'
51 # user_name: 'myaccount'
51 # user_name: 'myaccount'
52 # password: 'password'
52 # password: 'password'
53 #
53 #
54 # ==== SMTP server at using TLS (GMail)
54 # ==== SMTP server at using TLS (GMail)
55 #
55 #
56 # This requires some additional configuration. See the article at:
56 # This requires some additional configuration. See the article at:
57 # http://redmineblog.com/articles/setup-redmine-to-send-email-using-gmail/
57 # http://redmineblog.com/articles/setup-redmine-to-send-email-using-gmail/
58 #
58 #
59 # production:
59 # production:
60 # email_delivery:
60 # email_delivery:
61 # delivery_method: :smtp
61 # delivery_method: :smtp
62 # smtp_settings:
62 # smtp_settings:
63 # tls: true
63 # tls: true
64 # address: "smtp.gmail.com"
64 # address: "smtp.gmail.com"
65 # port: 587
65 # port: 587
66 # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
66 # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
67 # authentication: :plain
67 # authentication: :plain
68 # user_name: "your_email@gmail.com"
68 # user_name: "your_email@gmail.com"
69 # password: "your_password"
69 # password: "your_password"
70 #
70 #
71 #
71 #
72 # === More configuration options
72 # === More configuration options
73 #
73 #
74 # See the "Configuration options" at the following website for a list of the
74 # See the "Configuration options" at the following website for a list of the
75 # full options allowed:
75 # full options allowed:
76 #
76 #
77 # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer
77 # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer
78
78
79
79
80 # default configuration options for all environments
80 # default configuration options for all environments
81 default:
81 default:
82 # Outgoing emails configuration (see examples above)
82 # Outgoing emails configuration (see examples above)
83 email_delivery:
83 email_delivery:
84 delivery_method: :smtp
84 delivery_method: :smtp
85 smtp_settings:
85 smtp_settings:
86 address: smtp.example.net
86 address: smtp.example.net
87 port: 25
87 port: 25
88 domain: example.net
88 domain: example.net
89 authentication: :login
89 authentication: :login
90 user_name: "redmine@example.net"
90 user_name: "redmine@example.net"
91 password: "redmine"
91 password: "redmine"
92
92
93 # Absolute path to the directory where attachments are stored.
93 # Absolute path to the directory where attachments are stored.
94 # The default is the 'files' directory in your Redmine instance.
94 # The default is the 'files' directory in your Redmine instance.
95 # Your Redmine instance needs to have write permission on this
95 # Your Redmine instance needs to have write permission on this
96 # directory.
96 # directory.
97 # Examples:
97 # Examples:
98 # attachments_storage_path: /var/redmine/files
98 # attachments_storage_path: /var/redmine/files
99 # attachments_storage_path: D:/redmine/files
99 # attachments_storage_path: D:/redmine/files
100 attachments_storage_path:
100 attachments_storage_path:
101
101
102 # Configuration of the autologin cookie.
102 # Configuration of the autologin cookie.
103 # autologin_cookie_name: the name of the cookie (default: autologin)
103 # autologin_cookie_name: the name of the cookie (default: autologin)
104 # autologin_cookie_path: the cookie path (default: /)
104 # autologin_cookie_path: the cookie path (default: /)
105 # autologin_cookie_secure: true sets the cookie secure flag (default: false)
105 # autologin_cookie_secure: true sets the cookie secure flag (default: false)
106 autologin_cookie_name:
106 autologin_cookie_name:
107 autologin_cookie_path:
107 autologin_cookie_path:
108 autologin_cookie_secure:
108 autologin_cookie_secure:
109
109
110 # Configuration of SCM executable command.
110 # Configuration of SCM executable command.
111 # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe)
111 # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe)
112 # On Windows, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work.
112 # On Windows, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work.
113 scm_subversion_command: svn # (default: svn)
113 scm_subversion_command: svn # (default: svn)
114 scm_mercurial_command: "\"C:\Program Files\TortoiseHg\hg.exe\"" # (default: hg)
114 scm_mercurial_command: C:\Program Files\TortoiseHg\hg.exe # (default: hg)
115 scm_git_command: /usr/local/bin/git # (default: git)
115 scm_git_command: /usr/local/bin/git # (default: git)
116 scm_cvs_command: cvs # (default: cvs)
116 scm_cvs_command: cvs # (default: cvs)
117 scm_bazaar_command: bzr.exe # (default: bzr)
117 scm_bazaar_command: bzr.exe # (default: bzr)
118 scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs)
118 scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs)
119
119
120 # specific configuration options for production environment
120 # specific configuration options for production environment
121 # that overrides the default ones
121 # that overrides the default ones
122 production:
122 production:
123
123
124 # specific configuration options for development environment
124 # specific configuration options for development environment
125 # that overrides the default ones
125 # that overrides the default ones
126 development:
126 development:
@@ -1,344 +1,356
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'cgi'
18 require 'cgi'
19
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class CommandFailed < StandardError #:nodoc:
23 class CommandFailed < StandardError #:nodoc:
24 end
24 end
25
25
26 class AbstractAdapter #:nodoc:
26 class AbstractAdapter #:nodoc:
27 class << self
27 class << self
28 def client_command
29 ""
30 end
31
28 # Returns the version of the scm client
32 # Returns the version of the scm client
29 # Eg: [1, 5, 0] or [] if unknown
33 # Eg: [1, 5, 0] or [] if unknown
30 def client_version
34 def client_version
31 []
35 []
32 end
36 end
33
37
34 # Returns the version string of the scm client
38 # Returns the version string of the scm client
35 # Eg: '1.5.0' or 'Unknown version' if unknown
39 # Eg: '1.5.0' or 'Unknown version' if unknown
36 def client_version_string
40 def client_version_string
37 v = client_version || 'Unknown version'
41 v = client_version || 'Unknown version'
38 v.is_a?(Array) ? v.join('.') : v.to_s
42 v.is_a?(Array) ? v.join('.') : v.to_s
39 end
43 end
40
44
41 # Returns true if the current client version is above
45 # Returns true if the current client version is above
42 # or equals the given one
46 # or equals the given one
43 # If option is :unknown is set to true, it will return
47 # If option is :unknown is set to true, it will return
44 # true if the client version is unknown
48 # true if the client version is unknown
45 def client_version_above?(v, options={})
49 def client_version_above?(v, options={})
46 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
50 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
47 end
51 end
52
53 def client_available
54 true
55 end
56
57 def shell_quote(str)
58 if Redmine::Platform.mswin?
59 '"' + str.gsub(/"/, '\\"') + '"'
60 else
61 "'" + str.gsub(/'/, "'\"'\"'") + "'"
62 end
63 end
48 end
64 end
49
65
50 def initialize(url, root_url=nil, login=nil, password=nil)
66 def initialize(url, root_url=nil, login=nil, password=nil)
51 @url = url
67 @url = url
52 @login = login if login && !login.empty?
68 @login = login if login && !login.empty?
53 @password = (password || "") if @login
69 @password = (password || "") if @login
54 @root_url = root_url.blank? ? retrieve_root_url : root_url
70 @root_url = root_url.blank? ? retrieve_root_url : root_url
55 end
71 end
56
72
57 def adapter_name
73 def adapter_name
58 'Abstract'
74 'Abstract'
59 end
75 end
60
76
61 def supports_cat?
77 def supports_cat?
62 true
78 true
63 end
79 end
64
80
65 def supports_annotate?
81 def supports_annotate?
66 respond_to?('annotate')
82 respond_to?('annotate')
67 end
83 end
68
84
69 def root_url
85 def root_url
70 @root_url
86 @root_url
71 end
87 end
72
88
73 def url
89 def url
74 @url
90 @url
75 end
91 end
76
92
77 # get info about the svn repository
93 # get info about the svn repository
78 def info
94 def info
79 return nil
95 return nil
80 end
96 end
81
97
82 # Returns the entry identified by path and revision identifier
98 # Returns the entry identified by path and revision identifier
83 # or nil if entry doesn't exist in the repository
99 # or nil if entry doesn't exist in the repository
84 def entry(path=nil, identifier=nil)
100 def entry(path=nil, identifier=nil)
85 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
101 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
86 search_path = parts[0..-2].join('/')
102 search_path = parts[0..-2].join('/')
87 search_name = parts[-1]
103 search_name = parts[-1]
88 if search_path.blank? && search_name.blank?
104 if search_path.blank? && search_name.blank?
89 # Root entry
105 # Root entry
90 Entry.new(:path => '', :kind => 'dir')
106 Entry.new(:path => '', :kind => 'dir')
91 else
107 else
92 # Search for the entry in the parent directory
108 # Search for the entry in the parent directory
93 es = entries(search_path, identifier)
109 es = entries(search_path, identifier)
94 es ? es.detect {|e| e.name == search_name} : nil
110 es ? es.detect {|e| e.name == search_name} : nil
95 end
111 end
96 end
112 end
97
113
98 # Returns an Entries collection
114 # Returns an Entries collection
99 # or nil if the given path doesn't exist in the repository
115 # or nil if the given path doesn't exist in the repository
100 def entries(path=nil, identifier=nil)
116 def entries(path=nil, identifier=nil)
101 return nil
117 return nil
102 end
118 end
103
119
104 def branches
120 def branches
105 return nil
121 return nil
106 end
122 end
107
123
108 def tags
124 def tags
109 return nil
125 return nil
110 end
126 end
111
127
112 def default_branch
128 def default_branch
113 return nil
129 return nil
114 end
130 end
115
131
116 def properties(path, identifier=nil)
132 def properties(path, identifier=nil)
117 return nil
133 return nil
118 end
134 end
119
135
120 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
136 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
121 return nil
137 return nil
122 end
138 end
123
139
124 def diff(path, identifier_from, identifier_to=nil)
140 def diff(path, identifier_from, identifier_to=nil)
125 return nil
141 return nil
126 end
142 end
127
143
128 def cat(path, identifier=nil)
144 def cat(path, identifier=nil)
129 return nil
145 return nil
130 end
146 end
131
147
132 def with_leading_slash(path)
148 def with_leading_slash(path)
133 path ||= ''
149 path ||= ''
134 (path[0,1]!="/") ? "/#{path}" : path
150 (path[0,1]!="/") ? "/#{path}" : path
135 end
151 end
136
152
137 def with_trailling_slash(path)
153 def with_trailling_slash(path)
138 path ||= ''
154 path ||= ''
139 (path[-1,1] == "/") ? path : "#{path}/"
155 (path[-1,1] == "/") ? path : "#{path}/"
140 end
156 end
141
157
142 def without_leading_slash(path)
158 def without_leading_slash(path)
143 path ||= ''
159 path ||= ''
144 path.gsub(%r{^/+}, '')
160 path.gsub(%r{^/+}, '')
145 end
161 end
146
162
147 def without_trailling_slash(path)
163 def without_trailling_slash(path)
148 path ||= ''
164 path ||= ''
149 (path[-1,1] == "/") ? path[0..-2] : path
165 (path[-1,1] == "/") ? path[0..-2] : path
150 end
166 end
151
167
152 def shell_quote(str)
168 def shell_quote(str)
153 if Redmine::Platform.mswin?
169 self.class.shell_quote(str)
154 '"' + str.gsub(/"/, '\\"') + '"'
155 else
156 "'" + str.gsub(/'/, "'\"'\"'") + "'"
157 end
158 end
170 end
159
171
160 private
172 private
161 def retrieve_root_url
173 def retrieve_root_url
162 info = self.info
174 info = self.info
163 info ? info.root_url : nil
175 info ? info.root_url : nil
164 end
176 end
165
177
166 def target(path)
178 def target(path)
167 path ||= ''
179 path ||= ''
168 base = path.match(/^\//) ? root_url : url
180 base = path.match(/^\//) ? root_url : url
169 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
181 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
170 end
182 end
171
183
172 def logger
184 def logger
173 self.class.logger
185 self.class.logger
174 end
186 end
175
187
176 def shellout(cmd, &block)
188 def shellout(cmd, &block)
177 self.class.shellout(cmd, &block)
189 self.class.shellout(cmd, &block)
178 end
190 end
179
191
180 def self.logger
192 def self.logger
181 RAILS_DEFAULT_LOGGER
193 RAILS_DEFAULT_LOGGER
182 end
194 end
183
195
184 def self.shellout(cmd, &block)
196 def self.shellout(cmd, &block)
185 logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug?
197 logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug?
186 if Rails.env == 'development'
198 if Rails.env == 'development'
187 # Capture stderr when running in dev environment
199 # Capture stderr when running in dev environment
188 cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
200 cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
189 end
201 end
190 begin
202 begin
191 IO.popen(cmd, "r+") do |io|
203 IO.popen(cmd, "r+") do |io|
192 io.close_write
204 io.close_write
193 block.call(io) if block_given?
205 block.call(io) if block_given?
194 end
206 end
195 rescue Errno::ENOENT => e
207 rescue Errno::ENOENT => e
196 msg = strip_credential(e.message)
208 msg = strip_credential(e.message)
197 # The command failed, log it and re-raise
209 # The command failed, log it and re-raise
198 logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
210 logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
199 raise CommandFailed.new(msg)
211 raise CommandFailed.new(msg)
200 end
212 end
201 end
213 end
202
214
203 # Hides username/password in a given command
215 # Hides username/password in a given command
204 def self.strip_credential(cmd)
216 def self.strip_credential(cmd)
205 q = (Redmine::Platform.mswin? ? '"' : "'")
217 q = (Redmine::Platform.mswin? ? '"' : "'")
206 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
218 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
207 end
219 end
208
220
209 def strip_credential(cmd)
221 def strip_credential(cmd)
210 self.class.strip_credential(cmd)
222 self.class.strip_credential(cmd)
211 end
223 end
212 end
224 end
213
225
214 class Entries < Array
226 class Entries < Array
215 def sort_by_name
227 def sort_by_name
216 sort {|x,y|
228 sort {|x,y|
217 if x.kind == y.kind
229 if x.kind == y.kind
218 x.name.to_s <=> y.name.to_s
230 x.name.to_s <=> y.name.to_s
219 else
231 else
220 x.kind <=> y.kind
232 x.kind <=> y.kind
221 end
233 end
222 }
234 }
223 end
235 end
224
236
225 def revisions
237 def revisions
226 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
238 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
227 end
239 end
228 end
240 end
229
241
230 class Info
242 class Info
231 attr_accessor :root_url, :lastrev
243 attr_accessor :root_url, :lastrev
232 def initialize(attributes={})
244 def initialize(attributes={})
233 self.root_url = attributes[:root_url] if attributes[:root_url]
245 self.root_url = attributes[:root_url] if attributes[:root_url]
234 self.lastrev = attributes[:lastrev]
246 self.lastrev = attributes[:lastrev]
235 end
247 end
236 end
248 end
237
249
238 class Entry
250 class Entry
239 attr_accessor :name, :path, :kind, :size, :lastrev
251 attr_accessor :name, :path, :kind, :size, :lastrev
240 def initialize(attributes={})
252 def initialize(attributes={})
241 self.name = attributes[:name] if attributes[:name]
253 self.name = attributes[:name] if attributes[:name]
242 self.path = attributes[:path] if attributes[:path]
254 self.path = attributes[:path] if attributes[:path]
243 self.kind = attributes[:kind] if attributes[:kind]
255 self.kind = attributes[:kind] if attributes[:kind]
244 self.size = attributes[:size].to_i if attributes[:size]
256 self.size = attributes[:size].to_i if attributes[:size]
245 self.lastrev = attributes[:lastrev]
257 self.lastrev = attributes[:lastrev]
246 end
258 end
247
259
248 def is_file?
260 def is_file?
249 'file' == self.kind
261 'file' == self.kind
250 end
262 end
251
263
252 def is_dir?
264 def is_dir?
253 'dir' == self.kind
265 'dir' == self.kind
254 end
266 end
255
267
256 def is_text?
268 def is_text?
257 Redmine::MimeType.is_type?('text', name)
269 Redmine::MimeType.is_type?('text', name)
258 end
270 end
259 end
271 end
260
272
261 class Revisions < Array
273 class Revisions < Array
262 def latest
274 def latest
263 sort {|x,y|
275 sort {|x,y|
264 unless x.time.nil? or y.time.nil?
276 unless x.time.nil? or y.time.nil?
265 x.time <=> y.time
277 x.time <=> y.time
266 else
278 else
267 0
279 0
268 end
280 end
269 }.last
281 }.last
270 end
282 end
271 end
283 end
272
284
273 class Revision
285 class Revision
274 attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
286 attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
275 attr_writer :identifier
287 attr_writer :identifier
276
288
277 def initialize(attributes={})
289 def initialize(attributes={})
278 self.identifier = attributes[:identifier]
290 self.identifier = attributes[:identifier]
279 self.scmid = attributes[:scmid]
291 self.scmid = attributes[:scmid]
280 self.name = attributes[:name] || self.identifier
292 self.name = attributes[:name] || self.identifier
281 self.author = attributes[:author]
293 self.author = attributes[:author]
282 self.time = attributes[:time]
294 self.time = attributes[:time]
283 self.message = attributes[:message] || ""
295 self.message = attributes[:message] || ""
284 self.paths = attributes[:paths]
296 self.paths = attributes[:paths]
285 self.revision = attributes[:revision]
297 self.revision = attributes[:revision]
286 self.branch = attributes[:branch]
298 self.branch = attributes[:branch]
287 end
299 end
288
300
289 # Returns the identifier of this revision; see also Changeset model
301 # Returns the identifier of this revision; see also Changeset model
290 def identifier
302 def identifier
291 (@identifier || revision).to_s
303 (@identifier || revision).to_s
292 end
304 end
293
305
294 # Returns the readable identifier.
306 # Returns the readable identifier.
295 def format_identifier
307 def format_identifier
296 identifier
308 identifier
297 end
309 end
298
310
299 def save(repo)
311 def save(repo)
300 Changeset.transaction do
312 Changeset.transaction do
301 changeset = Changeset.new(
313 changeset = Changeset.new(
302 :repository => repo,
314 :repository => repo,
303 :revision => identifier,
315 :revision => identifier,
304 :scmid => scmid,
316 :scmid => scmid,
305 :committer => author,
317 :committer => author,
306 :committed_on => time,
318 :committed_on => time,
307 :comments => message)
319 :comments => message)
308
320
309 if changeset.save
321 if changeset.save
310 paths.each do |file|
322 paths.each do |file|
311 Change.create(
323 Change.create(
312 :changeset => changeset,
324 :changeset => changeset,
313 :action => file[:action],
325 :action => file[:action],
314 :path => file[:path])
326 :path => file[:path])
315 end
327 end
316 end
328 end
317 end
329 end
318 end
330 end
319 end
331 end
320
332
321 class Annotate
333 class Annotate
322 attr_reader :lines, :revisions
334 attr_reader :lines, :revisions
323
335
324 def initialize
336 def initialize
325 @lines = []
337 @lines = []
326 @revisions = []
338 @revisions = []
327 end
339 end
328
340
329 def add_line(line, revision)
341 def add_line(line, revision)
330 @lines << line
342 @lines << line
331 @revisions << revision
343 @revisions << revision
332 end
344 end
333
345
334 def content
346 def content
335 content = lines.join("\n")
347 content = lines.join("\n")
336 end
348 end
337
349
338 def empty?
350 def empty?
339 lines.empty?
351 lines.empty?
340 end
352 end
341 end
353 end
342 end
354 end
343 end
355 end
344 end
356 end
@@ -1,189 +1,199
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class BazaarAdapter < AbstractAdapter
23 class BazaarAdapter < AbstractAdapter
24
24
25 # Bazaar executable name
25 # Bazaar executable name
26 BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
26 BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
27
27
28 class << self
29 def client_command
30 @@bin ||= BZR_BIN
31 end
32
33 def sq_bin
34 @@sq_bin ||= shell_quote(BZR_BIN)
35 end
36 end
37
28 # Get info about the repository
38 # Get info about the repository
29 def info
39 def info
30 cmd = "#{BZR_BIN} revno #{target('')}"
40 cmd = "#{self.class.sq_bin} revno #{target('')}"
31 info = nil
41 info = nil
32 shellout(cmd) do |io|
42 shellout(cmd) do |io|
33 if io.read =~ %r{^(\d+)\r?$}
43 if io.read =~ %r{^(\d+)\r?$}
34 info = Info.new({:root_url => url,
44 info = Info.new({:root_url => url,
35 :lastrev => Revision.new({
45 :lastrev => Revision.new({
36 :identifier => $1
46 :identifier => $1
37 })
47 })
38 })
48 })
39 end
49 end
40 end
50 end
41 return nil if $? && $?.exitstatus != 0
51 return nil if $? && $?.exitstatus != 0
42 info
52 info
43 rescue CommandFailed
53 rescue CommandFailed
44 return nil
54 return nil
45 end
55 end
46
56
47 # Returns an Entries collection
57 # Returns an Entries collection
48 # or nil if the given path doesn't exist in the repository
58 # or nil if the given path doesn't exist in the repository
49 def entries(path=nil, identifier=nil)
59 def entries(path=nil, identifier=nil)
50 path ||= ''
60 path ||= ''
51 entries = Entries.new
61 entries = Entries.new
52 cmd = "#{BZR_BIN} ls -v --show-ids"
62 cmd = "#{self.class.sq_bin} ls -v --show-ids"
53 identifier = -1 unless identifier && identifier.to_i > 0
63 identifier = -1 unless identifier && identifier.to_i > 0
54 cmd << " -r#{identifier.to_i}"
64 cmd << " -r#{identifier.to_i}"
55 cmd << " #{target(path)}"
65 cmd << " #{target(path)}"
56 shellout(cmd) do |io|
66 shellout(cmd) do |io|
57 prefix = "#{url}/#{path}".gsub('\\', '/')
67 prefix = "#{url}/#{path}".gsub('\\', '/')
58 logger.debug "PREFIX: #{prefix}"
68 logger.debug "PREFIX: #{prefix}"
59 re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
69 re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
60 io.each_line do |line|
70 io.each_line do |line|
61 next unless line =~ re
71 next unless line =~ re
62 entries << Entry.new({:name => $3.strip,
72 entries << Entry.new({:name => $3.strip,
63 :path => ((path.empty? ? "" : "#{path}/") + $3.strip),
73 :path => ((path.empty? ? "" : "#{path}/") + $3.strip),
64 :kind => ($4.blank? ? 'file' : 'dir'),
74 :kind => ($4.blank? ? 'file' : 'dir'),
65 :size => nil,
75 :size => nil,
66 :lastrev => Revision.new(:revision => $5.strip)
76 :lastrev => Revision.new(:revision => $5.strip)
67 })
77 })
68 end
78 end
69 end
79 end
70 return nil if $? && $?.exitstatus != 0
80 return nil if $? && $?.exitstatus != 0
71 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
81 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
72 entries.sort_by_name
82 entries.sort_by_name
73 end
83 end
74
84
75 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
85 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
76 path ||= ''
86 path ||= ''
77 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
87 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
78 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
88 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
79 revisions = Revisions.new
89 revisions = Revisions.new
80 cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to}..#{identifier_from} #{target(path)}"
90 cmd = "#{self.class.sq_bin} log -v --show-ids -r#{identifier_to}..#{identifier_from} #{target(path)}"
81 shellout(cmd) do |io|
91 shellout(cmd) do |io|
82 revision = nil
92 revision = nil
83 parsing = nil
93 parsing = nil
84 io.each_line do |line|
94 io.each_line do |line|
85 if line =~ /^----/
95 if line =~ /^----/
86 revisions << revision if revision
96 revisions << revision if revision
87 revision = Revision.new(:paths => [], :message => '')
97 revision = Revision.new(:paths => [], :message => '')
88 parsing = nil
98 parsing = nil
89 else
99 else
90 next unless revision
100 next unless revision
91
101
92 if line =~ /^revno: (\d+)($|\s\[merge\]$)/
102 if line =~ /^revno: (\d+)($|\s\[merge\]$)/
93 revision.identifier = $1.to_i
103 revision.identifier = $1.to_i
94 elsif line =~ /^committer: (.+)$/
104 elsif line =~ /^committer: (.+)$/
95 revision.author = $1.strip
105 revision.author = $1.strip
96 elsif line =~ /^revision-id:(.+)$/
106 elsif line =~ /^revision-id:(.+)$/
97 revision.scmid = $1.strip
107 revision.scmid = $1.strip
98 elsif line =~ /^timestamp: (.+)$/
108 elsif line =~ /^timestamp: (.+)$/
99 revision.time = Time.parse($1).localtime
109 revision.time = Time.parse($1).localtime
100 elsif line =~ /^ -----/
110 elsif line =~ /^ -----/
101 # partial revisions
111 # partial revisions
102 parsing = nil unless parsing == 'message'
112 parsing = nil unless parsing == 'message'
103 elsif line =~ /^(message|added|modified|removed|renamed):/
113 elsif line =~ /^(message|added|modified|removed|renamed):/
104 parsing = $1
114 parsing = $1
105 elsif line =~ /^ (.*)$/
115 elsif line =~ /^ (.*)$/
106 if parsing == 'message'
116 if parsing == 'message'
107 revision.message << "#{$1}\n"
117 revision.message << "#{$1}\n"
108 else
118 else
109 if $1 =~ /^(.*)\s+(\S+)$/
119 if $1 =~ /^(.*)\s+(\S+)$/
110 path = $1.strip
120 path = $1.strip
111 revid = $2
121 revid = $2
112 case parsing
122 case parsing
113 when 'added'
123 when 'added'
114 revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
124 revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
115 when 'modified'
125 when 'modified'
116 revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
126 revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
117 when 'removed'
127 when 'removed'
118 revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
128 revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
119 when 'renamed'
129 when 'renamed'
120 new_path = path.split('=>').last
130 new_path = path.split('=>').last
121 revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
131 revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
122 end
132 end
123 end
133 end
124 end
134 end
125 else
135 else
126 parsing = nil
136 parsing = nil
127 end
137 end
128 end
138 end
129 end
139 end
130 revisions << revision if revision
140 revisions << revision if revision
131 end
141 end
132 return nil if $? && $?.exitstatus != 0
142 return nil if $? && $?.exitstatus != 0
133 revisions
143 revisions
134 end
144 end
135
145
136 def diff(path, identifier_from, identifier_to=nil)
146 def diff(path, identifier_from, identifier_to=nil)
137 path ||= ''
147 path ||= ''
138 if identifier_to
148 if identifier_to
139 identifier_to = identifier_to.to_i
149 identifier_to = identifier_to.to_i
140 else
150 else
141 identifier_to = identifier_from.to_i - 1
151 identifier_to = identifier_from.to_i - 1
142 end
152 end
143 if identifier_from
153 if identifier_from
144 identifier_from = identifier_from.to_i
154 identifier_from = identifier_from.to_i
145 end
155 end
146 cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
156 cmd = "#{self.class.sq_bin} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
147 diff = []
157 diff = []
148 shellout(cmd) do |io|
158 shellout(cmd) do |io|
149 io.each_line do |line|
159 io.each_line do |line|
150 diff << line
160 diff << line
151 end
161 end
152 end
162 end
153 #return nil if $? && $?.exitstatus != 0
163 #return nil if $? && $?.exitstatus != 0
154 diff
164 diff
155 end
165 end
156
166
157 def cat(path, identifier=nil)
167 def cat(path, identifier=nil)
158 cmd = "#{BZR_BIN} cat"
168 cmd = "#{self.class.sq_bin} cat"
159 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
169 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
160 cmd << " #{target(path)}"
170 cmd << " #{target(path)}"
161 cat = nil
171 cat = nil
162 shellout(cmd) do |io|
172 shellout(cmd) do |io|
163 io.binmode
173 io.binmode
164 cat = io.read
174 cat = io.read
165 end
175 end
166 return nil if $? && $?.exitstatus != 0
176 return nil if $? && $?.exitstatus != 0
167 cat
177 cat
168 end
178 end
169
179
170 def annotate(path, identifier=nil)
180 def annotate(path, identifier=nil)
171 cmd = "#{BZR_BIN} annotate --all"
181 cmd = "#{self.class.sq_bin} annotate --all"
172 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
182 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
173 cmd << " #{target(path)}"
183 cmd << " #{target(path)}"
174 blame = Annotate.new
184 blame = Annotate.new
175 shellout(cmd) do |io|
185 shellout(cmd) do |io|
176 author = nil
186 author = nil
177 identifier = nil
187 identifier = nil
178 io.each_line do |line|
188 io.each_line do |line|
179 next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
189 next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
180 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
190 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
181 end
191 end
182 end
192 end
183 return nil if $? && $?.exitstatus != 0
193 return nil if $? && $?.exitstatus != 0
184 blame
194 blame
185 end
195 end
186 end
196 end
187 end
197 end
188 end
198 end
189 end
199 end
@@ -1,371 +1,381
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class CvsAdapter < AbstractAdapter
23 class CvsAdapter < AbstractAdapter
24
24
25 # CVS executable name
25 # CVS executable name
26 CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
26 CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
27
27
28 class << self
29 def client_command
30 @@bin ||= CVS_BIN
31 end
32
33 def sq_bin
34 @@sq_bin ||= shell_quote(CVS_BIN)
35 end
36 end
37
28 # Guidelines for the input:
38 # Guidelines for the input:
29 # url -> the project-path, relative to the cvsroot (eg. module name)
39 # url -> the project-path, relative to the cvsroot (eg. module name)
30 # root_url -> the good old, sometimes damned, CVSROOT
40 # root_url -> the good old, sometimes damned, CVSROOT
31 # login -> unnecessary
41 # login -> unnecessary
32 # password -> unnecessary too
42 # password -> unnecessary too
33 def initialize(url, root_url=nil, login=nil, password=nil)
43 def initialize(url, root_url=nil, login=nil, password=nil)
34 @url = url
44 @url = url
35 @login = login if login && !login.empty?
45 @login = login if login && !login.empty?
36 @password = (password || "") if @login
46 @password = (password || "") if @login
37 #TODO: better Exception here (IllegalArgumentException)
47 #TODO: better Exception here (IllegalArgumentException)
38 raise CommandFailed if root_url.blank?
48 raise CommandFailed if root_url.blank?
39 @root_url = root_url
49 @root_url = root_url
40 end
50 end
41
51
42 def root_url
52 def root_url
43 @root_url
53 @root_url
44 end
54 end
45
55
46 def url
56 def url
47 @url
57 @url
48 end
58 end
49
59
50 def info
60 def info
51 logger.debug "<cvs> info"
61 logger.debug "<cvs> info"
52 Info.new({:root_url => @root_url, :lastrev => nil})
62 Info.new({:root_url => @root_url, :lastrev => nil})
53 end
63 end
54
64
55 def get_previous_revision(revision)
65 def get_previous_revision(revision)
56 CvsRevisionHelper.new(revision).prevRev
66 CvsRevisionHelper.new(revision).prevRev
57 end
67 end
58
68
59 # Returns an Entries collection
69 # Returns an Entries collection
60 # or nil if the given path doesn't exist in the repository
70 # or nil if the given path doesn't exist in the repository
61 # this method is used by the repository-browser (aka LIST)
71 # this method is used by the repository-browser (aka LIST)
62 def entries(path=nil, identifier=nil)
72 def entries(path=nil, identifier=nil)
63 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
73 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
64 path_with_project="#{url}#{with_leading_slash(path)}"
74 path_with_project="#{url}#{with_leading_slash(path)}"
65 entries = Entries.new
75 entries = Entries.new
66 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rls -e"
76 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rls -e"
67 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
77 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
68 cmd << " #{shell_quote path_with_project}"
78 cmd << " #{shell_quote path_with_project}"
69 shellout(cmd) do |io|
79 shellout(cmd) do |io|
70 io.each_line(){|line|
80 io.each_line(){|line|
71 fields=line.chop.split('/',-1)
81 fields=line.chop.split('/',-1)
72 logger.debug(">>InspectLine #{fields.inspect}")
82 logger.debug(">>InspectLine #{fields.inspect}")
73
83
74 if fields[0]!="D"
84 if fields[0]!="D"
75 entries << Entry.new({:name => fields[-5],
85 entries << Entry.new({:name => fields[-5],
76 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
86 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
77 :path => "#{path}/#{fields[-5]}",
87 :path => "#{path}/#{fields[-5]}",
78 :kind => 'file',
88 :kind => 'file',
79 :size => nil,
89 :size => nil,
80 :lastrev => Revision.new({
90 :lastrev => Revision.new({
81 :revision => fields[-4],
91 :revision => fields[-4],
82 :name => fields[-4],
92 :name => fields[-4],
83 :time => Time.parse(fields[-3]),
93 :time => Time.parse(fields[-3]),
84 :author => ''
94 :author => ''
85 })
95 })
86 })
96 })
87 else
97 else
88 entries << Entry.new({:name => fields[1],
98 entries << Entry.new({:name => fields[1],
89 :path => "#{path}/#{fields[1]}",
99 :path => "#{path}/#{fields[1]}",
90 :kind => 'dir',
100 :kind => 'dir',
91 :size => nil,
101 :size => nil,
92 :lastrev => nil
102 :lastrev => nil
93 })
103 })
94 end
104 end
95 }
105 }
96 end
106 end
97 return nil if $? && $?.exitstatus != 0
107 return nil if $? && $?.exitstatus != 0
98 entries.sort_by_name
108 entries.sort_by_name
99 end
109 end
100
110
101 STARTLOG="----------------------------"
111 STARTLOG="----------------------------"
102 ENDLOG ="============================================================================="
112 ENDLOG ="============================================================================="
103
113
104 # Returns all revisions found between identifier_from and identifier_to
114 # Returns all revisions found between identifier_from and identifier_to
105 # in the repository. both identifier have to be dates or nil.
115 # in the repository. both identifier have to be dates or nil.
106 # these method returns nothing but yield every result in block
116 # these method returns nothing but yield every result in block
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
117 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
108 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
118 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
109
119
110 path_with_project="#{url}#{with_leading_slash(path)}"
120 path_with_project="#{url}#{with_leading_slash(path)}"
111 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rlog"
121 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rlog"
112 cmd << " -d\">#{time_to_cvstime_rlog(identifier_from)}\"" if identifier_from
122 cmd << " -d\">#{time_to_cvstime_rlog(identifier_from)}\"" if identifier_from
113 cmd << " #{shell_quote path_with_project}"
123 cmd << " #{shell_quote path_with_project}"
114 shellout(cmd) do |io|
124 shellout(cmd) do |io|
115 state="entry_start"
125 state="entry_start"
116
126
117 commit_log=String.new
127 commit_log=String.new
118 revision=nil
128 revision=nil
119 date=nil
129 date=nil
120 author=nil
130 author=nil
121 entry_path=nil
131 entry_path=nil
122 entry_name=nil
132 entry_name=nil
123 file_state=nil
133 file_state=nil
124 branch_map=nil
134 branch_map=nil
125
135
126 io.each_line() do |line|
136 io.each_line() do |line|
127
137
128 if state!="revision" && /^#{ENDLOG}/ =~ line
138 if state!="revision" && /^#{ENDLOG}/ =~ line
129 commit_log=String.new
139 commit_log=String.new
130 revision=nil
140 revision=nil
131 state="entry_start"
141 state="entry_start"
132 end
142 end
133
143
134 if state=="entry_start"
144 if state=="entry_start"
135 branch_map=Hash.new
145 branch_map=Hash.new
136 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
146 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
137 entry_path = normalize_cvs_path($1)
147 entry_path = normalize_cvs_path($1)
138 entry_name = normalize_path(File.basename($1))
148 entry_name = normalize_path(File.basename($1))
139 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
149 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
140 elsif /^head: (.+)$/ =~ line
150 elsif /^head: (.+)$/ =~ line
141 entry_headRev = $1 #unless entry.nil?
151 entry_headRev = $1 #unless entry.nil?
142 elsif /^symbolic names:/ =~ line
152 elsif /^symbolic names:/ =~ line
143 state="symbolic" #unless entry.nil?
153 state="symbolic" #unless entry.nil?
144 elsif /^#{STARTLOG}/ =~ line
154 elsif /^#{STARTLOG}/ =~ line
145 commit_log=String.new
155 commit_log=String.new
146 state="revision"
156 state="revision"
147 end
157 end
148 next
158 next
149 elsif state=="symbolic"
159 elsif state=="symbolic"
150 if /^(.*):\s(.*)/ =~ (line.strip)
160 if /^(.*):\s(.*)/ =~ (line.strip)
151 branch_map[$1]=$2
161 branch_map[$1]=$2
152 else
162 else
153 state="tags"
163 state="tags"
154 next
164 next
155 end
165 end
156 elsif state=="tags"
166 elsif state=="tags"
157 if /^#{STARTLOG}/ =~ line
167 if /^#{STARTLOG}/ =~ line
158 commit_log = ""
168 commit_log = ""
159 state="revision"
169 state="revision"
160 elsif /^#{ENDLOG}/ =~ line
170 elsif /^#{ENDLOG}/ =~ line
161 state="head"
171 state="head"
162 end
172 end
163 next
173 next
164 elsif state=="revision"
174 elsif state=="revision"
165 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
175 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
166 if revision
176 if revision
167
177
168 revHelper=CvsRevisionHelper.new(revision)
178 revHelper=CvsRevisionHelper.new(revision)
169 revBranch="HEAD"
179 revBranch="HEAD"
170
180
171 branch_map.each() do |branch_name,branch_point|
181 branch_map.each() do |branch_name,branch_point|
172 if revHelper.is_in_branch_with_symbol(branch_point)
182 if revHelper.is_in_branch_with_symbol(branch_point)
173 revBranch=branch_name
183 revBranch=branch_name
174 end
184 end
175 end
185 end
176
186
177 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
187 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
178
188
179 yield Revision.new({
189 yield Revision.new({
180 :time => date,
190 :time => date,
181 :author => author,
191 :author => author,
182 :message=>commit_log.chomp,
192 :message=>commit_log.chomp,
183 :paths => [{
193 :paths => [{
184 :revision => revision,
194 :revision => revision,
185 :branch=> revBranch,
195 :branch=> revBranch,
186 :path=>entry_path,
196 :path=>entry_path,
187 :name=>entry_name,
197 :name=>entry_name,
188 :kind=>'file',
198 :kind=>'file',
189 :action=>file_state
199 :action=>file_state
190 }]
200 }]
191 })
201 })
192 end
202 end
193
203
194 commit_log=String.new
204 commit_log=String.new
195 revision=nil
205 revision=nil
196
206
197 if /^#{ENDLOG}/ =~ line
207 if /^#{ENDLOG}/ =~ line
198 state="entry_start"
208 state="entry_start"
199 end
209 end
200 next
210 next
201 end
211 end
202
212
203 if /^branches: (.+)$/ =~ line
213 if /^branches: (.+)$/ =~ line
204 #TODO: version.branch = $1
214 #TODO: version.branch = $1
205 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
215 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
206 revision = $1
216 revision = $1
207 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
217 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
208 date = Time.parse($1)
218 date = Time.parse($1)
209 author = /author: ([^;]+)/.match(line)[1]
219 author = /author: ([^;]+)/.match(line)[1]
210 file_state = /state: ([^;]+)/.match(line)[1]
220 file_state = /state: ([^;]+)/.match(line)[1]
211 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
221 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
212 # useful for stats or something else
222 # useful for stats or something else
213 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
223 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
214 # unless linechanges.nil?
224 # unless linechanges.nil?
215 # version.line_plus = linechanges[1]
225 # version.line_plus = linechanges[1]
216 # version.line_minus = linechanges[2]
226 # version.line_minus = linechanges[2]
217 # else
227 # else
218 # version.line_plus = 0
228 # version.line_plus = 0
219 # version.line_minus = 0
229 # version.line_minus = 0
220 # end
230 # end
221 else
231 else
222 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
232 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
223 end
233 end
224 end
234 end
225 end
235 end
226 end
236 end
227 end
237 end
228
238
229 def diff(path, identifier_from, identifier_to=nil)
239 def diff(path, identifier_from, identifier_to=nil)
230 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
240 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
231 path_with_project="#{url}#{with_leading_slash(path)}"
241 path_with_project="#{url}#{with_leading_slash(path)}"
232 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
242 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
233 diff = []
243 diff = []
234 shellout(cmd) do |io|
244 shellout(cmd) do |io|
235 io.each_line do |line|
245 io.each_line do |line|
236 diff << line
246 diff << line
237 end
247 end
238 end
248 end
239 return nil if $? && $?.exitstatus != 0
249 return nil if $? && $?.exitstatus != 0
240 diff
250 diff
241 end
251 end
242
252
243 def cat(path, identifier=nil)
253 def cat(path, identifier=nil)
244 identifier = (identifier) ? identifier : "HEAD"
254 identifier = (identifier) ? identifier : "HEAD"
245 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
255 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
246 path_with_project="#{url}#{with_leading_slash(path)}"
256 path_with_project="#{url}#{with_leading_slash(path)}"
247 cmd = "#{CVS_BIN} -d #{shell_quote root_url} co"
257 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} co"
248 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
258 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
249 cmd << " -p #{shell_quote path_with_project}"
259 cmd << " -p #{shell_quote path_with_project}"
250 cat = nil
260 cat = nil
251 shellout(cmd) do |io|
261 shellout(cmd) do |io|
252 io.binmode
262 io.binmode
253 cat = io.read
263 cat = io.read
254 end
264 end
255 return nil if $? && $?.exitstatus != 0
265 return nil if $? && $?.exitstatus != 0
256 cat
266 cat
257 end
267 end
258
268
259 def annotate(path, identifier=nil)
269 def annotate(path, identifier=nil)
260 identifier = (identifier) ? identifier.to_i : "HEAD"
270 identifier = (identifier) ? identifier.to_i : "HEAD"
261 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
271 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
262 path_with_project="#{url}#{with_leading_slash(path)}"
272 path_with_project="#{url}#{with_leading_slash(path)}"
263 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
273 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
264 blame = Annotate.new
274 blame = Annotate.new
265 shellout(cmd) do |io|
275 shellout(cmd) do |io|
266 io.each_line do |line|
276 io.each_line do |line|
267 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
277 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
268 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
278 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
269 end
279 end
270 end
280 end
271 return nil if $? && $?.exitstatus != 0
281 return nil if $? && $?.exitstatus != 0
272 blame
282 blame
273 end
283 end
274
284
275 private
285 private
276
286
277 # Returns the root url without the connexion string
287 # Returns the root url without the connexion string
278 # :pserver:anonymous@foo.bar:/path => /path
288 # :pserver:anonymous@foo.bar:/path => /path
279 # :ext:cvsservername:/path => /path
289 # :ext:cvsservername:/path => /path
280 def root_url_path
290 def root_url_path
281 root_url.to_s.gsub(/^:.+:\d*/, '')
291 root_url.to_s.gsub(/^:.+:\d*/, '')
282 end
292 end
283
293
284 # convert a date/time into the CVS-format
294 # convert a date/time into the CVS-format
285 def time_to_cvstime(time)
295 def time_to_cvstime(time)
286 return nil if time.nil?
296 return nil if time.nil?
287 return Time.now if time == 'HEAD'
297 return Time.now if time == 'HEAD'
288
298
289 unless time.kind_of? Time
299 unless time.kind_of? Time
290 time = Time.parse(time)
300 time = Time.parse(time)
291 end
301 end
292 return time.strftime("%Y-%m-%d %H:%M:%S")
302 return time.strftime("%Y-%m-%d %H:%M:%S")
293 end
303 end
294
304
295 def time_to_cvstime_rlog(time)
305 def time_to_cvstime_rlog(time)
296 return nil if time.nil?
306 return nil if time.nil?
297 t1 = time.clone.localtime
307 t1 = time.clone.localtime
298 return t1.strftime("%Y-%m-%d %H:%M:%S")
308 return t1.strftime("%Y-%m-%d %H:%M:%S")
299 end
309 end
300
310
301 def normalize_cvs_path(path)
311 def normalize_cvs_path(path)
302 normalize_path(path.gsub(/Attic\//,''))
312 normalize_path(path.gsub(/Attic\//,''))
303 end
313 end
304
314
305 def normalize_path(path)
315 def normalize_path(path)
306 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
316 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
307 end
317 end
308 end
318 end
309
319
310 class CvsRevisionHelper
320 class CvsRevisionHelper
311 attr_accessor :complete_rev, :revision, :base, :branchid
321 attr_accessor :complete_rev, :revision, :base, :branchid
312
322
313 def initialize(complete_rev)
323 def initialize(complete_rev)
314 @complete_rev = complete_rev
324 @complete_rev = complete_rev
315 parseRevision()
325 parseRevision()
316 end
326 end
317
327
318 def branchPoint
328 def branchPoint
319 return @base
329 return @base
320 end
330 end
321
331
322 def branchVersion
332 def branchVersion
323 if isBranchRevision
333 if isBranchRevision
324 return @base+"."+@branchid
334 return @base+"."+@branchid
325 end
335 end
326 return @base
336 return @base
327 end
337 end
328
338
329 def isBranchRevision
339 def isBranchRevision
330 !@branchid.nil?
340 !@branchid.nil?
331 end
341 end
332
342
333 def prevRev
343 def prevRev
334 unless @revision==0
344 unless @revision==0
335 return buildRevision(@revision-1)
345 return buildRevision(@revision-1)
336 end
346 end
337 return buildRevision(@revision)
347 return buildRevision(@revision)
338 end
348 end
339
349
340 def is_in_branch_with_symbol(branch_symbol)
350 def is_in_branch_with_symbol(branch_symbol)
341 bpieces=branch_symbol.split(".")
351 bpieces=branch_symbol.split(".")
342 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
352 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
343 return (branchVersion==branch_start)
353 return (branchVersion==branch_start)
344 end
354 end
345
355
346 private
356 private
347 def buildRevision(rev)
357 def buildRevision(rev)
348 if rev== 0
358 if rev== 0
349 @base
359 @base
350 elsif @branchid.nil?
360 elsif @branchid.nil?
351 @base+"."+rev.to_s
361 @base+"."+rev.to_s
352 else
362 else
353 @base+"."+@branchid+"."+rev.to_s
363 @base+"."+@branchid+"."+rev.to_s
354 end
364 end
355 end
365 end
356
366
357 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
367 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
358 def parseRevision()
368 def parseRevision()
359 pieces=@complete_rev.split(".")
369 pieces=@complete_rev.split(".")
360 @revision=pieces.last.to_i
370 @revision=pieces.last.to_i
361 baseSize=1
371 baseSize=1
362 baseSize+=(pieces.size/2)
372 baseSize+=(pieces.size/2)
363 @base=pieces[0..-baseSize].join(".")
373 @base=pieces[0..-baseSize].join(".")
364 if baseSize > 2
374 if baseSize > 2
365 @branchid=pieces[-2]
375 @branchid=pieces[-2]
366 end
376 end
367 end
377 end
368 end
378 end
369 end
379 end
370 end
380 end
371 end
381 end
@@ -1,225 +1,233
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'rexml/document'
19 require 'rexml/document'
20
20
21 module Redmine
21 module Redmine
22 module Scm
22 module Scm
23 module Adapters
23 module Adapters
24 class DarcsAdapter < AbstractAdapter
24 class DarcsAdapter < AbstractAdapter
25 # Darcs executable name
25 # Darcs executable name
26 DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
26 DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
27
27
28 class << self
28 class << self
29 def client_command
30 @@bin ||= DARCS_BIN
31 end
32
33 def sq_bin
34 @@sq_bin ||= shell_quote(DARCS_BIN)
35 end
36
29 def client_version
37 def client_version
30 @@client_version ||= (darcs_binary_version || [])
38 @@client_version ||= (darcs_binary_version || [])
31 end
39 end
32
40
33 def darcs_binary_version
41 def darcs_binary_version
34 darcsversion = darcs_binary_version_from_command_line
42 darcsversion = darcs_binary_version_from_command_line
35 if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
43 if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
36 m[2].scan(%r{\d+}).collect(&:to_i)
44 m[2].scan(%r{\d+}).collect(&:to_i)
37 end
45 end
38 end
46 end
39
47
40 def darcs_binary_version_from_command_line
48 def darcs_binary_version_from_command_line
41 shellout("#{DARCS_BIN} --version") { |io| io.read }.to_s
49 shellout("#{sq_bin} --version") { |io| io.read }.to_s
42 end
50 end
43 end
51 end
44
52
45 def initialize(url, root_url=nil, login=nil, password=nil)
53 def initialize(url, root_url=nil, login=nil, password=nil)
46 @url = url
54 @url = url
47 @root_url = url
55 @root_url = url
48 end
56 end
49
57
50 def supports_cat?
58 def supports_cat?
51 # cat supported in darcs 2.0.0 and higher
59 # cat supported in darcs 2.0.0 and higher
52 self.class.client_version_above?([2, 0, 0])
60 self.class.client_version_above?([2, 0, 0])
53 end
61 end
54
62
55 # Get info about the darcs repository
63 # Get info about the darcs repository
56 def info
64 def info
57 rev = revisions(nil,nil,nil,{:limit => 1})
65 rev = revisions(nil,nil,nil,{:limit => 1})
58 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
66 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
59 end
67 end
60
68
61 # Returns an Entries collection
69 # Returns an Entries collection
62 # or nil if the given path doesn't exist in the repository
70 # or nil if the given path doesn't exist in the repository
63 def entries(path=nil, identifier=nil)
71 def entries(path=nil, identifier=nil)
64 path_prefix = (path.blank? ? '' : "#{path}/")
72 path_prefix = (path.blank? ? '' : "#{path}/")
65 if path.blank?
73 if path.blank?
66 path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
74 path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
67 end
75 end
68 entries = Entries.new
76 entries = Entries.new
69 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --xml-output"
77 cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
70 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
78 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
71 cmd << " #{shell_quote path}"
79 cmd << " #{shell_quote path}"
72 shellout(cmd) do |io|
80 shellout(cmd) do |io|
73 begin
81 begin
74 doc = REXML::Document.new(io)
82 doc = REXML::Document.new(io)
75 if doc.root.name == 'directory'
83 if doc.root.name == 'directory'
76 doc.elements.each('directory/*') do |element|
84 doc.elements.each('directory/*') do |element|
77 next unless ['file', 'directory'].include? element.name
85 next unless ['file', 'directory'].include? element.name
78 entries << entry_from_xml(element, path_prefix)
86 entries << entry_from_xml(element, path_prefix)
79 end
87 end
80 elsif doc.root.name == 'file'
88 elsif doc.root.name == 'file'
81 entries << entry_from_xml(doc.root, path_prefix)
89 entries << entry_from_xml(doc.root, path_prefix)
82 end
90 end
83 rescue
91 rescue
84 end
92 end
85 end
93 end
86 return nil if $? && $?.exitstatus != 0
94 return nil if $? && $?.exitstatus != 0
87 entries.compact.sort_by_name
95 entries.compact.sort_by_name
88 end
96 end
89
97
90 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
98 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
91 path = '.' if path.blank?
99 path = '.' if path.blank?
92 revisions = Revisions.new
100 revisions = Revisions.new
93 cmd = "#{DARCS_BIN} changes --repodir #{shell_quote @url} --xml-output"
101 cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
94 cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
102 cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
95 cmd << " --last #{options[:limit].to_i}" if options[:limit]
103 cmd << " --last #{options[:limit].to_i}" if options[:limit]
96 shellout(cmd) do |io|
104 shellout(cmd) do |io|
97 begin
105 begin
98 doc = REXML::Document.new(io)
106 doc = REXML::Document.new(io)
99 doc.elements.each("changelog/patch") do |patch|
107 doc.elements.each("changelog/patch") do |patch|
100 message = patch.elements['name'].text
108 message = patch.elements['name'].text
101 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
109 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
102 revisions << Revision.new({:identifier => nil,
110 revisions << Revision.new({:identifier => nil,
103 :author => patch.attributes['author'],
111 :author => patch.attributes['author'],
104 :scmid => patch.attributes['hash'],
112 :scmid => patch.attributes['hash'],
105 :time => Time.parse(patch.attributes['local_date']),
113 :time => Time.parse(patch.attributes['local_date']),
106 :message => message,
114 :message => message,
107 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
115 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
108 })
116 })
109 end
117 end
110 rescue
118 rescue
111 end
119 end
112 end
120 end
113 return nil if $? && $?.exitstatus != 0
121 return nil if $? && $?.exitstatus != 0
114 revisions
122 revisions
115 end
123 end
116
124
117 def diff(path, identifier_from, identifier_to=nil)
125 def diff(path, identifier_from, identifier_to=nil)
118 path = '*' if path.blank?
126 path = '*' if path.blank?
119 cmd = "#{DARCS_BIN} diff --repodir #{shell_quote @url}"
127 cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
120 if identifier_to.nil?
128 if identifier_to.nil?
121 cmd << " --match #{shell_quote("hash #{identifier_from}")}"
129 cmd << " --match #{shell_quote("hash #{identifier_from}")}"
122 else
130 else
123 cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
131 cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
124 cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
132 cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
125 end
133 end
126 cmd << " -u #{shell_quote path}"
134 cmd << " -u #{shell_quote path}"
127 diff = []
135 diff = []
128 shellout(cmd) do |io|
136 shellout(cmd) do |io|
129 io.each_line do |line|
137 io.each_line do |line|
130 diff << line
138 diff << line
131 end
139 end
132 end
140 end
133 return nil if $? && $?.exitstatus != 0
141 return nil if $? && $?.exitstatus != 0
134 diff
142 diff
135 end
143 end
136
144
137 def cat(path, identifier=nil)
145 def cat(path, identifier=nil)
138 cmd = "#{DARCS_BIN} show content --repodir #{shell_quote @url}"
146 cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
139 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
147 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
140 cmd << " #{shell_quote path}"
148 cmd << " #{shell_quote path}"
141 cat = nil
149 cat = nil
142 shellout(cmd) do |io|
150 shellout(cmd) do |io|
143 io.binmode
151 io.binmode
144 cat = io.read
152 cat = io.read
145 end
153 end
146 return nil if $? && $?.exitstatus != 0
154 return nil if $? && $?.exitstatus != 0
147 cat
155 cat
148 end
156 end
149
157
150 private
158 private
151
159
152 # Returns an Entry from the given XML element
160 # Returns an Entry from the given XML element
153 # or nil if the entry was deleted
161 # or nil if the entry was deleted
154 def entry_from_xml(element, path_prefix)
162 def entry_from_xml(element, path_prefix)
155 modified_element = element.elements['modified']
163 modified_element = element.elements['modified']
156 if modified_element.elements['modified_how'].text.match(/removed/)
164 if modified_element.elements['modified_how'].text.match(/removed/)
157 return nil
165 return nil
158 end
166 end
159
167
160 Entry.new({:name => element.attributes['name'],
168 Entry.new({:name => element.attributes['name'],
161 :path => path_prefix + element.attributes['name'],
169 :path => path_prefix + element.attributes['name'],
162 :kind => element.name == 'file' ? 'file' : 'dir',
170 :kind => element.name == 'file' ? 'file' : 'dir',
163 :size => nil,
171 :size => nil,
164 :lastrev => Revision.new({
172 :lastrev => Revision.new({
165 :identifier => nil,
173 :identifier => nil,
166 :scmid => modified_element.elements['patch'].attributes['hash']
174 :scmid => modified_element.elements['patch'].attributes['hash']
167 })
175 })
168 })
176 })
169 end
177 end
170
178
171 def get_paths_for_patch(hash)
179 def get_paths_for_patch(hash)
172 paths = get_paths_for_patch_raw(hash)
180 paths = get_paths_for_patch_raw(hash)
173 if self.class.client_version_above?([2, 4])
181 if self.class.client_version_above?([2, 4])
174 orig_paths = paths
182 orig_paths = paths
175 paths = []
183 paths = []
176 add_paths = []
184 add_paths = []
177 add_paths_name = []
185 add_paths_name = []
178 mod_paths = []
186 mod_paths = []
179 other_paths = []
187 other_paths = []
180 orig_paths.each do |path|
188 orig_paths.each do |path|
181 if path[:action] == 'A'
189 if path[:action] == 'A'
182 add_paths << path
190 add_paths << path
183 add_paths_name << path[:path]
191 add_paths_name << path[:path]
184 elsif path[:action] == 'M'
192 elsif path[:action] == 'M'
185 mod_paths << path
193 mod_paths << path
186 else
194 else
187 other_paths << path
195 other_paths << path
188 end
196 end
189 end
197 end
190 add_paths_name.each do |add_path|
198 add_paths_name.each do |add_path|
191 mod_paths.delete_if { |m| m[:path] == add_path }
199 mod_paths.delete_if { |m| m[:path] == add_path }
192 end
200 end
193 paths.concat add_paths
201 paths.concat add_paths
194 paths.concat mod_paths
202 paths.concat mod_paths
195 paths.concat other_paths
203 paths.concat other_paths
196 end
204 end
197 paths
205 paths
198 end
206 end
199
207
200 # Retrieve changed paths for a single patch
208 # Retrieve changed paths for a single patch
201 def get_paths_for_patch_raw(hash)
209 def get_paths_for_patch_raw(hash)
202 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --summary --xml-output"
210 cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
203 cmd << " --match #{shell_quote("hash #{hash}")} "
211 cmd << " --match #{shell_quote("hash #{hash}")} "
204 paths = []
212 paths = []
205 shellout(cmd) do |io|
213 shellout(cmd) do |io|
206 begin
214 begin
207 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
215 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
208 # A root element is added so that REXML doesn't raise an error
216 # A root element is added so that REXML doesn't raise an error
209 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
217 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
210 doc.elements.each('fake_root/summary/*') do |modif|
218 doc.elements.each('fake_root/summary/*') do |modif|
211 paths << {:action => modif.name[0,1].upcase,
219 paths << {:action => modif.name[0,1].upcase,
212 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
220 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
213 }
221 }
214 end
222 end
215 rescue
223 rescue
216 end
224 end
217 end
225 end
218 paths
226 paths
219 rescue CommandFailed
227 rescue CommandFailed
220 paths
228 paths
221 end
229 end
222 end
230 end
223 end
231 end
224 end
232 end
225 end
233 end
@@ -1,93 +1,98
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # FileSystem adapter
4 # FileSystem adapter
5 # File written by Paul Rivier, at Demotera.
5 # File written by Paul Rivier, at Demotera.
6 #
6 #
7 # This program is free software; you can redistribute it and/or
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either version 2
9 # as published by the Free Software Foundation; either version 2
10 # of the License, or (at your option) any later version.
10 # of the License, or (at your option) any later version.
11 #
11 #
12 # This program is distributed in the hope that it will be useful,
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
15 # GNU General Public License for more details.
16 #
16 #
17 # You should have received a copy of the GNU General Public License
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
20
21 require 'redmine/scm/adapters/abstract_adapter'
21 require 'redmine/scm/adapters/abstract_adapter'
22 require 'find'
22 require 'find'
23
23
24 module Redmine
24 module Redmine
25 module Scm
25 module Scm
26 module Adapters
26 module Adapters
27 class FilesystemAdapter < AbstractAdapter
27 class FilesystemAdapter < AbstractAdapter
28
28
29 class << self
30 def client_available
31 true
32 end
33 end
29
34
30 def initialize(url, root_url=nil, login=nil, password=nil)
35 def initialize(url, root_url=nil, login=nil, password=nil)
31 @url = with_trailling_slash(url)
36 @url = with_trailling_slash(url)
32 end
37 end
33
38
34 def format_path_ends(path, leading=true, trailling=true)
39 def format_path_ends(path, leading=true, trailling=true)
35 path = leading ? with_leading_slash(path) :
40 path = leading ? with_leading_slash(path) :
36 without_leading_slash(path)
41 without_leading_slash(path)
37 trailling ? with_trailling_slash(path) :
42 trailling ? with_trailling_slash(path) :
38 without_trailling_slash(path)
43 without_trailling_slash(path)
39 end
44 end
40
45
41 def info
46 def info
42 info = Info.new({:root_url => target(),
47 info = Info.new({:root_url => target(),
43 :lastrev => nil
48 :lastrev => nil
44 })
49 })
45 info
50 info
46 rescue CommandFailed
51 rescue CommandFailed
47 return nil
52 return nil
48 end
53 end
49
54
50 def entries(path="", identifier=nil)
55 def entries(path="", identifier=nil)
51 entries = Entries.new
56 entries = Entries.new
52 Dir.new(target(path)).each do |e|
57 Dir.new(target(path)).each do |e|
53 relative_path = format_path_ends((format_path_ends(path,
58 relative_path = format_path_ends((format_path_ends(path,
54 false,
59 false,
55 true) + e),
60 true) + e),
56 false,false)
61 false,false)
57 target = target(relative_path)
62 target = target(relative_path)
58 entries <<
63 entries <<
59 Entry.new({ :name => File.basename(e),
64 Entry.new({ :name => File.basename(e),
60 # below : list unreadable files, but dont link them.
65 # below : list unreadable files, but dont link them.
61 :path => File.readable?(target) ? relative_path : "",
66 :path => File.readable?(target) ? relative_path : "",
62 :kind => (File.directory?(target) ? 'dir' : 'file'),
67 :kind => (File.directory?(target) ? 'dir' : 'file'),
63 :size => (File.directory?(target) ? nil : [File.size(target)].pack('l').unpack('L').first),
68 :size => (File.directory?(target) ? nil : [File.size(target)].pack('l').unpack('L').first),
64 :lastrev =>
69 :lastrev =>
65 Revision.new({:time => (File.mtime(target)).localtime,
70 Revision.new({:time => (File.mtime(target)).localtime,
66 })
71 })
67 }) if File.exist?(target) and # paranoid test
72 }) if File.exist?(target) and # paranoid test
68 %w{file directory}.include?(File.ftype(target)) and # avoid special types
73 %w{file directory}.include?(File.ftype(target)) and # avoid special types
69 not File.basename(e).match(/^\.+$/) # avoid . and ..
74 not File.basename(e).match(/^\.+$/) # avoid . and ..
70 end
75 end
71 entries.sort_by_name
76 entries.sort_by_name
72 end
77 end
73
78
74 def cat(path, identifier=nil)
79 def cat(path, identifier=nil)
75 File.new(target(path), "rb").read
80 File.new(target(path), "rb").read
76 end
81 end
77
82
78 private
83 private
79
84
80 # AbstractAdapter::target is implicitly made to quote paths.
85 # AbstractAdapter::target is implicitly made to quote paths.
81 # Here we do not shell-out, so we do not want quotes.
86 # Here we do not shell-out, so we do not want quotes.
82 def target(path=nil)
87 def target(path=nil)
83 #Prevent the use of ..
88 #Prevent the use of ..
84 if path and !path.match(/(^|\/)\.\.(\/|$)/)
89 if path and !path.match(/(^|\/)\.\.(\/|$)/)
85 return "#{self.url}#{without_leading_slash(path)}"
90 return "#{self.url}#{without_leading_slash(path)}"
86 end
91 end
87 return self.url
92 return self.url
88 end
93 end
89
94
90 end
95 end
91 end
96 end
92 end
97 end
93 end
98 end
@@ -1,277 +1,291
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class GitAdapter < AbstractAdapter
23 class GitAdapter < AbstractAdapter
24 # Git executable name
24 # Git executable name
25 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
25 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
26
26
27 class << self
28 def client_command
29 @@bin ||= GIT_BIN
30 end
31
32 def sq_bin
33 @@sq_bin ||= shell_quote(GIT_BIN)
34 end
35
36 def client_available
37 !client_version.empty?
38 end
39 end
40
27 def info
41 def info
28 begin
42 begin
29 Info.new(:root_url => url, :lastrev => lastrev('',nil))
43 Info.new(:root_url => url, :lastrev => lastrev('',nil))
30 rescue
44 rescue
31 nil
45 nil
32 end
46 end
33 end
47 end
34
48
35 def branches
49 def branches
36 return @branches if @branches
50 return @branches if @branches
37 @branches = []
51 @branches = []
38 cmd = "#{GIT_BIN} --git-dir #{target('')} branch --no-color"
52 cmd = "#{self.class.sq_bin} --git-dir #{target('')} branch --no-color"
39 shellout(cmd) do |io|
53 shellout(cmd) do |io|
40 io.each_line do |line|
54 io.each_line do |line|
41 @branches << line.match('\s*\*?\s*(.*)$')[1]
55 @branches << line.match('\s*\*?\s*(.*)$')[1]
42 end
56 end
43 end
57 end
44 @branches.sort!
58 @branches.sort!
45 end
59 end
46
60
47 def tags
61 def tags
48 return @tags if @tags
62 return @tags if @tags
49 cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
63 cmd = "#{self.class.sq_bin} --git-dir #{target('')} tag"
50 shellout(cmd) do |io|
64 shellout(cmd) do |io|
51 @tags = io.readlines.sort!.map{|t| t.strip}
65 @tags = io.readlines.sort!.map{|t| t.strip}
52 end
66 end
53 end
67 end
54
68
55 def default_branch
69 def default_branch
56 branches.include?('master') ? 'master' : branches.first
70 branches.include?('master') ? 'master' : branches.first
57 end
71 end
58
72
59 def entries(path=nil, identifier=nil)
73 def entries(path=nil, identifier=nil)
60 path ||= ''
74 path ||= ''
61 entries = Entries.new
75 entries = Entries.new
62 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
76 cmd = "#{self.class.sq_bin} --git-dir #{target('')} ls-tree -l "
63 cmd << shell_quote("HEAD:" + path) if identifier.nil?
77 cmd << shell_quote("HEAD:" + path) if identifier.nil?
64 cmd << shell_quote(identifier + ":" + path) if identifier
78 cmd << shell_quote(identifier + ":" + path) if identifier
65 shellout(cmd) do |io|
79 shellout(cmd) do |io|
66 io.each_line do |line|
80 io.each_line do |line|
67 e = line.chomp.to_s
81 e = line.chomp.to_s
68 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
82 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
69 type = $1
83 type = $1
70 sha = $2
84 sha = $2
71 size = $3
85 size = $3
72 name = $4
86 name = $4
73 full_path = path.empty? ? name : "#{path}/#{name}"
87 full_path = path.empty? ? name : "#{path}/#{name}"
74 entries << Entry.new({:name => name,
88 entries << Entry.new({:name => name,
75 :path => full_path,
89 :path => full_path,
76 :kind => (type == "tree") ? 'dir' : 'file',
90 :kind => (type == "tree") ? 'dir' : 'file',
77 :size => (type == "tree") ? nil : size,
91 :size => (type == "tree") ? nil : size,
78 :lastrev => lastrev(full_path,identifier)
92 :lastrev => lastrev(full_path,identifier)
79 }) unless entries.detect{|entry| entry.name == name}
93 }) unless entries.detect{|entry| entry.name == name}
80 end
94 end
81 end
95 end
82 end
96 end
83 return nil if $? && $?.exitstatus != 0
97 return nil if $? && $?.exitstatus != 0
84 entries.sort_by_name
98 entries.sort_by_name
85 end
99 end
86
100
87 def lastrev(path,rev)
101 def lastrev(path,rev)
88 return nil if path.nil?
102 return nil if path.nil?
89 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
103 cmd = "#{self.class.sq_bin} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
90 cmd << " #{shell_quote rev} " if rev
104 cmd << " #{shell_quote rev} " if rev
91 cmd << "-- #{shell_quote path} " unless path.empty?
105 cmd << "-- #{shell_quote path} " unless path.empty?
92 lines = []
106 lines = []
93 shellout(cmd) { |io| lines = io.readlines }
107 shellout(cmd) { |io| lines = io.readlines }
94 return nil if $? && $?.exitstatus != 0
108 return nil if $? && $?.exitstatus != 0
95 begin
109 begin
96 id = lines[0].split[1]
110 id = lines[0].split[1]
97 author = lines[1].match('Author:\s+(.*)$')[1]
111 author = lines[1].match('Author:\s+(.*)$')[1]
98 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1]).localtime
112 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1]).localtime
99
113
100 Revision.new({
114 Revision.new({
101 :identifier => id,
115 :identifier => id,
102 :scmid => id,
116 :scmid => id,
103 :author => author,
117 :author => author,
104 :time => time,
118 :time => time,
105 :message => nil,
119 :message => nil,
106 :paths => nil
120 :paths => nil
107 })
121 })
108 rescue NoMethodError => e
122 rescue NoMethodError => e
109 logger.error("The revision '#{path}' has a wrong format")
123 logger.error("The revision '#{path}' has a wrong format")
110 return nil
124 return nil
111 end
125 end
112 end
126 end
113
127
114 def revisions(path, identifier_from, identifier_to, options={})
128 def revisions(path, identifier_from, identifier_to, options={})
115 revisions = Revisions.new
129 revisions = Revisions.new
116
130
117 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --raw --date=iso --pretty=fuller "
131 cmd = "#{self.class.sq_bin} --git-dir #{target('')} log --no-color --raw --date=iso --pretty=fuller "
118 cmd << " --reverse " if options[:reverse]
132 cmd << " --reverse " if options[:reverse]
119 cmd << " --all " if options[:all]
133 cmd << " --all " if options[:all]
120 cmd << " -n #{options[:limit].to_i} " if options[:limit]
134 cmd << " -n #{options[:limit].to_i} " if options[:limit]
121 cmd << "#{shell_quote(identifier_from + '..')}" if identifier_from
135 cmd << "#{shell_quote(identifier_from + '..')}" if identifier_from
122 cmd << "#{shell_quote identifier_to}" if identifier_to
136 cmd << "#{shell_quote identifier_to}" if identifier_to
123 cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since]
137 cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since]
124 cmd << " -- #{shell_quote path}" if path && !path.empty?
138 cmd << " -- #{shell_quote path}" if path && !path.empty?
125
139
126 shellout(cmd) do |io|
140 shellout(cmd) do |io|
127 files=[]
141 files=[]
128 changeset = {}
142 changeset = {}
129 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
143 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
130 revno = 1
144 revno = 1
131
145
132 io.each_line do |line|
146 io.each_line do |line|
133 if line =~ /^commit ([0-9a-f]{40})$/
147 if line =~ /^commit ([0-9a-f]{40})$/
134 key = "commit"
148 key = "commit"
135 value = $1
149 value = $1
136 if (parsing_descr == 1 || parsing_descr == 2)
150 if (parsing_descr == 1 || parsing_descr == 2)
137 parsing_descr = 0
151 parsing_descr = 0
138 revision = Revision.new({
152 revision = Revision.new({
139 :identifier => changeset[:commit],
153 :identifier => changeset[:commit],
140 :scmid => changeset[:commit],
154 :scmid => changeset[:commit],
141 :author => changeset[:author],
155 :author => changeset[:author],
142 :time => Time.parse(changeset[:date]),
156 :time => Time.parse(changeset[:date]),
143 :message => changeset[:description],
157 :message => changeset[:description],
144 :paths => files
158 :paths => files
145 })
159 })
146 if block_given?
160 if block_given?
147 yield revision
161 yield revision
148 else
162 else
149 revisions << revision
163 revisions << revision
150 end
164 end
151 changeset = {}
165 changeset = {}
152 files = []
166 files = []
153 revno = revno + 1
167 revno = revno + 1
154 end
168 end
155 changeset[:commit] = $1
169 changeset[:commit] = $1
156 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
170 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
157 key = $1
171 key = $1
158 value = $2
172 value = $2
159 if key == "Author"
173 if key == "Author"
160 changeset[:author] = value
174 changeset[:author] = value
161 elsif key == "CommitDate"
175 elsif key == "CommitDate"
162 changeset[:date] = value
176 changeset[:date] = value
163 end
177 end
164 elsif (parsing_descr == 0) && line.chomp.to_s == ""
178 elsif (parsing_descr == 0) && line.chomp.to_s == ""
165 parsing_descr = 1
179 parsing_descr = 1
166 changeset[:description] = ""
180 changeset[:description] = ""
167 elsif (parsing_descr == 1 || parsing_descr == 2) \
181 elsif (parsing_descr == 1 || parsing_descr == 2) \
168 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
182 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
169 parsing_descr = 2
183 parsing_descr = 2
170 fileaction = $1
184 fileaction = $1
171 filepath = $2
185 filepath = $2
172 files << {:action => fileaction, :path => filepath}
186 files << {:action => fileaction, :path => filepath}
173 elsif (parsing_descr == 1 || parsing_descr == 2) \
187 elsif (parsing_descr == 1 || parsing_descr == 2) \
174 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
188 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
175 parsing_descr = 2
189 parsing_descr = 2
176 fileaction = $1
190 fileaction = $1
177 filepath = $3
191 filepath = $3
178 files << {:action => fileaction, :path => filepath}
192 files << {:action => fileaction, :path => filepath}
179 elsif (parsing_descr == 1) && line.chomp.to_s == ""
193 elsif (parsing_descr == 1) && line.chomp.to_s == ""
180 parsing_descr = 2
194 parsing_descr = 2
181 elsif (parsing_descr == 1)
195 elsif (parsing_descr == 1)
182 changeset[:description] << line[4..-1]
196 changeset[:description] << line[4..-1]
183 end
197 end
184 end
198 end
185
199
186 if changeset[:commit]
200 if changeset[:commit]
187 revision = Revision.new({
201 revision = Revision.new({
188 :identifier => changeset[:commit],
202 :identifier => changeset[:commit],
189 :scmid => changeset[:commit],
203 :scmid => changeset[:commit],
190 :author => changeset[:author],
204 :author => changeset[:author],
191 :time => Time.parse(changeset[:date]),
205 :time => Time.parse(changeset[:date]),
192 :message => changeset[:description],
206 :message => changeset[:description],
193 :paths => files
207 :paths => files
194 })
208 })
195
209
196 if block_given?
210 if block_given?
197 yield revision
211 yield revision
198 else
212 else
199 revisions << revision
213 revisions << revision
200 end
214 end
201 end
215 end
202 end
216 end
203
217
204 return nil if $? && $?.exitstatus != 0
218 return nil if $? && $?.exitstatus != 0
205 revisions
219 revisions
206 end
220 end
207
221
208 def diff(path, identifier_from, identifier_to=nil)
222 def diff(path, identifier_from, identifier_to=nil)
209 path ||= ''
223 path ||= ''
210
224
211 if identifier_to
225 if identifier_to
212 cmd = "#{GIT_BIN} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
226 cmd = "#{self.class.sq_bin} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
213 else
227 else
214 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
228 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
215 end
229 end
216
230
217 cmd << " -- #{shell_quote path}" unless path.empty?
231 cmd << " -- #{shell_quote path}" unless path.empty?
218 diff = []
232 diff = []
219 shellout(cmd) do |io|
233 shellout(cmd) do |io|
220 io.each_line do |line|
234 io.each_line do |line|
221 diff << line
235 diff << line
222 end
236 end
223 end
237 end
224 return nil if $? && $?.exitstatus != 0
238 return nil if $? && $?.exitstatus != 0
225 diff
239 diff
226 end
240 end
227
241
228 def annotate(path, identifier=nil)
242 def annotate(path, identifier=nil)
229 identifier = 'HEAD' if identifier.blank?
243 identifier = 'HEAD' if identifier.blank?
230 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
244 cmd = "#{self.class.sq_bin} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
231 blame = Annotate.new
245 blame = Annotate.new
232 content = nil
246 content = nil
233 shellout(cmd) { |io| io.binmode; content = io.read }
247 shellout(cmd) { |io| io.binmode; content = io.read }
234 return nil if $? && $?.exitstatus != 0
248 return nil if $? && $?.exitstatus != 0
235 # git annotates binary files
249 # git annotates binary files
236 return nil if content.is_binary_data?
250 return nil if content.is_binary_data?
237 identifier = ''
251 identifier = ''
238 # git shows commit author on the first occurrence only
252 # git shows commit author on the first occurrence only
239 authors_by_commit = {}
253 authors_by_commit = {}
240 content.split("\n").each do |line|
254 content.split("\n").each do |line|
241 if line =~ /^([0-9a-f]{39,40})\s.*/
255 if line =~ /^([0-9a-f]{39,40})\s.*/
242 identifier = $1
256 identifier = $1
243 elsif line =~ /^author (.+)/
257 elsif line =~ /^author (.+)/
244 authors_by_commit[identifier] = $1.strip
258 authors_by_commit[identifier] = $1.strip
245 elsif line =~ /^\t(.*)/
259 elsif line =~ /^\t(.*)/
246 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
260 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
247 identifier = ''
261 identifier = ''
248 author = ''
262 author = ''
249 end
263 end
250 end
264 end
251 blame
265 blame
252 end
266 end
253
267
254 def cat(path, identifier=nil)
268 def cat(path, identifier=nil)
255 if identifier.nil?
269 if identifier.nil?
256 identifier = 'HEAD'
270 identifier = 'HEAD'
257 end
271 end
258 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
272 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
259 cat = nil
273 cat = nil
260 shellout(cmd) do |io|
274 shellout(cmd) do |io|
261 io.binmode
275 io.binmode
262 cat = io.read
276 cat = io.read
263 end
277 end
264 return nil if $? && $?.exitstatus != 0
278 return nil if $? && $?.exitstatus != 0
265 cat
279 cat
266 end
280 end
267
281
268 class Revision < Redmine::Scm::Adapters::Revision
282 class Revision < Redmine::Scm::Adapters::Revision
269 # Returns the readable identifier
283 # Returns the readable identifier
270 def format_identifier
284 def format_identifier
271 identifier[0,8]
285 identifier[0,8]
272 end
286 end
273 end
287 end
274 end
288 end
275 end
289 end
276 end
290 end
277 end
291 end
@@ -1,225 +1,237
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'cgi'
19 require 'cgi'
20
20
21 module Redmine
21 module Redmine
22 module Scm
22 module Scm
23 module Adapters
23 module Adapters
24 class MercurialAdapter < AbstractAdapter
24 class MercurialAdapter < AbstractAdapter
25
25
26 # Mercurial executable name
26 # Mercurial executable name
27 HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
27 HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
28 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
29 TEMPLATE_NAME = "hg-template"
29 TEMPLATE_NAME = "hg-template"
30 TEMPLATE_EXTENSION = "tmpl"
30 TEMPLATE_EXTENSION = "tmpl"
31
31
32 class << self
32 class << self
33 def client_command
34 @@bin ||= HG_BIN
35 end
36
37 def sq_bin
38 @@sq_bin ||= shell_quote(HG_BIN)
39 end
40
33 def client_version
41 def client_version
34 @@client_version ||= (hgversion || [])
42 @@client_version ||= (hgversion || [])
35 end
43 end
36
44
45 def client_available
46 !client_version.empty?
47 end
48
37 def hgversion
49 def hgversion
38 # The hg version is expressed either as a
50 # The hg version is expressed either as a
39 # release number (eg 0.9.5 or 1.0) or as a revision
51 # release number (eg 0.9.5 or 1.0) or as a revision
40 # id composed of 12 hexa characters.
52 # id composed of 12 hexa characters.
41 theversion = hgversion_from_command_line
53 theversion = hgversion_from_command_line
42 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
54 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
43 m[2].scan(%r{\d+}).collect(&:to_i)
55 m[2].scan(%r{\d+}).collect(&:to_i)
44 end
56 end
45 end
57 end
46
58
47 def hgversion_from_command_line
59 def hgversion_from_command_line
48 shellout("#{HG_BIN} --version") { |io| io.read }.to_s
60 shellout("#{sq_bin} --version") { |io| io.read }.to_s
49 end
61 end
50
62
51 def template_path
63 def template_path
52 @@template_path ||= template_path_for(client_version)
64 @@template_path ||= template_path_for(client_version)
53 end
65 end
54
66
55 def template_path_for(version)
67 def template_path_for(version)
56 if ((version <=> [0,9,5]) > 0) || version.empty?
68 if ((version <=> [0,9,5]) > 0) || version.empty?
57 ver = "1.0"
69 ver = "1.0"
58 else
70 else
59 ver = "0.9.5"
71 ver = "0.9.5"
60 end
72 end
61 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
73 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
62 end
74 end
63 end
75 end
64
76
65 def info
77 def info
66 cmd = "#{HG_BIN} -R #{target('')} root"
78 cmd = "#{self.class.sq_bin} -R #{target('')} root"
67 root_url = nil
79 root_url = nil
68 shellout(cmd) do |io|
80 shellout(cmd) do |io|
69 root_url = io.read
81 root_url = io.read
70 end
82 end
71 return nil if $? && $?.exitstatus != 0
83 return nil if $? && $?.exitstatus != 0
72 info = Info.new({:root_url => root_url.chomp,
84 info = Info.new({:root_url => root_url.chomp,
73 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
85 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
74 })
86 })
75 info
87 info
76 rescue CommandFailed
88 rescue CommandFailed
77 return nil
89 return nil
78 end
90 end
79
91
80 def entries(path=nil, identifier=nil)
92 def entries(path=nil, identifier=nil)
81 path ||= ''
93 path ||= ''
82 entries = Entries.new
94 entries = Entries.new
83 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
95 cmd = "#{self.class.sq_bin} -R #{target('')} --cwd #{target('')} locate"
84 cmd << " -r #{hgrev(identifier)}"
96 cmd << " -r #{hgrev(identifier)}"
85 cmd << " " + shell_quote("path:#{path}") unless path.empty?
97 cmd << " " + shell_quote("path:#{path}") unless path.empty?
86 shellout(cmd) do |io|
98 shellout(cmd) do |io|
87 io.each_line do |line|
99 io.each_line do |line|
88 # HG uses antislashs as separator on Windows
100 # HG uses antislashs as separator on Windows
89 line = line.gsub(/\\/, "/")
101 line = line.gsub(/\\/, "/")
90 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
102 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
91 e ||= line
103 e ||= line
92 e = e.chomp.split(%r{[\/\\]})
104 e = e.chomp.split(%r{[\/\\]})
93 entries << Entry.new({:name => e.first,
105 entries << Entry.new({:name => e.first,
94 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
106 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
95 :kind => (e.size > 1 ? 'dir' : 'file'),
107 :kind => (e.size > 1 ? 'dir' : 'file'),
96 :lastrev => Revision.new
108 :lastrev => Revision.new
97 }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
109 }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
98 end
110 end
99 end
111 end
100 end
112 end
101 return nil if $? && $?.exitstatus != 0
113 return nil if $? && $?.exitstatus != 0
102 entries.sort_by_name
114 entries.sort_by_name
103 end
115 end
104
116
105 # Fetch the revisions by using a template file that
117 # Fetch the revisions by using a template file that
106 # makes Mercurial produce a xml output.
118 # makes Mercurial produce a xml output.
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
119 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
108 revisions = Revisions.new
120 revisions = Revisions.new
109 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
121 cmd = "#{self.class.sq_bin} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
110 if identifier_from && identifier_to
122 if identifier_from && identifier_to
111 cmd << " -r #{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
123 cmd << " -r #{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
112 elsif identifier_from
124 elsif identifier_from
113 cmd << " -r #{hgrev(identifier_from)}:"
125 cmd << " -r #{hgrev(identifier_from)}:"
114 end
126 end
115 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
127 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
116 cmd << " #{shell_quote path}" unless path.blank?
128 cmd << " #{shell_quote path}" unless path.blank?
117 shellout(cmd) do |io|
129 shellout(cmd) do |io|
118 begin
130 begin
119 # HG doesn't close the XML Document...
131 # HG doesn't close the XML Document...
120 doc = REXML::Document.new(io.read << "</log>")
132 doc = REXML::Document.new(io.read << "</log>")
121 doc.elements.each("log/logentry") do |logentry|
133 doc.elements.each("log/logentry") do |logentry|
122 paths = []
134 paths = []
123 copies = logentry.get_elements('paths/path-copied')
135 copies = logentry.get_elements('paths/path-copied')
124 logentry.elements.each("paths/path") do |path|
136 logentry.elements.each("paths/path") do |path|
125 # Detect if the added file is a copy
137 # Detect if the added file is a copy
126 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
138 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
127 from_path = c.attributes['copyfrom-path']
139 from_path = c.attributes['copyfrom-path']
128 from_rev = logentry.attributes['revision']
140 from_rev = logentry.attributes['revision']
129 end
141 end
130 paths << {:action => path.attributes['action'],
142 paths << {:action => path.attributes['action'],
131 :path => "/#{CGI.unescape(path.text)}",
143 :path => "/#{CGI.unescape(path.text)}",
132 :from_path => from_path ? "/#{CGI.unescape(from_path)}" : nil,
144 :from_path => from_path ? "/#{CGI.unescape(from_path)}" : nil,
133 :from_revision => from_rev ? from_rev : nil
145 :from_revision => from_rev ? from_rev : nil
134 }
146 }
135 end
147 end
136 paths.sort! { |x,y| x[:path] <=> y[:path] }
148 paths.sort! { |x,y| x[:path] <=> y[:path] }
137
149
138 revisions << Revision.new({:identifier => logentry.attributes['revision'],
150 revisions << Revision.new({:identifier => logentry.attributes['revision'],
139 :scmid => logentry.attributes['node'],
151 :scmid => logentry.attributes['node'],
140 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
152 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
141 :time => Time.parse(logentry.elements['date'].text).localtime,
153 :time => Time.parse(logentry.elements['date'].text).localtime,
142 :message => logentry.elements['msg'].text,
154 :message => logentry.elements['msg'].text,
143 :paths => paths
155 :paths => paths
144 })
156 })
145 end
157 end
146 rescue
158 rescue
147 logger.debug($!)
159 logger.debug($!)
148 end
160 end
149 end
161 end
150 return nil if $? && $?.exitstatus != 0
162 return nil if $? && $?.exitstatus != 0
151 revisions
163 revisions
152 end
164 end
153
165
154 def diff(path, identifier_from, identifier_to=nil)
166 def diff(path, identifier_from, identifier_to=nil)
155 path ||= ''
167 path ||= ''
156 diff_args = ''
168 diff_args = ''
157 diff = []
169 diff = []
158 if identifier_to
170 if identifier_to
159 diff_args = "-r #{hgrev(identifier_to)} -r #{hgrev(identifier_from)}"
171 diff_args = "-r #{hgrev(identifier_to)} -r #{hgrev(identifier_from)}"
160 else
172 else
161 if self.class.client_version_above?([1, 2])
173 if self.class.client_version_above?([1, 2])
162 diff_args = "-c #{hgrev(identifier_from)}"
174 diff_args = "-c #{hgrev(identifier_from)}"
163 else
175 else
164 return []
176 return []
165 end
177 end
166 end
178 end
167 cmd = "#{HG_BIN} -R #{target('')} --config diff.git=false diff --nodates #{diff_args}"
179 cmd = "#{self.class.sq_bin} -R #{target('')} --config diff.git=false diff --nodates #{diff_args}"
168 cmd << " -I #{target(path)}" unless path.empty?
180 cmd << " -I #{target(path)}" unless path.empty?
169 shellout(cmd) do |io|
181 shellout(cmd) do |io|
170 io.each_line do |line|
182 io.each_line do |line|
171 diff << line
183 diff << line
172 end
184 end
173 end
185 end
174 return nil if $? && $?.exitstatus != 0
186 return nil if $? && $?.exitstatus != 0
175 diff
187 diff
176 end
188 end
177
189
178 def cat(path, identifier=nil)
190 def cat(path, identifier=nil)
179 cmd = "#{HG_BIN} -R #{target('')} cat"
191 cmd = "#{self.class.sq_bin} -R #{target('')} cat"
180 cmd << " -r #{hgrev(identifier)}"
192 cmd << " -r #{hgrev(identifier)}"
181 cmd << " #{target(path)}"
193 cmd << " #{target(path)}"
182 cat = nil
194 cat = nil
183 shellout(cmd) do |io|
195 shellout(cmd) do |io|
184 io.binmode
196 io.binmode
185 cat = io.read
197 cat = io.read
186 end
198 end
187 return nil if $? && $?.exitstatus != 0
199 return nil if $? && $?.exitstatus != 0
188 cat
200 cat
189 end
201 end
190
202
191 def annotate(path, identifier=nil)
203 def annotate(path, identifier=nil)
192 path ||= ''
204 path ||= ''
193 cmd = "#{HG_BIN} -R #{target('')}"
205 cmd = "#{self.class.sq_bin} -R #{target('')}"
194 cmd << " annotate -ncu"
206 cmd << " annotate -ncu"
195 cmd << " -r #{hgrev(identifier)}"
207 cmd << " -r #{hgrev(identifier)}"
196 cmd << " #{target(path)}"
208 cmd << " #{target(path)}"
197 blame = Annotate.new
209 blame = Annotate.new
198 shellout(cmd) do |io|
210 shellout(cmd) do |io|
199 io.each_line do |line|
211 io.each_line do |line|
200 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
212 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
201 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
213 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
202 :identifier => $3)
214 :identifier => $3)
203 blame.add_line($4.rstrip, r)
215 blame.add_line($4.rstrip, r)
204 end
216 end
205 end
217 end
206 return nil if $? && $?.exitstatus != 0
218 return nil if $? && $?.exitstatus != 0
207 blame
219 blame
208 end
220 end
209
221
210 class Revision < Redmine::Scm::Adapters::Revision
222 class Revision < Redmine::Scm::Adapters::Revision
211 # Returns the readable identifier
223 # Returns the readable identifier
212 def format_identifier
224 def format_identifier
213 "#{revision}:#{scmid}"
225 "#{revision}:#{scmid}"
214 end
226 end
215 end
227 end
216
228
217 # Returns correct revision identifier
229 # Returns correct revision identifier
218 def hgrev(identifier)
230 def hgrev(identifier)
219 shell_quote(identifier.blank? ? 'tip' : identifier.to_s)
231 shell_quote(identifier.blank? ? 'tip' : identifier.to_s)
220 end
232 end
221 private :hgrev
233 private :hgrev
222 end
234 end
223 end
235 end
224 end
236 end
225 end
237 end
@@ -1,256 +1,264
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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 = Redmine::Configuration['scm_subversion_command'] || "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
34 def svn_binary_version
42 def svn_binary_version
35 cmd = "#{SVN_BIN} --version"
43 cmd = "#{sq_bin} --version"
36 version = nil
44 version = nil
37 shellout(cmd) do |io|
45 shellout(cmd) do |io|
38 # Read svn version in first returned line
46 # Read svn version in first returned line
39 if m = io.read.to_s.match(%r{\A(.*?)((\d+\.)+\d+)})
47 if m = io.read.to_s.match(%r{\A(.*?)((\d+\.)+\d+)})
40 version = m[2].scan(%r{\d+}).collect(&:to_i)
48 version = m[2].scan(%r{\d+}).collect(&:to_i)
41 end
49 end
42 end
50 end
43 return nil if $? && $?.exitstatus != 0
51 return nil if $? && $?.exitstatus != 0
44 version
52 version
45 end
53 end
46 end
54 end
47
55
48 # Get info about the svn repository
56 # Get info about the svn repository
49 def info
57 def info
50 cmd = "#{SVN_BIN} info --xml #{target}"
58 cmd = "#{self.class.sq_bin} info --xml #{target}"
51 cmd << credentials_string
59 cmd << credentials_string
52 info = nil
60 info = nil
53 shellout(cmd) do |io|
61 shellout(cmd) do |io|
54 output = io.read
62 output = io.read
55 begin
63 begin
56 doc = ActiveSupport::XmlMini.parse(output)
64 doc = ActiveSupport::XmlMini.parse(output)
57 #root_url = doc.elements["info/entry/repository/root"].text
65 #root_url = doc.elements["info/entry/repository/root"].text
58 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
66 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
59 :lastrev => Revision.new({
67 :lastrev => Revision.new({
60 :identifier => doc['info']['entry']['commit']['revision'],
68 :identifier => doc['info']['entry']['commit']['revision'],
61 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
69 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
62 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
70 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
63 })
71 })
64 })
72 })
65 rescue
73 rescue
66 end
74 end
67 end
75 end
68 return nil if $? && $?.exitstatus != 0
76 return nil if $? && $?.exitstatus != 0
69 info
77 info
70 rescue CommandFailed
78 rescue CommandFailed
71 return nil
79 return nil
72 end
80 end
73
81
74 # Returns an Entries collection
82 # Returns an Entries collection
75 # or nil if the given path doesn't exist in the repository
83 # or nil if the given path doesn't exist in the repository
76 def entries(path=nil, identifier=nil)
84 def entries(path=nil, identifier=nil)
77 path ||= ''
85 path ||= ''
78 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
86 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
79 entries = Entries.new
87 entries = Entries.new
80 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
88 cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
81 cmd << credentials_string
89 cmd << credentials_string
82 shellout(cmd) do |io|
90 shellout(cmd) do |io|
83 output = io.read
91 output = io.read
84 begin
92 begin
85 doc = ActiveSupport::XmlMini.parse(output)
93 doc = ActiveSupport::XmlMini.parse(output)
86 each_xml_element(doc['lists']['list'], 'entry') do |entry|
94 each_xml_element(doc['lists']['list'], 'entry') do |entry|
87 commit = entry['commit']
95 commit = entry['commit']
88 commit_date = commit['date']
96 commit_date = commit['date']
89 # Skip directory if there is no commit date (usually that
97 # Skip directory if there is no commit date (usually that
90 # means that we don't have read access to it)
98 # means that we don't have read access to it)
91 next if entry['kind'] == 'dir' && commit_date.nil?
99 next if entry['kind'] == 'dir' && commit_date.nil?
92 name = entry['name']['__content__']
100 name = entry['name']['__content__']
93 entries << Entry.new({:name => URI.unescape(name),
101 entries << Entry.new({:name => URI.unescape(name),
94 :path => ((path.empty? ? "" : "#{path}/") + name),
102 :path => ((path.empty? ? "" : "#{path}/") + name),
95 :kind => entry['kind'],
103 :kind => entry['kind'],
96 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
104 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
97 :lastrev => Revision.new({
105 :lastrev => Revision.new({
98 :identifier => commit['revision'],
106 :identifier => commit['revision'],
99 :time => Time.parse(commit_date['__content__'].to_s).localtime,
107 :time => Time.parse(commit_date['__content__'].to_s).localtime,
100 :author => ((a = commit['author']) ? a['__content__'] : nil)
108 :author => ((a = commit['author']) ? a['__content__'] : nil)
101 })
109 })
102 })
110 })
103 end
111 end
104 rescue Exception => e
112 rescue Exception => e
105 logger.error("Error parsing svn output: #{e.message}")
113 logger.error("Error parsing svn output: #{e.message}")
106 logger.error("Output was:\n #{output}")
114 logger.error("Output was:\n #{output}")
107 end
115 end
108 end
116 end
109 return nil if $? && $?.exitstatus != 0
117 return nil if $? && $?.exitstatus != 0
110 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
118 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
111 entries.sort_by_name
119 entries.sort_by_name
112 end
120 end
113
121
114 def properties(path, identifier=nil)
122 def properties(path, identifier=nil)
115 # proplist xml output supported in svn 1.5.0 and higher
123 # proplist xml output supported in svn 1.5.0 and higher
116 return nil unless self.class.client_version_above?([1, 5, 0])
124 return nil unless self.class.client_version_above?([1, 5, 0])
117
125
118 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
126 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
119 cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
127 cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
120 cmd << credentials_string
128 cmd << credentials_string
121 properties = {}
129 properties = {}
122 shellout(cmd) do |io|
130 shellout(cmd) do |io|
123 output = io.read
131 output = io.read
124 begin
132 begin
125 doc = ActiveSupport::XmlMini.parse(output)
133 doc = ActiveSupport::XmlMini.parse(output)
126 each_xml_element(doc['properties']['target'], 'property') do |property|
134 each_xml_element(doc['properties']['target'], 'property') do |property|
127 properties[ property['name'] ] = property['__content__'].to_s
135 properties[ property['name'] ] = property['__content__'].to_s
128 end
136 end
129 rescue
137 rescue
130 end
138 end
131 end
139 end
132 return nil if $? && $?.exitstatus != 0
140 return nil if $? && $?.exitstatus != 0
133 properties
141 properties
134 end
142 end
135
143
136 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
144 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
137 path ||= ''
145 path ||= ''
138 identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
146 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
147 identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
140 revisions = Revisions.new
148 revisions = Revisions.new
141 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
149 cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
142 cmd << credentials_string
150 cmd << credentials_string
143 cmd << " --verbose " if options[:with_paths]
151 cmd << " --verbose " if options[:with_paths]
144 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
152 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
145 cmd << ' ' + target(path)
153 cmd << ' ' + target(path)
146 shellout(cmd) do |io|
154 shellout(cmd) do |io|
147 output = io.read
155 output = io.read
148 begin
156 begin
149 doc = ActiveSupport::XmlMini.parse(output)
157 doc = ActiveSupport::XmlMini.parse(output)
150 each_xml_element(doc['log'], 'logentry') do |logentry|
158 each_xml_element(doc['log'], 'logentry') do |logentry|
151 paths = []
159 paths = []
152 each_xml_element(logentry['paths'], 'path') do |path|
160 each_xml_element(logentry['paths'], 'path') do |path|
153 paths << {:action => path['action'],
161 paths << {:action => path['action'],
154 :path => path['__content__'],
162 :path => path['__content__'],
155 :from_path => path['copyfrom-path'],
163 :from_path => path['copyfrom-path'],
156 :from_revision => path['copyfrom-rev']
164 :from_revision => path['copyfrom-rev']
157 }
165 }
158 end if logentry['paths'] && logentry['paths']['path']
166 end if logentry['paths'] && logentry['paths']['path']
159 paths.sort! { |x,y| x[:path] <=> y[:path] }
167 paths.sort! { |x,y| x[:path] <=> y[:path] }
160
168
161 revisions << Revision.new({:identifier => logentry['revision'],
169 revisions << Revision.new({:identifier => logentry['revision'],
162 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
170 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
163 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
171 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
164 :message => logentry['msg']['__content__'],
172 :message => logentry['msg']['__content__'],
165 :paths => paths
173 :paths => paths
166 })
174 })
167 end
175 end
168 rescue
176 rescue
169 end
177 end
170 end
178 end
171 return nil if $? && $?.exitstatus != 0
179 return nil if $? && $?.exitstatus != 0
172 revisions
180 revisions
173 end
181 end
174
182
175 def diff(path, identifier_from, identifier_to=nil, type="inline")
183 def diff(path, identifier_from, identifier_to=nil, type="inline")
176 path ||= ''
184 path ||= ''
177 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
185 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
178 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
186 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
179
187
180 cmd = "#{SVN_BIN} diff -r "
188 cmd = "#{self.class.sq_bin} diff -r "
181 cmd << "#{identifier_to}:"
189 cmd << "#{identifier_to}:"
182 cmd << "#{identifier_from}"
190 cmd << "#{identifier_from}"
183 cmd << " #{target(path)}@#{identifier_from}"
191 cmd << " #{target(path)}@#{identifier_from}"
184 cmd << credentials_string
192 cmd << credentials_string
185 diff = []
193 diff = []
186 shellout(cmd) do |io|
194 shellout(cmd) do |io|
187 io.each_line do |line|
195 io.each_line do |line|
188 diff << line
196 diff << line
189 end
197 end
190 end
198 end
191 return nil if $? && $?.exitstatus != 0
199 return nil if $? && $?.exitstatus != 0
192 diff
200 diff
193 end
201 end
194
202
195 def cat(path, identifier=nil)
203 def cat(path, identifier=nil)
196 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
204 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
197 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
205 cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
198 cmd << credentials_string
206 cmd << credentials_string
199 cat = nil
207 cat = nil
200 shellout(cmd) do |io|
208 shellout(cmd) do |io|
201 io.binmode
209 io.binmode
202 cat = io.read
210 cat = io.read
203 end
211 end
204 return nil if $? && $?.exitstatus != 0
212 return nil if $? && $?.exitstatus != 0
205 cat
213 cat
206 end
214 end
207
215
208 def annotate(path, identifier=nil)
216 def annotate(path, identifier=nil)
209 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
217 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
210 cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
218 cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
211 cmd << credentials_string
219 cmd << credentials_string
212 blame = Annotate.new
220 blame = Annotate.new
213 shellout(cmd) do |io|
221 shellout(cmd) do |io|
214 io.each_line do |line|
222 io.each_line do |line|
215 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
223 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
216 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
224 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
217 end
225 end
218 end
226 end
219 return nil if $? && $?.exitstatus != 0
227 return nil if $? && $?.exitstatus != 0
220 blame
228 blame
221 end
229 end
222
230
223 private
231 private
224
232
225 def credentials_string
233 def credentials_string
226 str = ''
234 str = ''
227 str << " --username #{shell_quote(@login)}" unless @login.blank?
235 str << " --username #{shell_quote(@login)}" unless @login.blank?
228 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
236 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
229 str << " --no-auth-cache --non-interactive"
237 str << " --no-auth-cache --non-interactive"
230 str
238 str
231 end
239 end
232
240
233 # Helper that iterates over the child elements of a xml node
241 # 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
242 # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
235 def each_xml_element(node, name)
243 def each_xml_element(node, name)
236 if node && node[name]
244 if node && node[name]
237 if node[name].is_a?(Hash)
245 if node[name].is_a?(Hash)
238 yield node[name]
246 yield node[name]
239 else
247 else
240 node[name].each do |element|
248 node[name].each do |element|
241 yield element
249 yield element
242 end
250 end
243 end
251 end
244 end
252 end
245 end
253 end
246
254
247 def target(path = '')
255 def target(path = '')
248 base = path.match(/^\//) ? root_url : url
256 base = path.match(/^\//) ? root_url : url
249 uri = "#{base}/#{path}"
257 uri = "#{base}/#{path}"
250 uri = URI.escape(URI.escape(uri), '[]')
258 uri = URI.escape(URI.escape(uri), '[]')
251 shell_quote(uri.gsub(/[?<>\*]/, ''))
259 shell_quote(uri.gsub(/[?<>\*]/, ''))
252 end
260 end
253 end
261 end
254 end
262 end
255 end
263 end
256 end
264 end
General Comments 0
You need to be logged in to leave comments. Login now