comparison lib/redmine/scm/adapters/.svn/text-base/git_adapter.rb.svn-base @ 441:cbce1fd3b1b7 redmine-1.2

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