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