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