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