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