##// END OF EJS Templates
scm: git: use stdin instead of command line in "git log" (#10470)...
Toshi MARUYAMA -
r9148:3e11f9abfe9d
parent child
Show More
@@ -1,382 +1,382
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require '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
27
28 # raised if scm command exited with error, e.g. unknown revision.
28 # raised if scm command exited with error, e.g. unknown revision.
29 class ScmCommandAborted < CommandFailed; end
29 class ScmCommandAborted < CommandFailed; end
30
30
31 class << self
31 class << self
32 def client_command
32 def client_command
33 ""
33 ""
34 end
34 end
35
35
36 def shell_quote_command
36 def shell_quote_command
37 if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
37 if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
38 client_command
38 client_command
39 else
39 else
40 shell_quote(client_command)
40 shell_quote(client_command)
41 end
41 end
42 end
42 end
43
43
44 # Returns the version of the scm client
44 # Returns the version of the scm client
45 # Eg: [1, 5, 0] or [] if unknown
45 # Eg: [1, 5, 0] or [] if unknown
46 def client_version
46 def client_version
47 []
47 []
48 end
48 end
49
49
50 # Returns the version string of the scm client
50 # Returns the version string of the scm client
51 # Eg: '1.5.0' or 'Unknown version' if unknown
51 # Eg: '1.5.0' or 'Unknown version' if unknown
52 def client_version_string
52 def client_version_string
53 v = client_version || 'Unknown version'
53 v = client_version || 'Unknown version'
54 v.is_a?(Array) ? v.join('.') : v.to_s
54 v.is_a?(Array) ? v.join('.') : v.to_s
55 end
55 end
56
56
57 # Returns true if the current client version is above
57 # Returns true if the current client version is above
58 # or equals the given one
58 # or equals the given one
59 # If option is :unknown is set to true, it will return
59 # If option is :unknown is set to true, it will return
60 # true if the client version is unknown
60 # true if the client version is unknown
61 def client_version_above?(v, options={})
61 def client_version_above?(v, options={})
62 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
62 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
63 end
63 end
64
64
65 def client_available
65 def client_available
66 true
66 true
67 end
67 end
68
68
69 def shell_quote(str)
69 def shell_quote(str)
70 if Redmine::Platform.mswin?
70 if Redmine::Platform.mswin?
71 '"' + str.gsub(/"/, '\\"') + '"'
71 '"' + str.gsub(/"/, '\\"') + '"'
72 else
72 else
73 "'" + str.gsub(/'/, "'\"'\"'") + "'"
73 "'" + str.gsub(/'/, "'\"'\"'") + "'"
74 end
74 end
75 end
75 end
76 end
76 end
77
77
78 def initialize(url, root_url=nil, login=nil, password=nil,
78 def initialize(url, root_url=nil, login=nil, password=nil,
79 path_encoding=nil)
79 path_encoding=nil)
80 @url = url
80 @url = url
81 @login = login if login && !login.empty?
81 @login = login if login && !login.empty?
82 @password = (password || "") if @login
82 @password = (password || "") if @login
83 @root_url = root_url.blank? ? retrieve_root_url : root_url
83 @root_url = root_url.blank? ? retrieve_root_url : root_url
84 end
84 end
85
85
86 def adapter_name
86 def adapter_name
87 'Abstract'
87 'Abstract'
88 end
88 end
89
89
90 def supports_cat?
90 def supports_cat?
91 true
91 true
92 end
92 end
93
93
94 def supports_annotate?
94 def supports_annotate?
95 respond_to?('annotate')
95 respond_to?('annotate')
96 end
96 end
97
97
98 def root_url
98 def root_url
99 @root_url
99 @root_url
100 end
100 end
101
101
102 def url
102 def url
103 @url
103 @url
104 end
104 end
105
105
106 def path_encoding
106 def path_encoding
107 nil
107 nil
108 end
108 end
109
109
110 # get info about the svn repository
110 # get info about the svn repository
111 def info
111 def info
112 return nil
112 return nil
113 end
113 end
114
114
115 # Returns the entry identified by path and revision identifier
115 # Returns the entry identified by path and revision identifier
116 # or nil if entry doesn't exist in the repository
116 # or nil if entry doesn't exist in the repository
117 def entry(path=nil, identifier=nil)
117 def entry(path=nil, identifier=nil)
118 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
118 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
119 search_path = parts[0..-2].join('/')
119 search_path = parts[0..-2].join('/')
120 search_name = parts[-1]
120 search_name = parts[-1]
121 if search_path.blank? && search_name.blank?
121 if search_path.blank? && search_name.blank?
122 # Root entry
122 # Root entry
123 Entry.new(:path => '', :kind => 'dir')
123 Entry.new(:path => '', :kind => 'dir')
124 else
124 else
125 # Search for the entry in the parent directory
125 # Search for the entry in the parent directory
126 es = entries(search_path, identifier)
126 es = entries(search_path, identifier)
127 es ? es.detect {|e| e.name == search_name} : nil
127 es ? es.detect {|e| e.name == search_name} : nil
128 end
128 end
129 end
129 end
130
130
131 # Returns an Entries collection
131 # Returns an Entries collection
132 # or nil if the given path doesn't exist in the repository
132 # or nil if the given path doesn't exist in the repository
133 def entries(path=nil, identifier=nil, options={})
133 def entries(path=nil, identifier=nil, options={})
134 return nil
134 return nil
135 end
135 end
136
136
137 def branches
137 def branches
138 return nil
138 return nil
139 end
139 end
140
140
141 def tags
141 def tags
142 return nil
142 return nil
143 end
143 end
144
144
145 def default_branch
145 def default_branch
146 return nil
146 return nil
147 end
147 end
148
148
149 def properties(path, identifier=nil)
149 def properties(path, identifier=nil)
150 return nil
150 return nil
151 end
151 end
152
152
153 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
153 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
154 return nil
154 return nil
155 end
155 end
156
156
157 def diff(path, identifier_from, identifier_to=nil)
157 def diff(path, identifier_from, identifier_to=nil)
158 return nil
158 return nil
159 end
159 end
160
160
161 def cat(path, identifier=nil)
161 def cat(path, identifier=nil)
162 return nil
162 return nil
163 end
163 end
164
164
165 def with_leading_slash(path)
165 def with_leading_slash(path)
166 path ||= ''
166 path ||= ''
167 (path[0,1]!="/") ? "/#{path}" : path
167 (path[0,1]!="/") ? "/#{path}" : path
168 end
168 end
169
169
170 def with_trailling_slash(path)
170 def with_trailling_slash(path)
171 path ||= ''
171 path ||= ''
172 (path[-1,1] == "/") ? path : "#{path}/"
172 (path[-1,1] == "/") ? path : "#{path}/"
173 end
173 end
174
174
175 def without_leading_slash(path)
175 def without_leading_slash(path)
176 path ||= ''
176 path ||= ''
177 path.gsub(%r{^/+}, '')
177 path.gsub(%r{^/+}, '')
178 end
178 end
179
179
180 def without_trailling_slash(path)
180 def without_trailling_slash(path)
181 path ||= ''
181 path ||= ''
182 (path[-1,1] == "/") ? path[0..-2] : path
182 (path[-1,1] == "/") ? path[0..-2] : path
183 end
183 end
184
184
185 def shell_quote(str)
185 def shell_quote(str)
186 self.class.shell_quote(str)
186 self.class.shell_quote(str)
187 end
187 end
188
188
189 private
189 private
190 def retrieve_root_url
190 def retrieve_root_url
191 info = self.info
191 info = self.info
192 info ? info.root_url : nil
192 info ? info.root_url : nil
193 end
193 end
194
194
195 def target(path, sq=true)
195 def target(path, sq=true)
196 path ||= ''
196 path ||= ''
197 base = path.match(/^\//) ? root_url : url
197 base = path.match(/^\//) ? root_url : url
198 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
198 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
199 if sq
199 if sq
200 str = shell_quote(str)
200 str = shell_quote(str)
201 end
201 end
202 str
202 str
203 end
203 end
204
204
205 def logger
205 def logger
206 self.class.logger
206 self.class.logger
207 end
207 end
208
208
209 def shellout(cmd, &block)
209 def shellout(cmd, options = {}, &block)
210 self.class.shellout(cmd, &block)
210 self.class.shellout(cmd, options, &block)
211 end
211 end
212
212
213 def self.logger
213 def self.logger
214 Rails.logger
214 Rails.logger
215 end
215 end
216
216
217 def self.shellout(cmd, &block)
217 def self.shellout(cmd, options = {}, &block)
218 if logger && logger.debug?
218 if logger && logger.debug?
219 logger.debug "Shelling out: #{strip_credential(cmd)}"
219 logger.debug "Shelling out: #{strip_credential(cmd)}"
220 end
220 end
221 if Rails.env == 'development'
221 if Rails.env == 'development'
222 # Capture stderr when running in dev environment
222 # Capture stderr when running in dev environment
223 cmd = "#{cmd} 2>>#{shell_quote(Rails.root.join('log/scm.stderr.log').to_s)}"
223 cmd = "#{cmd} 2>>#{shell_quote(Rails.root.join('log/scm.stderr.log').to_s)}"
224 end
224 end
225 begin
225 begin
226 mode = "r+"
226 mode = "r+"
227 IO.popen(cmd, mode) do |io|
227 IO.popen(cmd, mode) do |io|
228 io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
228 io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
229 io.close_write
229 io.close_write unless options[:write_stdin]
230 block.call(io) if block_given?
230 block.call(io) if block_given?
231 end
231 end
232 ## If scm command does not exist,
232 ## If scm command does not exist,
233 ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
233 ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
234 ## in production environment.
234 ## in production environment.
235 # rescue Errno::ENOENT => e
235 # rescue Errno::ENOENT => e
236 rescue Exception => e
236 rescue Exception => e
237 msg = strip_credential(e.message)
237 msg = strip_credential(e.message)
238 # The command failed, log it and re-raise
238 # The command failed, log it and re-raise
239 logmsg = "SCM command failed, "
239 logmsg = "SCM command failed, "
240 logmsg += "make sure that your SCM command (e.g. svn) is "
240 logmsg += "make sure that your SCM command (e.g. svn) is "
241 logmsg += "in PATH (#{ENV['PATH']})\n"
241 logmsg += "in PATH (#{ENV['PATH']})\n"
242 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
242 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
243 logmsg += "#{strip_credential(cmd)}\n"
243 logmsg += "#{strip_credential(cmd)}\n"
244 logmsg += "with: #{msg}"
244 logmsg += "with: #{msg}"
245 logger.error(logmsg)
245 logger.error(logmsg)
246 raise CommandFailed.new(msg)
246 raise CommandFailed.new(msg)
247 end
247 end
248 end
248 end
249
249
250 # Hides username/password in a given command
250 # Hides username/password in a given command
251 def self.strip_credential(cmd)
251 def self.strip_credential(cmd)
252 q = (Redmine::Platform.mswin? ? '"' : "'")
252 q = (Redmine::Platform.mswin? ? '"' : "'")
253 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
253 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
254 end
254 end
255
255
256 def strip_credential(cmd)
256 def strip_credential(cmd)
257 self.class.strip_credential(cmd)
257 self.class.strip_credential(cmd)
258 end
258 end
259
259
260 def scm_iconv(to, from, str)
260 def scm_iconv(to, from, str)
261 return nil if str.nil?
261 return nil if str.nil?
262 return str if to == from
262 return str if to == from
263 begin
263 begin
264 Iconv.conv(to, from, str)
264 Iconv.conv(to, from, str)
265 rescue Iconv::Failure => err
265 rescue Iconv::Failure => err
266 logger.error("failed to convert from #{from} to #{to}. #{err}")
266 logger.error("failed to convert from #{from} to #{to}. #{err}")
267 nil
267 nil
268 end
268 end
269 end
269 end
270 end
270 end
271
271
272 class Entries < Array
272 class Entries < Array
273 def sort_by_name
273 def sort_by_name
274 sort {|x,y|
274 sort {|x,y|
275 if x.kind == y.kind
275 if x.kind == y.kind
276 x.name.to_s <=> y.name.to_s
276 x.name.to_s <=> y.name.to_s
277 else
277 else
278 x.kind <=> y.kind
278 x.kind <=> y.kind
279 end
279 end
280 }
280 }
281 end
281 end
282
282
283 def revisions
283 def revisions
284 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
284 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
285 end
285 end
286 end
286 end
287
287
288 class Info
288 class Info
289 attr_accessor :root_url, :lastrev
289 attr_accessor :root_url, :lastrev
290 def initialize(attributes={})
290 def initialize(attributes={})
291 self.root_url = attributes[:root_url] if attributes[:root_url]
291 self.root_url = attributes[:root_url] if attributes[:root_url]
292 self.lastrev = attributes[:lastrev]
292 self.lastrev = attributes[:lastrev]
293 end
293 end
294 end
294 end
295
295
296 class Entry
296 class Entry
297 attr_accessor :name, :path, :kind, :size, :lastrev
297 attr_accessor :name, :path, :kind, :size, :lastrev
298 def initialize(attributes={})
298 def initialize(attributes={})
299 self.name = attributes[:name] if attributes[:name]
299 self.name = attributes[:name] if attributes[:name]
300 self.path = attributes[:path] if attributes[:path]
300 self.path = attributes[:path] if attributes[:path]
301 self.kind = attributes[:kind] if attributes[:kind]
301 self.kind = attributes[:kind] if attributes[:kind]
302 self.size = attributes[:size].to_i if attributes[:size]
302 self.size = attributes[:size].to_i if attributes[:size]
303 self.lastrev = attributes[:lastrev]
303 self.lastrev = attributes[:lastrev]
304 end
304 end
305
305
306 def is_file?
306 def is_file?
307 'file' == self.kind
307 'file' == self.kind
308 end
308 end
309
309
310 def is_dir?
310 def is_dir?
311 'dir' == self.kind
311 'dir' == self.kind
312 end
312 end
313
313
314 def is_text?
314 def is_text?
315 Redmine::MimeType.is_type?('text', name)
315 Redmine::MimeType.is_type?('text', name)
316 end
316 end
317 end
317 end
318
318
319 class Revisions < Array
319 class Revisions < Array
320 def latest
320 def latest
321 sort {|x,y|
321 sort {|x,y|
322 unless x.time.nil? or y.time.nil?
322 unless x.time.nil? or y.time.nil?
323 x.time <=> y.time
323 x.time <=> y.time
324 else
324 else
325 0
325 0
326 end
326 end
327 }.last
327 }.last
328 end
328 end
329 end
329 end
330
330
331 class Revision
331 class Revision
332 attr_accessor :scmid, :name, :author, :time, :message,
332 attr_accessor :scmid, :name, :author, :time, :message,
333 :paths, :revision, :branch, :identifier,
333 :paths, :revision, :branch, :identifier,
334 :parents
334 :parents
335
335
336 def initialize(attributes={})
336 def initialize(attributes={})
337 self.identifier = attributes[:identifier]
337 self.identifier = attributes[:identifier]
338 self.scmid = attributes[:scmid]
338 self.scmid = attributes[:scmid]
339 self.name = attributes[:name] || self.identifier
339 self.name = attributes[:name] || self.identifier
340 self.author = attributes[:author]
340 self.author = attributes[:author]
341 self.time = attributes[:time]
341 self.time = attributes[:time]
342 self.message = attributes[:message] || ""
342 self.message = attributes[:message] || ""
343 self.paths = attributes[:paths]
343 self.paths = attributes[:paths]
344 self.revision = attributes[:revision]
344 self.revision = attributes[:revision]
345 self.branch = attributes[:branch]
345 self.branch = attributes[:branch]
346 self.parents = attributes[:parents]
346 self.parents = attributes[:parents]
347 end
347 end
348
348
349 # Returns the readable identifier.
349 # Returns the readable identifier.
350 def format_identifier
350 def format_identifier
351 self.identifier.to_s
351 self.identifier.to_s
352 end
352 end
353 end
353 end
354
354
355 class Annotate
355 class Annotate
356 attr_reader :lines, :revisions
356 attr_reader :lines, :revisions
357
357
358 def initialize
358 def initialize
359 @lines = []
359 @lines = []
360 @revisions = []
360 @revisions = []
361 end
361 end
362
362
363 def add_line(line, revision)
363 def add_line(line, revision)
364 @lines << line
364 @lines << line
365 @revisions << revision
365 @revisions << revision
366 end
366 end
367
367
368 def content
368 def content
369 content = lines.join("\n")
369 content = lines.join("\n")
370 end
370 end
371
371
372 def empty?
372 def empty?
373 lines.empty?
373 lines.empty?
374 end
374 end
375 end
375 end
376
376
377 class Branch < String
377 class Branch < String
378 attr_accessor :revision, :scmid
378 attr_accessor :revision, :scmid
379 end
379 end
380 end
380 end
381 end
381 end
382 end
382 end
@@ -1,407 +1,412
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19
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
24
25 # Git executable name
25 # Git executable name
26 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
26 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
27
27
28 class GitBranch < Branch
28 class GitBranch < Branch
29 attr_accessor :is_default
29 attr_accessor :is_default
30 end
30 end
31
31
32 class << self
32 class << self
33 def client_command
33 def client_command
34 @@bin ||= GIT_BIN
34 @@bin ||= GIT_BIN
35 end
35 end
36
36
37 def sq_bin
37 def sq_bin
38 @@sq_bin ||= shell_quote_command
38 @@sq_bin ||= shell_quote_command
39 end
39 end
40
40
41 def client_version
41 def client_version
42 @@client_version ||= (scm_command_version || [])
42 @@client_version ||= (scm_command_version || [])
43 end
43 end
44
44
45 def client_available
45 def client_available
46 !client_version.empty?
46 !client_version.empty?
47 end
47 end
48
48
49 def scm_command_version
49 def scm_command_version
50 scm_version = scm_version_from_command_line.dup
50 scm_version = scm_version_from_command_line.dup
51 if scm_version.respond_to?(:force_encoding)
51 if scm_version.respond_to?(:force_encoding)
52 scm_version.force_encoding('ASCII-8BIT')
52 scm_version.force_encoding('ASCII-8BIT')
53 end
53 end
54 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
54 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
55 m[2].scan(%r{\d+}).collect(&:to_i)
55 m[2].scan(%r{\d+}).collect(&:to_i)
56 end
56 end
57 end
57 end
58
58
59 def scm_version_from_command_line
59 def scm_version_from_command_line
60 shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
60 shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
61 end
61 end
62 end
62 end
63
63
64 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
64 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
65 super
65 super
66 @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
66 @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
67 end
67 end
68
68
69 def path_encoding
69 def path_encoding
70 @path_encoding
70 @path_encoding
71 end
71 end
72
72
73 def info
73 def info
74 begin
74 begin
75 Info.new(:root_url => url, :lastrev => lastrev('',nil))
75 Info.new(:root_url => url, :lastrev => lastrev('',nil))
76 rescue
76 rescue
77 nil
77 nil
78 end
78 end
79 end
79 end
80
80
81 def branches
81 def branches
82 return @branches if @branches
82 return @branches if @branches
83 @branches = []
83 @branches = []
84 cmd_args = %w|branch --no-color --verbose --no-abbrev|
84 cmd_args = %w|branch --no-color --verbose --no-abbrev|
85 git_cmd(cmd_args) do |io|
85 git_cmd(cmd_args) do |io|
86 io.each_line do |line|
86 io.each_line do |line|
87 branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
87 branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
88 bran = GitBranch.new(branch_rev[2])
88 bran = GitBranch.new(branch_rev[2])
89 bran.revision = branch_rev[3]
89 bran.revision = branch_rev[3]
90 bran.scmid = branch_rev[3]
90 bran.scmid = branch_rev[3]
91 bran.is_default = ( branch_rev[1] == '*' )
91 bran.is_default = ( branch_rev[1] == '*' )
92 @branches << bran
92 @branches << bran
93 end
93 end
94 end
94 end
95 @branches.sort!
95 @branches.sort!
96 rescue ScmCommandAborted
96 rescue ScmCommandAborted
97 nil
97 nil
98 end
98 end
99
99
100 def tags
100 def tags
101 return @tags if @tags
101 return @tags if @tags
102 cmd_args = %w|tag|
102 cmd_args = %w|tag|
103 git_cmd(cmd_args) do |io|
103 git_cmd(cmd_args) do |io|
104 @tags = io.readlines.sort!.map{|t| t.strip}
104 @tags = io.readlines.sort!.map{|t| t.strip}
105 end
105 end
106 rescue ScmCommandAborted
106 rescue ScmCommandAborted
107 nil
107 nil
108 end
108 end
109
109
110 def default_branch
110 def default_branch
111 bras = self.branches
111 bras = self.branches
112 return nil if bras.nil?
112 return nil if bras.nil?
113 default_bras = bras.select{|x| x.is_default == true}
113 default_bras = bras.select{|x| x.is_default == true}
114 return default_bras.first.to_s if ! default_bras.empty?
114 return default_bras.first.to_s if ! default_bras.empty?
115 master_bras = bras.select{|x| x.to_s == 'master'}
115 master_bras = bras.select{|x| x.to_s == 'master'}
116 master_bras.empty? ? bras.first.to_s : 'master'
116 master_bras.empty? ? bras.first.to_s : 'master'
117 end
117 end
118
118
119 def entry(path=nil, identifier=nil)
119 def entry(path=nil, identifier=nil)
120 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
120 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
121 search_path = parts[0..-2].join('/')
121 search_path = parts[0..-2].join('/')
122 search_name = parts[-1]
122 search_name = parts[-1]
123 if search_path.blank? && search_name.blank?
123 if search_path.blank? && search_name.blank?
124 # Root entry
124 # Root entry
125 Entry.new(:path => '', :kind => 'dir')
125 Entry.new(:path => '', :kind => 'dir')
126 else
126 else
127 # Search for the entry in the parent directory
127 # Search for the entry in the parent directory
128 es = entries(search_path, identifier,
128 es = entries(search_path, identifier,
129 options = {:report_last_commit => false})
129 options = {:report_last_commit => false})
130 es ? es.detect {|e| e.name == search_name} : nil
130 es ? es.detect {|e| e.name == search_name} : nil
131 end
131 end
132 end
132 end
133
133
134 def entries(path=nil, identifier=nil, options={})
134 def entries(path=nil, identifier=nil, options={})
135 path ||= ''
135 path ||= ''
136 p = scm_iconv(@path_encoding, 'UTF-8', path)
136 p = scm_iconv(@path_encoding, 'UTF-8', path)
137 entries = Entries.new
137 entries = Entries.new
138 cmd_args = %w|ls-tree -l|
138 cmd_args = %w|ls-tree -l|
139 cmd_args << "HEAD:#{p}" if identifier.nil?
139 cmd_args << "HEAD:#{p}" if identifier.nil?
140 cmd_args << "#{identifier}:#{p}" if identifier
140 cmd_args << "#{identifier}:#{p}" if identifier
141 git_cmd(cmd_args) do |io|
141 git_cmd(cmd_args) do |io|
142 io.each_line do |line|
142 io.each_line do |line|
143 e = line.chomp.to_s
143 e = line.chomp.to_s
144 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
144 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
145 type = $1
145 type = $1
146 sha = $2
146 sha = $2
147 size = $3
147 size = $3
148 name = $4
148 name = $4
149 if name.respond_to?(:force_encoding)
149 if name.respond_to?(:force_encoding)
150 name.force_encoding(@path_encoding)
150 name.force_encoding(@path_encoding)
151 end
151 end
152 full_path = p.empty? ? name : "#{p}/#{name}"
152 full_path = p.empty? ? name : "#{p}/#{name}"
153 n = scm_iconv('UTF-8', @path_encoding, name)
153 n = scm_iconv('UTF-8', @path_encoding, name)
154 full_p = scm_iconv('UTF-8', @path_encoding, full_path)
154 full_p = scm_iconv('UTF-8', @path_encoding, full_path)
155 entries << Entry.new({:name => n,
155 entries << Entry.new({:name => n,
156 :path => full_p,
156 :path => full_p,
157 :kind => (type == "tree") ? 'dir' : 'file',
157 :kind => (type == "tree") ? 'dir' : 'file',
158 :size => (type == "tree") ? nil : size,
158 :size => (type == "tree") ? nil : size,
159 :lastrev => options[:report_last_commit] ?
159 :lastrev => options[:report_last_commit] ?
160 lastrev(full_path, identifier) : Revision.new
160 lastrev(full_path, identifier) : Revision.new
161 }) unless entries.detect{|entry| entry.name == name}
161 }) unless entries.detect{|entry| entry.name == name}
162 end
162 end
163 end
163 end
164 end
164 end
165 entries.sort_by_name
165 entries.sort_by_name
166 rescue ScmCommandAborted
166 rescue ScmCommandAborted
167 nil
167 nil
168 end
168 end
169
169
170 def lastrev(path, rev)
170 def lastrev(path, rev)
171 return nil if path.nil?
171 return nil if path.nil?
172 cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
172 cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
173 cmd_args << rev if rev
173 cmd_args << rev if rev
174 cmd_args << "--" << path unless path.empty?
174 cmd_args << "--" << path unless path.empty?
175 lines = []
175 lines = []
176 git_cmd(cmd_args) { |io| lines = io.readlines }
176 git_cmd(cmd_args) { |io| lines = io.readlines }
177 begin
177 begin
178 id = lines[0].split[1]
178 id = lines[0].split[1]
179 author = lines[1].match('Author:\s+(.*)$')[1]
179 author = lines[1].match('Author:\s+(.*)$')[1]
180 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
180 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
181
181
182 Revision.new({
182 Revision.new({
183 :identifier => id,
183 :identifier => id,
184 :scmid => id,
184 :scmid => id,
185 :author => author,
185 :author => author,
186 :time => time,
186 :time => time,
187 :message => nil,
187 :message => nil,
188 :paths => nil
188 :paths => nil
189 })
189 })
190 rescue NoMethodError => e
190 rescue NoMethodError => e
191 logger.error("The revision '#{path}' has a wrong format")
191 logger.error("The revision '#{path}' has a wrong format")
192 return nil
192 return nil
193 end
193 end
194 rescue ScmCommandAborted
194 rescue ScmCommandAborted
195 nil
195 nil
196 end
196 end
197
197
198 def revisions(path, identifier_from, identifier_to, options={})
198 def revisions(path, identifier_from, identifier_to, options={})
199 revs = Revisions.new
199 revs = Revisions.new
200 cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents|
200 cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin|
201 cmd_args << "--reverse" if options[:reverse]
201 cmd_args << "--reverse" if options[:reverse]
202 cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
202 cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
203 from_to = ""
203 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
204 revisions = []
204 if identifier_from || identifier_to
205 if identifier_from || identifier_to
205 from_to << "#{identifier_from}.." if identifier_from
206 revisions << ""
206 from_to << "#{identifier_to}" if identifier_to
207 revisions[0] << "#{identifier_from}.." if identifier_from
207 cmd_args << from_to if !from_to.empty?
208 revisions[0] << "#{identifier_to}" if identifier_to
208 else
209 else
209 cmd_args += options[:includes] unless options[:includes].blank?
210 unless options[:includes].blank?
211 revisions += options[:includes]
212 end
210 unless options[:excludes].blank?
213 unless options[:excludes].blank?
211 cmd_args << "--not"
214 revisions += options[:excludes].map{|r| "^#{r}"}
212 cmd_args += options[:excludes]
213 end
215 end
214 end
216 end
215 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
216
217
217 git_cmd(cmd_args) do |io|
218 git_cmd(cmd_args, {:write_stdin => true}) do |io|
219 io.binmode
220 io.puts(revisions.join("\n"))
221 io.close_write
218 files=[]
222 files=[]
219 changeset = {}
223 changeset = {}
220 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
224 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
221
225
222 io.each_line do |line|
226 io.each_line do |line|
223 if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
227 if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
224 key = "commit"
228 key = "commit"
225 value = $1
229 value = $1
226 parents_str = $2
230 parents_str = $2
227 if (parsing_descr == 1 || parsing_descr == 2)
231 if (parsing_descr == 1 || parsing_descr == 2)
228 parsing_descr = 0
232 parsing_descr = 0
229 revision = Revision.new({
233 revision = Revision.new({
230 :identifier => changeset[:commit],
234 :identifier => changeset[:commit],
231 :scmid => changeset[:commit],
235 :scmid => changeset[:commit],
232 :author => changeset[:author],
236 :author => changeset[:author],
233 :time => Time.parse(changeset[:date]),
237 :time => Time.parse(changeset[:date]),
234 :message => changeset[:description],
238 :message => changeset[:description],
235 :paths => files,
239 :paths => files,
236 :parents => changeset[:parents]
240 :parents => changeset[:parents]
237 })
241 })
238 if block_given?
242 if block_given?
239 yield revision
243 yield revision
240 else
244 else
241 revs << revision
245 revs << revision
242 end
246 end
243 changeset = {}
247 changeset = {}
244 files = []
248 files = []
245 end
249 end
246 changeset[:commit] = $1
250 changeset[:commit] = $1
247 unless parents_str.nil? or parents_str == ""
251 unless parents_str.nil? or parents_str == ""
248 changeset[:parents] = parents_str.strip.split(' ')
252 changeset[:parents] = parents_str.strip.split(' ')
249 end
253 end
250 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
254 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
251 key = $1
255 key = $1
252 value = $2
256 value = $2
253 if key == "Author"
257 if key == "Author"
254 changeset[:author] = value
258 changeset[:author] = value
255 elsif key == "CommitDate"
259 elsif key == "CommitDate"
256 changeset[:date] = value
260 changeset[:date] = value
257 end
261 end
258 elsif (parsing_descr == 0) && line.chomp.to_s == ""
262 elsif (parsing_descr == 0) && line.chomp.to_s == ""
259 parsing_descr = 1
263 parsing_descr = 1
260 changeset[:description] = ""
264 changeset[:description] = ""
261 elsif (parsing_descr == 1 || parsing_descr == 2) \
265 elsif (parsing_descr == 1 || parsing_descr == 2) \
262 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
266 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
263 parsing_descr = 2
267 parsing_descr = 2
264 fileaction = $1
268 fileaction = $1
265 filepath = $2
269 filepath = $2
266 p = scm_iconv('UTF-8', @path_encoding, filepath)
270 p = scm_iconv('UTF-8', @path_encoding, filepath)
267 files << {:action => fileaction, :path => p}
271 files << {:action => fileaction, :path => p}
268 elsif (parsing_descr == 1 || parsing_descr == 2) \
272 elsif (parsing_descr == 1 || parsing_descr == 2) \
269 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
273 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
270 parsing_descr = 2
274 parsing_descr = 2
271 fileaction = $1
275 fileaction = $1
272 filepath = $3
276 filepath = $3
273 p = scm_iconv('UTF-8', @path_encoding, filepath)
277 p = scm_iconv('UTF-8', @path_encoding, filepath)
274 files << {:action => fileaction, :path => p}
278 files << {:action => fileaction, :path => p}
275 elsif (parsing_descr == 1) && line.chomp.to_s == ""
279 elsif (parsing_descr == 1) && line.chomp.to_s == ""
276 parsing_descr = 2
280 parsing_descr = 2
277 elsif (parsing_descr == 1)
281 elsif (parsing_descr == 1)
278 changeset[:description] << line[4..-1]
282 changeset[:description] << line[4..-1]
279 end
283 end
280 end
284 end
281
285
282 if changeset[:commit]
286 if changeset[:commit]
283 revision = Revision.new({
287 revision = Revision.new({
284 :identifier => changeset[:commit],
288 :identifier => changeset[:commit],
285 :scmid => changeset[:commit],
289 :scmid => changeset[:commit],
286 :author => changeset[:author],
290 :author => changeset[:author],
287 :time => Time.parse(changeset[:date]),
291 :time => Time.parse(changeset[:date]),
288 :message => changeset[:description],
292 :message => changeset[:description],
289 :paths => files,
293 :paths => files,
290 :parents => changeset[:parents]
294 :parents => changeset[:parents]
291 })
295 })
292 if block_given?
296 if block_given?
293 yield revision
297 yield revision
294 else
298 else
295 revs << revision
299 revs << revision
296 end
300 end
297 end
301 end
298 end
302 end
299 revs
303 revs
300 rescue ScmCommandAborted => e
304 rescue ScmCommandAborted => e
301 err_msg = "git log error: #{e.message}"
305 err_msg = "git log error: #{e.message}"
302 logger.error(err_msg)
306 logger.error(err_msg)
303 if block_given?
307 if block_given?
304 raise CommandFailed, err_msg
308 raise CommandFailed, err_msg
305 else
309 else
306 revs
310 revs
307 end
311 end
308 end
312 end
309
313
310 def diff(path, identifier_from, identifier_to=nil)
314 def diff(path, identifier_from, identifier_to=nil)
311 path ||= ''
315 path ||= ''
312 cmd_args = []
316 cmd_args = []
313 if identifier_to
317 if identifier_to
314 cmd_args << "diff" << "--no-color" << identifier_to << identifier_from
318 cmd_args << "diff" << "--no-color" << identifier_to << identifier_from
315 else
319 else
316 cmd_args << "show" << "--no-color" << identifier_from
320 cmd_args << "show" << "--no-color" << identifier_from
317 end
321 end
318 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
322 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
319 diff = []
323 diff = []
320 git_cmd(cmd_args) do |io|
324 git_cmd(cmd_args) do |io|
321 io.each_line do |line|
325 io.each_line do |line|
322 diff << line
326 diff << line
323 end
327 end
324 end
328 end
325 diff
329 diff
326 rescue ScmCommandAborted
330 rescue ScmCommandAborted
327 nil
331 nil
328 end
332 end
329
333
330 def annotate(path, identifier=nil)
334 def annotate(path, identifier=nil)
331 identifier = 'HEAD' if identifier.blank?
335 identifier = 'HEAD' if identifier.blank?
332 cmd_args = %w|blame|
336 cmd_args = %w|blame|
333 cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
337 cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
334 blame = Annotate.new
338 blame = Annotate.new
335 content = nil
339 content = nil
336 git_cmd(cmd_args) { |io| io.binmode; content = io.read }
340 git_cmd(cmd_args) { |io| io.binmode; content = io.read }
337 # git annotates binary files
341 # git annotates binary files
338 return nil if content.is_binary_data?
342 return nil if content.is_binary_data?
339 identifier = ''
343 identifier = ''
340 # git shows commit author on the first occurrence only
344 # git shows commit author on the first occurrence only
341 authors_by_commit = {}
345 authors_by_commit = {}
342 content.split("\n").each do |line|
346 content.split("\n").each do |line|
343 if line =~ /^([0-9a-f]{39,40})\s.*/
347 if line =~ /^([0-9a-f]{39,40})\s.*/
344 identifier = $1
348 identifier = $1
345 elsif line =~ /^author (.+)/
349 elsif line =~ /^author (.+)/
346 authors_by_commit[identifier] = $1.strip
350 authors_by_commit[identifier] = $1.strip
347 elsif line =~ /^\t(.*)/
351 elsif line =~ /^\t(.*)/
348 blame.add_line($1, Revision.new(
352 blame.add_line($1, Revision.new(
349 :identifier => identifier,
353 :identifier => identifier,
350 :revision => identifier,
354 :revision => identifier,
351 :scmid => identifier,
355 :scmid => identifier,
352 :author => authors_by_commit[identifier]
356 :author => authors_by_commit[identifier]
353 ))
357 ))
354 identifier = ''
358 identifier = ''
355 author = ''
359 author = ''
356 end
360 end
357 end
361 end
358 blame
362 blame
359 rescue ScmCommandAborted
363 rescue ScmCommandAborted
360 nil
364 nil
361 end
365 end
362
366
363 def cat(path, identifier=nil)
367 def cat(path, identifier=nil)
364 if identifier.nil?
368 if identifier.nil?
365 identifier = 'HEAD'
369 identifier = 'HEAD'
366 end
370 end
367 cmd_args = %w|show --no-color|
371 cmd_args = %w|show --no-color|
368 cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
372 cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
369 cat = nil
373 cat = nil
370 git_cmd(cmd_args) do |io|
374 git_cmd(cmd_args) do |io|
371 io.binmode
375 io.binmode
372 cat = io.read
376 cat = io.read
373 end
377 end
374 cat
378 cat
375 rescue ScmCommandAborted
379 rescue ScmCommandAborted
376 nil
380 nil
377 end
381 end
378
382
379 class Revision < Redmine::Scm::Adapters::Revision
383 class Revision < Redmine::Scm::Adapters::Revision
380 # Returns the readable identifier
384 # Returns the readable identifier
381 def format_identifier
385 def format_identifier
382 identifier[0,8]
386 identifier[0,8]
383 end
387 end
384 end
388 end
385
389
386 def git_cmd(args, &block)
390 def git_cmd(args, options = {}, &block)
387 repo_path = root_url || url
391 repo_path = root_url || url
388 full_args = ['--git-dir', repo_path]
392 full_args = ['--git-dir', repo_path]
389 if self.class.client_version_above?([1, 7, 2])
393 if self.class.client_version_above?([1, 7, 2])
390 full_args << '-c' << 'core.quotepath=false'
394 full_args << '-c' << 'core.quotepath=false'
391 full_args << '-c' << 'log.decorate=no'
395 full_args << '-c' << 'log.decorate=no'
392 end
396 end
393 full_args += args
397 full_args += args
394 ret = shellout(
398 ret = shellout(
395 self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
399 self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
400 options,
396 &block
401 &block
397 )
402 )
398 if $? && $?.exitstatus != 0
403 if $? && $?.exitstatus != 0
399 raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
404 raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
400 end
405 end
401 ret
406 ret
402 end
407 end
403 private :git_cmd
408 private :git_cmd
404 end
409 end
405 end
410 end
406 end
411 end
407 end
412 end
General Comments 0
You need to be logged in to leave comments. Login now