Mercurial > hg > soundsoftware-site
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 |