##// END OF EJS Templates
scm: git: switch "-c core.quotepath=false" in git version above 1.7.2 or not (#5251)....
Toshi MARUYAMA -
r4949:16be7f2e3009
parent child
Show More
@@ -1,352 +1,355
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
24
25 SCM_GIT_REPORT_LAST_COMMIT = true
25 SCM_GIT_REPORT_LAST_COMMIT = true
26
26
27 # Git executable name
27 # Git executable name
28 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
28 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
29
29
30 # raised if scm command exited with error, e.g. unknown revision.
30 # raised if scm command exited with error, e.g. unknown revision.
31 class ScmCommandAborted < CommandFailed; end
31 class ScmCommandAborted < CommandFailed; end
32
32
33 class << self
33 class << self
34 def client_command
34 def client_command
35 @@bin ||= GIT_BIN
35 @@bin ||= GIT_BIN
36 end
36 end
37
37
38 def sq_bin
38 def sq_bin
39 @@sq_bin ||= shell_quote(GIT_BIN)
39 @@sq_bin ||= shell_quote(GIT_BIN)
40 end
40 end
41
41
42 def client_version
42 def client_version
43 @@client_version ||= (scm_command_version || [])
43 @@client_version ||= (scm_command_version || [])
44 end
44 end
45
45
46 def client_available
46 def client_available
47 !client_version.empty?
47 !client_version.empty?
48 end
48 end
49
49
50 def scm_command_version
50 def scm_command_version
51 scm_version = scm_version_from_command_line.dup
51 scm_version = scm_version_from_command_line.dup
52 if scm_version.respond_to?(:force_encoding)
52 if scm_version.respond_to?(:force_encoding)
53 scm_version.force_encoding('ASCII-8BIT')
53 scm_version.force_encoding('ASCII-8BIT')
54 end
54 end
55 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
55 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
56 m[2].scan(%r{\d+}).collect(&:to_i)
56 m[2].scan(%r{\d+}).collect(&:to_i)
57 end
57 end
58 end
58 end
59
59
60 def scm_version_from_command_line
60 def scm_version_from_command_line
61 shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
61 shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
62 end
62 end
63 end
63 end
64
64
65 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
65 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
66 super
66 super
67 @path_encoding = path_encoding || 'UTF-8'
67 @path_encoding = path_encoding || 'UTF-8'
68 @flag_report_last_commit = SCM_GIT_REPORT_LAST_COMMIT
68 @flag_report_last_commit = SCM_GIT_REPORT_LAST_COMMIT
69 end
69 end
70
70
71 def info
71 def info
72 begin
72 begin
73 Info.new(:root_url => url, :lastrev => lastrev('',nil))
73 Info.new(:root_url => url, :lastrev => lastrev('',nil))
74 rescue
74 rescue
75 nil
75 nil
76 end
76 end
77 end
77 end
78
78
79 def branches
79 def branches
80 return @branches if @branches
80 return @branches if @branches
81 @branches = []
81 @branches = []
82 cmd_args = %w|branch --no-color|
82 cmd_args = %w|branch --no-color|
83 scm_cmd(*cmd_args) do |io|
83 scm_cmd(*cmd_args) do |io|
84 io.each_line do |line|
84 io.each_line do |line|
85 @branches << line.match('\s*\*?\s*(.*)$')[1]
85 @branches << line.match('\s*\*?\s*(.*)$')[1]
86 end
86 end
87 end
87 end
88 @branches.sort!
88 @branches.sort!
89 rescue ScmCommandAborted
89 rescue ScmCommandAborted
90 nil
90 nil
91 end
91 end
92
92
93 def tags
93 def tags
94 return @tags if @tags
94 return @tags if @tags
95 cmd_args = %w|tag|
95 cmd_args = %w|tag|
96 scm_cmd(*cmd_args) do |io|
96 scm_cmd(*cmd_args) do |io|
97 @tags = io.readlines.sort!.map{|t| t.strip}
97 @tags = io.readlines.sort!.map{|t| t.strip}
98 end
98 end
99 rescue ScmCommandAborted
99 rescue ScmCommandAborted
100 nil
100 nil
101 end
101 end
102
102
103 def default_branch
103 def default_branch
104 branches.include?('master') ? 'master' : branches.first
104 branches.include?('master') ? 'master' : branches.first
105 end
105 end
106
106
107 def entries(path=nil, identifier=nil)
107 def entries(path=nil, identifier=nil)
108 path ||= ''
108 path ||= ''
109 p = scm_iconv(@path_encoding, 'UTF-8', path)
109 p = scm_iconv(@path_encoding, 'UTF-8', path)
110 entries = Entries.new
110 entries = Entries.new
111 cmd_args = %w|ls-tree -l|
111 cmd_args = %w|ls-tree -l|
112 cmd_args << "HEAD:#{p}" if identifier.nil?
112 cmd_args << "HEAD:#{p}" if identifier.nil?
113 cmd_args << "#{identifier}:#{p}" if identifier
113 cmd_args << "#{identifier}:#{p}" if identifier
114 scm_cmd(*cmd_args) do |io|
114 scm_cmd(*cmd_args) do |io|
115 io.each_line do |line|
115 io.each_line do |line|
116 e = line.chomp.to_s
116 e = line.chomp.to_s
117 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
117 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
118 type = $1
118 type = $1
119 sha = $2
119 sha = $2
120 size = $3
120 size = $3
121 name = $4
121 name = $4
122 if name.respond_to?(:force_encoding)
122 if name.respond_to?(:force_encoding)
123 name.force_encoding(@path_encoding)
123 name.force_encoding(@path_encoding)
124 end
124 end
125 full_path = p.empty? ? name : "#{p}/#{name}"
125 full_path = p.empty? ? name : "#{p}/#{name}"
126 n = scm_iconv('UTF-8', @path_encoding, name)
126 n = scm_iconv('UTF-8', @path_encoding, name)
127 full_p = scm_iconv('UTF-8', @path_encoding, full_path)
127 full_p = scm_iconv('UTF-8', @path_encoding, full_path)
128 entries << Entry.new({:name => n,
128 entries << Entry.new({:name => n,
129 :path => full_p,
129 :path => full_p,
130 :kind => (type == "tree") ? 'dir' : 'file',
130 :kind => (type == "tree") ? 'dir' : 'file',
131 :size => (type == "tree") ? nil : size,
131 :size => (type == "tree") ? nil : size,
132 :lastrev => @flag_report_last_commit ? lastrev(full_path, identifier) : Revision.new
132 :lastrev => @flag_report_last_commit ? lastrev(full_path, identifier) : Revision.new
133 }) unless entries.detect{|entry| entry.name == name}
133 }) unless entries.detect{|entry| entry.name == name}
134 end
134 end
135 end
135 end
136 end
136 end
137 entries.sort_by_name
137 entries.sort_by_name
138 rescue ScmCommandAborted
138 rescue ScmCommandAborted
139 nil
139 nil
140 end
140 end
141
141
142 def lastrev(path, rev)
142 def lastrev(path, rev)
143 return nil if path.nil?
143 return nil if path.nil?
144 cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
144 cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
145 cmd_args << rev if rev
145 cmd_args << rev if rev
146 cmd_args << "--" << path unless path.empty?
146 cmd_args << "--" << path unless path.empty?
147 lines = []
147 lines = []
148 scm_cmd(*cmd_args) { |io| lines = io.readlines }
148 scm_cmd(*cmd_args) { |io| lines = io.readlines }
149 begin
149 begin
150 id = lines[0].split[1]
150 id = lines[0].split[1]
151 author = lines[1].match('Author:\s+(.*)$')[1]
151 author = lines[1].match('Author:\s+(.*)$')[1]
152 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
152 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
153
153
154 Revision.new({
154 Revision.new({
155 :identifier => id,
155 :identifier => id,
156 :scmid => id,
156 :scmid => id,
157 :author => author,
157 :author => author,
158 :time => time,
158 :time => time,
159 :message => nil,
159 :message => nil,
160 :paths => nil
160 :paths => nil
161 })
161 })
162 rescue NoMethodError => e
162 rescue NoMethodError => e
163 logger.error("The revision '#{path}' has a wrong format")
163 logger.error("The revision '#{path}' has a wrong format")
164 return nil
164 return nil
165 end
165 end
166 rescue ScmCommandAborted
166 rescue ScmCommandAborted
167 nil
167 nil
168 end
168 end
169
169
170 def revisions(path, identifier_from, identifier_to, options={})
170 def revisions(path, identifier_from, identifier_to, options={})
171 revisions = Revisions.new
171 revisions = Revisions.new
172 cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller|
172 cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller|
173 cmd_args << "--reverse" if options[:reverse]
173 cmd_args << "--reverse" if options[:reverse]
174 cmd_args << "--all" if options[:all]
174 cmd_args << "--all" if options[:all]
175 cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
175 cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
176 from_to = ""
176 from_to = ""
177 from_to << "#{identifier_from}.." if identifier_from
177 from_to << "#{identifier_from}.." if identifier_from
178 from_to << "#{identifier_to}" if identifier_to
178 from_to << "#{identifier_to}" if identifier_to
179 cmd_args << from_to if !from_to.empty?
179 cmd_args << from_to if !from_to.empty?
180 cmd_args << "--since=#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}" if options[:since]
180 cmd_args << "--since=#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}" if options[:since]
181 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
181 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
182
182
183 scm_cmd *cmd_args do |io|
183 scm_cmd *cmd_args do |io|
184 files=[]
184 files=[]
185 changeset = {}
185 changeset = {}
186 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
186 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
187
187
188 io.each_line do |line|
188 io.each_line do |line|
189 if line =~ /^commit ([0-9a-f]{40})$/
189 if line =~ /^commit ([0-9a-f]{40})$/
190 key = "commit"
190 key = "commit"
191 value = $1
191 value = $1
192 if (parsing_descr == 1 || parsing_descr == 2)
192 if (parsing_descr == 1 || parsing_descr == 2)
193 parsing_descr = 0
193 parsing_descr = 0
194 revision = Revision.new({
194 revision = Revision.new({
195 :identifier => changeset[:commit],
195 :identifier => changeset[:commit],
196 :scmid => changeset[:commit],
196 :scmid => changeset[:commit],
197 :author => changeset[:author],
197 :author => changeset[:author],
198 :time => Time.parse(changeset[:date]),
198 :time => Time.parse(changeset[:date]),
199 :message => changeset[:description],
199 :message => changeset[:description],
200 :paths => files
200 :paths => files
201 })
201 })
202 if block_given?
202 if block_given?
203 yield revision
203 yield revision
204 else
204 else
205 revisions << revision
205 revisions << revision
206 end
206 end
207 changeset = {}
207 changeset = {}
208 files = []
208 files = []
209 end
209 end
210 changeset[:commit] = $1
210 changeset[:commit] = $1
211 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
211 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
212 key = $1
212 key = $1
213 value = $2
213 value = $2
214 if key == "Author"
214 if key == "Author"
215 changeset[:author] = value
215 changeset[:author] = value
216 elsif key == "CommitDate"
216 elsif key == "CommitDate"
217 changeset[:date] = value
217 changeset[:date] = value
218 end
218 end
219 elsif (parsing_descr == 0) && line.chomp.to_s == ""
219 elsif (parsing_descr == 0) && line.chomp.to_s == ""
220 parsing_descr = 1
220 parsing_descr = 1
221 changeset[:description] = ""
221 changeset[:description] = ""
222 elsif (parsing_descr == 1 || parsing_descr == 2) \
222 elsif (parsing_descr == 1 || parsing_descr == 2) \
223 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
223 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
224 parsing_descr = 2
224 parsing_descr = 2
225 fileaction = $1
225 fileaction = $1
226 filepath = $2
226 filepath = $2
227 p = scm_iconv('UTF-8', @path_encoding, filepath)
227 p = scm_iconv('UTF-8', @path_encoding, filepath)
228 files << {:action => fileaction, :path => p}
228 files << {:action => fileaction, :path => p}
229 elsif (parsing_descr == 1 || parsing_descr == 2) \
229 elsif (parsing_descr == 1 || parsing_descr == 2) \
230 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
230 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
231 parsing_descr = 2
231 parsing_descr = 2
232 fileaction = $1
232 fileaction = $1
233 filepath = $3
233 filepath = $3
234 p = scm_iconv('UTF-8', @path_encoding, filepath)
234 p = scm_iconv('UTF-8', @path_encoding, filepath)
235 files << {:action => fileaction, :path => p}
235 files << {:action => fileaction, :path => p}
236 elsif (parsing_descr == 1) && line.chomp.to_s == ""
236 elsif (parsing_descr == 1) && line.chomp.to_s == ""
237 parsing_descr = 2
237 parsing_descr = 2
238 elsif (parsing_descr == 1)
238 elsif (parsing_descr == 1)
239 changeset[:description] << line[4..-1]
239 changeset[:description] << line[4..-1]
240 end
240 end
241 end
241 end
242
242
243 if changeset[:commit]
243 if changeset[:commit]
244 revision = Revision.new({
244 revision = Revision.new({
245 :identifier => changeset[:commit],
245 :identifier => changeset[:commit],
246 :scmid => changeset[:commit],
246 :scmid => changeset[:commit],
247 :author => changeset[:author],
247 :author => changeset[:author],
248 :time => Time.parse(changeset[:date]),
248 :time => Time.parse(changeset[:date]),
249 :message => changeset[:description],
249 :message => changeset[:description],
250 :paths => files
250 :paths => files
251 })
251 })
252
252
253 if block_given?
253 if block_given?
254 yield revision
254 yield revision
255 else
255 else
256 revisions << revision
256 revisions << revision
257 end
257 end
258 end
258 end
259 end
259 end
260 revisions
260 revisions
261 rescue ScmCommandAborted
261 rescue ScmCommandAborted
262 revisions
262 revisions
263 end
263 end
264
264
265 def diff(path, identifier_from, identifier_to=nil)
265 def diff(path, identifier_from, identifier_to=nil)
266 path ||= ''
266 path ||= ''
267 cmd_args = []
267 cmd_args = []
268 if identifier_to
268 if identifier_to
269 cmd_args << "diff" << "--no-color" << identifier_to << identifier_from
269 cmd_args << "diff" << "--no-color" << identifier_to << identifier_from
270 else
270 else
271 cmd_args << "show" << "--no-color" << identifier_from
271 cmd_args << "show" << "--no-color" << identifier_from
272 end
272 end
273 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
273 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
274 diff = []
274 diff = []
275 scm_cmd *cmd_args do |io|
275 scm_cmd *cmd_args do |io|
276 io.each_line do |line|
276 io.each_line do |line|
277 diff << line
277 diff << line
278 end
278 end
279 end
279 end
280 diff
280 diff
281 rescue ScmCommandAborted
281 rescue ScmCommandAborted
282 nil
282 nil
283 end
283 end
284
284
285 def annotate(path, identifier=nil)
285 def annotate(path, identifier=nil)
286 identifier = 'HEAD' if identifier.blank?
286 identifier = 'HEAD' if identifier.blank?
287 cmd_args = %w|blame|
287 cmd_args = %w|blame|
288 cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
288 cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
289 blame = Annotate.new
289 blame = Annotate.new
290 content = nil
290 content = nil
291 scm_cmd(*cmd_args) { |io| io.binmode; content = io.read }
291 scm_cmd(*cmd_args) { |io| io.binmode; content = io.read }
292 # git annotates binary files
292 # git annotates binary files
293 return nil if content.is_binary_data?
293 return nil if content.is_binary_data?
294 identifier = ''
294 identifier = ''
295 # git shows commit author on the first occurrence only
295 # git shows commit author on the first occurrence only
296 authors_by_commit = {}
296 authors_by_commit = {}
297 content.split("\n").each do |line|
297 content.split("\n").each do |line|
298 if line =~ /^([0-9a-f]{39,40})\s.*/
298 if line =~ /^([0-9a-f]{39,40})\s.*/
299 identifier = $1
299 identifier = $1
300 elsif line =~ /^author (.+)/
300 elsif line =~ /^author (.+)/
301 authors_by_commit[identifier] = $1.strip
301 authors_by_commit[identifier] = $1.strip
302 elsif line =~ /^\t(.*)/
302 elsif line =~ /^\t(.*)/
303 blame.add_line($1, Revision.new(
303 blame.add_line($1, Revision.new(
304 :identifier => identifier,
304 :identifier => identifier,
305 :author => authors_by_commit[identifier]))
305 :author => authors_by_commit[identifier]))
306 identifier = ''
306 identifier = ''
307 author = ''
307 author = ''
308 end
308 end
309 end
309 end
310 blame
310 blame
311 rescue ScmCommandAborted
311 rescue ScmCommandAborted
312 nil
312 nil
313 end
313 end
314
314
315 def cat(path, identifier=nil)
315 def cat(path, identifier=nil)
316 if identifier.nil?
316 if identifier.nil?
317 identifier = 'HEAD'
317 identifier = 'HEAD'
318 end
318 end
319 cmd_args = %w|show --no-color|
319 cmd_args = %w|show --no-color|
320 cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
320 cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
321 cat = nil
321 cat = nil
322 scm_cmd(*cmd_args) do |io|
322 scm_cmd(*cmd_args) do |io|
323 io.binmode
323 io.binmode
324 cat = io.read
324 cat = io.read
325 end
325 end
326 cat
326 cat
327 rescue ScmCommandAborted
327 rescue ScmCommandAborted
328 nil
328 nil
329 end
329 end
330
330
331 class Revision < Redmine::Scm::Adapters::Revision
331 class Revision < Redmine::Scm::Adapters::Revision
332 # Returns the readable identifier
332 # Returns the readable identifier
333 def format_identifier
333 def format_identifier
334 identifier[0,8]
334 identifier[0,8]
335 end
335 end
336 end
336 end
337
337
338 def scm_cmd(*args, &block)
338 def scm_cmd(*args, &block)
339 repo_path = root_url || url
339 repo_path = root_url || url
340 full_args = [GIT_BIN, '--git-dir', repo_path, '-c', 'core.quotepath=false']
340 full_args = [GIT_BIN, '--git-dir', repo_path]
341 if self.class.client_version_above?([1, 7, 2])
342 full_args << '-c' << 'core.quotepath=false'
343 end
341 full_args += args
344 full_args += args
342 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
345 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
343 if $? && $?.exitstatus != 0
346 if $? && $?.exitstatus != 0
344 raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
347 raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
345 end
348 end
346 ret
349 ret
347 end
350 end
348 private :scm_cmd
351 private :scm_cmd
349 end
352 end
350 end
353 end
351 end
354 end
352 end
355 end
General Comments 0
You need to be logged in to leave comments. Login now