##// END OF EJS Templates
scm: git: code clean up adapter....
Toshi MARUYAMA -
r4914:a1364ed2b847
parent child
Show More
@@ -1,334 +1,332
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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
25 SCM_GIT_REPORT_LAST_COMMIT = true
25 SCM_GIT_REPORT_LAST_COMMIT = true
26
26
27 # Git executable name
27 # Git executable name
28 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
28 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
29
29
30 # raised if scm command exited with error, e.g. unknown revision.
30 # raised if scm command exited with error, e.g. unknown revision.
31 class ScmCommandAborted < CommandFailed; end
31 class ScmCommandAborted < CommandFailed; end
32
32
33 class << self
33 class << self
34 def client_command
34 def client_command
35 @@bin ||= GIT_BIN
35 @@bin ||= GIT_BIN
36 end
36 end
37
37
38 def sq_bin
38 def sq_bin
39 @@sq_bin ||= shell_quote(GIT_BIN)
39 @@sq_bin ||= shell_quote(GIT_BIN)
40 end
40 end
41
41
42 def client_version
42 def client_version
43 @@client_version ||= (scm_command_version || [])
43 @@client_version ||= (scm_command_version || [])
44 end
44 end
45
45
46 def client_available
46 def client_available
47 !client_version.empty?
47 !client_version.empty?
48 end
48 end
49
49
50 def scm_command_version
50 def scm_command_version
51 scm_version = scm_version_from_command_line.dup
51 scm_version = scm_version_from_command_line.dup
52 if scm_version.respond_to?(:force_encoding)
52 if scm_version.respond_to?(:force_encoding)
53 scm_version.force_encoding('ASCII-8BIT')
53 scm_version.force_encoding('ASCII-8BIT')
54 end
54 end
55 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
55 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
56 m[2].scan(%r{\d+}).collect(&:to_i)
56 m[2].scan(%r{\d+}).collect(&:to_i)
57 end
57 end
58 end
58 end
59
59
60 def scm_version_from_command_line
60 def scm_version_from_command_line
61 shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
61 shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
62 end
62 end
63 end
63 end
64
64
65 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
65 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
66 super
66 super
67 @flag_report_last_commit = SCM_GIT_REPORT_LAST_COMMIT
67 @flag_report_last_commit = SCM_GIT_REPORT_LAST_COMMIT
68 end
68 end
69
69
70 def info
70 def info
71 begin
71 begin
72 Info.new(:root_url => url, :lastrev => lastrev('',nil))
72 Info.new(:root_url => url, :lastrev => lastrev('',nil))
73 rescue
73 rescue
74 nil
74 nil
75 end
75 end
76 end
76 end
77
77
78 def branches
78 def branches
79 return @branches if @branches
79 return @branches if @branches
80 @branches = []
80 @branches = []
81 cmd = "#{self.class.sq_bin} --git-dir #{target('')} branch --no-color"
81 cmd = "#{self.class.sq_bin} --git-dir #{target('')} branch --no-color"
82 shellout(cmd) do |io|
82 shellout(cmd) do |io|
83 io.each_line do |line|
83 io.each_line do |line|
84 @branches << line.match('\s*\*?\s*(.*)$')[1]
84 @branches << line.match('\s*\*?\s*(.*)$')[1]
85 end
85 end
86 end
86 end
87 @branches.sort!
87 @branches.sort!
88 end
88 end
89
89
90 def tags
90 def tags
91 return @tags if @tags
91 return @tags if @tags
92 cmd = "#{self.class.sq_bin} --git-dir #{target('')} tag"
92 cmd = "#{self.class.sq_bin} --git-dir #{target('')} tag"
93 shellout(cmd) do |io|
93 shellout(cmd) do |io|
94 @tags = io.readlines.sort!.map{|t| t.strip}
94 @tags = io.readlines.sort!.map{|t| t.strip}
95 end
95 end
96 end
96 end
97
97
98 def default_branch
98 def default_branch
99 branches.include?('master') ? 'master' : branches.first
99 branches.include?('master') ? 'master' : branches.first
100 end
100 end
101
101
102 def entries(path=nil, identifier=nil)
102 def entries(path=nil, identifier=nil)
103 path ||= ''
103 path ||= ''
104 entries = Entries.new
104 entries = Entries.new
105 cmd = "#{self.class.sq_bin} --git-dir #{target('')} ls-tree -l "
105 cmd = "#{self.class.sq_bin} --git-dir #{target('')} ls-tree -l "
106 cmd << shell_quote("HEAD:" + path) if identifier.nil?
106 cmd << shell_quote("HEAD:" + path) if identifier.nil?
107 cmd << shell_quote(identifier + ":" + path) if identifier
107 cmd << shell_quote(identifier + ":" + path) if identifier
108 shellout(cmd) do |io|
108 shellout(cmd) do |io|
109 io.each_line do |line|
109 io.each_line do |line|
110 e = line.chomp.to_s
110 e = line.chomp.to_s
111 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
111 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
112 type = $1
112 type = $1
113 sha = $2
113 sha = $2
114 size = $3
114 size = $3
115 name = $4
115 name = $4
116 full_path = path.empty? ? name : "#{path}/#{name}"
116 full_path = path.empty? ? name : "#{path}/#{name}"
117 entries << Entry.new({:name => name,
117 entries << Entry.new({:name => name,
118 :path => full_path,
118 :path => full_path,
119 :kind => (type == "tree") ? 'dir' : 'file',
119 :kind => (type == "tree") ? 'dir' : 'file',
120 :size => (type == "tree") ? nil : size,
120 :size => (type == "tree") ? nil : size,
121 :lastrev => @flag_report_last_commit ? lastrev(full_path,identifier) : Revision.new
121 :lastrev => @flag_report_last_commit ? lastrev(full_path,identifier) : Revision.new
122 }) unless entries.detect{|entry| entry.name == name}
122 }) unless entries.detect{|entry| entry.name == name}
123 end
123 end
124 end
124 end
125 end
125 end
126 return nil if $? && $?.exitstatus != 0
126 return nil if $? && $?.exitstatus != 0
127 entries.sort_by_name
127 entries.sort_by_name
128 end
128 end
129
129
130 def lastrev(path, rev)
130 def lastrev(path, rev)
131 return nil if path.nil?
131 return nil if path.nil?
132 cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
132 cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
133 cmd_args << rev if rev
133 cmd_args << rev if rev
134 cmd_args << "--" << path unless path.empty?
134 cmd_args << "--" << path unless path.empty?
135 lines = []
135 lines = []
136 scm_cmd(*cmd_args) { |io| lines = io.readlines }
136 scm_cmd(*cmd_args) { |io| lines = io.readlines }
137 begin
137 begin
138 id = lines[0].split[1]
138 id = lines[0].split[1]
139 author = lines[1].match('Author:\s+(.*)$')[1]
139 author = lines[1].match('Author:\s+(.*)$')[1]
140 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
140 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
141
141
142 Revision.new({
142 Revision.new({
143 :identifier => id,
143 :identifier => id,
144 :scmid => id,
144 :scmid => id,
145 :author => author,
145 :author => author,
146 :time => time,
146 :time => time,
147 :message => nil,
147 :message => nil,
148 :paths => nil
148 :paths => nil
149 })
149 })
150 rescue NoMethodError => e
150 rescue NoMethodError => e
151 logger.error("The revision '#{path}' has a wrong format")
151 logger.error("The revision '#{path}' has a wrong format")
152 return nil
152 return nil
153 end
153 end
154 rescue ScmCommandAborted
154 rescue ScmCommandAborted
155 nil
155 nil
156 end
156 end
157
157
158 def revisions(path, identifier_from, identifier_to, options={})
158 def revisions(path, identifier_from, identifier_to, options={})
159 revisions = Revisions.new
159 revisions = Revisions.new
160 cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller|
160 cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller|
161 cmd_args << "--reverse" if options[:reverse]
161 cmd_args << "--reverse" if options[:reverse]
162 cmd_args << "--all" if options[:all]
162 cmd_args << "--all" if options[:all]
163 cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
163 cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
164 from_to = ""
164 from_to = ""
165 from_to << "#{identifier_from}.." if identifier_from
165 from_to << "#{identifier_from}.." if identifier_from
166 from_to << "#{identifier_to}" if identifier_to
166 from_to << "#{identifier_to}" if identifier_to
167 cmd_args << from_to if !from_to.empty?
167 cmd_args << from_to if !from_to.empty?
168 cmd_args << "--since=#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}" if options[:since]
168 cmd_args << "--since=#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}" if options[:since]
169 cmd_args << "--" << path if path && !path.empty?
169 cmd_args << "--" << path if path && !path.empty?
170
170
171 scm_cmd *cmd_args do |io|
171 scm_cmd *cmd_args do |io|
172 files=[]
172 files=[]
173 changeset = {}
173 changeset = {}
174 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
174 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
175 revno = 1
176
175
177 io.each_line do |line|
176 io.each_line do |line|
178 if line =~ /^commit ([0-9a-f]{40})$/
177 if line =~ /^commit ([0-9a-f]{40})$/
179 key = "commit"
178 key = "commit"
180 value = $1
179 value = $1
181 if (parsing_descr == 1 || parsing_descr == 2)
180 if (parsing_descr == 1 || parsing_descr == 2)
182 parsing_descr = 0
181 parsing_descr = 0
183 revision = Revision.new({
182 revision = Revision.new({
184 :identifier => changeset[:commit],
183 :identifier => changeset[:commit],
185 :scmid => changeset[:commit],
184 :scmid => changeset[:commit],
186 :author => changeset[:author],
185 :author => changeset[:author],
187 :time => Time.parse(changeset[:date]),
186 :time => Time.parse(changeset[:date]),
188 :message => changeset[:description],
187 :message => changeset[:description],
189 :paths => files
188 :paths => files
190 })
189 })
191 if block_given?
190 if block_given?
192 yield revision
191 yield revision
193 else
192 else
194 revisions << revision
193 revisions << revision
195 end
194 end
196 changeset = {}
195 changeset = {}
197 files = []
196 files = []
198 revno = revno + 1
199 end
197 end
200 changeset[:commit] = $1
198 changeset[:commit] = $1
201 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
199 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
202 key = $1
200 key = $1
203 value = $2
201 value = $2
204 if key == "Author"
202 if key == "Author"
205 changeset[:author] = value
203 changeset[:author] = value
206 elsif key == "CommitDate"
204 elsif key == "CommitDate"
207 changeset[:date] = value
205 changeset[:date] = value
208 end
206 end
209 elsif (parsing_descr == 0) && line.chomp.to_s == ""
207 elsif (parsing_descr == 0) && line.chomp.to_s == ""
210 parsing_descr = 1
208 parsing_descr = 1
211 changeset[:description] = ""
209 changeset[:description] = ""
212 elsif (parsing_descr == 1 || parsing_descr == 2) \
210 elsif (parsing_descr == 1 || parsing_descr == 2) \
213 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
211 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
214 parsing_descr = 2
212 parsing_descr = 2
215 fileaction = $1
213 fileaction = $1
216 filepath = $2
214 filepath = $2
217 files << {:action => fileaction, :path => filepath}
215 files << {:action => fileaction, :path => filepath}
218 elsif (parsing_descr == 1 || parsing_descr == 2) \
216 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(.+)$/
217 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
220 parsing_descr = 2
218 parsing_descr = 2
221 fileaction = $1
219 fileaction = $1
222 filepath = $3
220 filepath = $3
223 files << {:action => fileaction, :path => filepath}
221 files << {:action => fileaction, :path => filepath}
224 elsif (parsing_descr == 1) && line.chomp.to_s == ""
222 elsif (parsing_descr == 1) && line.chomp.to_s == ""
225 parsing_descr = 2
223 parsing_descr = 2
226 elsif (parsing_descr == 1)
224 elsif (parsing_descr == 1)
227 changeset[:description] << line[4..-1]
225 changeset[:description] << line[4..-1]
228 end
226 end
229 end
227 end
230
228
231 if changeset[:commit]
229 if changeset[:commit]
232 revision = Revision.new({
230 revision = Revision.new({
233 :identifier => changeset[:commit],
231 :identifier => changeset[:commit],
234 :scmid => changeset[:commit],
232 :scmid => changeset[:commit],
235 :author => changeset[:author],
233 :author => changeset[:author],
236 :time => Time.parse(changeset[:date]),
234 :time => Time.parse(changeset[:date]),
237 :message => changeset[:description],
235 :message => changeset[:description],
238 :paths => files
236 :paths => files
239 })
237 })
240
238
241 if block_given?
239 if block_given?
242 yield revision
240 yield revision
243 else
241 else
244 revisions << revision
242 revisions << revision
245 end
243 end
246 end
244 end
247 end
245 end
248 revisions
246 revisions
249 rescue ScmCommandAborted
247 rescue ScmCommandAborted
250 revisions
248 revisions
251 end
249 end
252
250
253 def diff(path, identifier_from, identifier_to=nil)
251 def diff(path, identifier_from, identifier_to=nil)
254 path ||= ''
252 path ||= ''
255
253
256 if identifier_to
254 if identifier_to
257 cmd = "#{self.class.sq_bin} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
255 cmd = "#{self.class.sq_bin} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
258 else
256 else
259 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
257 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
260 end
258 end
261
259
262 cmd << " -- #{shell_quote path}" unless path.empty?
260 cmd << " -- #{shell_quote path}" unless path.empty?
263 diff = []
261 diff = []
264 shellout(cmd) do |io|
262 shellout(cmd) do |io|
265 io.each_line do |line|
263 io.each_line do |line|
266 diff << line
264 diff << line
267 end
265 end
268 end
266 end
269 return nil if $? && $?.exitstatus != 0
267 return nil if $? && $?.exitstatus != 0
270 diff
268 diff
271 end
269 end
272
270
273 def annotate(path, identifier=nil)
271 def annotate(path, identifier=nil)
274 identifier = 'HEAD' if identifier.blank?
272 identifier = 'HEAD' if identifier.blank?
275 cmd = "#{self.class.sq_bin} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
273 cmd = "#{self.class.sq_bin} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
276 blame = Annotate.new
274 blame = Annotate.new
277 content = nil
275 content = nil
278 shellout(cmd) { |io| io.binmode; content = io.read }
276 shellout(cmd) { |io| io.binmode; content = io.read }
279 return nil if $? && $?.exitstatus != 0
277 return nil if $? && $?.exitstatus != 0
280 # git annotates binary files
278 # git annotates binary files
281 return nil if content.is_binary_data?
279 return nil if content.is_binary_data?
282 identifier = ''
280 identifier = ''
283 # git shows commit author on the first occurrence only
281 # git shows commit author on the first occurrence only
284 authors_by_commit = {}
282 authors_by_commit = {}
285 content.split("\n").each do |line|
283 content.split("\n").each do |line|
286 if line =~ /^([0-9a-f]{39,40})\s.*/
284 if line =~ /^([0-9a-f]{39,40})\s.*/
287 identifier = $1
285 identifier = $1
288 elsif line =~ /^author (.+)/
286 elsif line =~ /^author (.+)/
289 authors_by_commit[identifier] = $1.strip
287 authors_by_commit[identifier] = $1.strip
290 elsif line =~ /^\t(.*)/
288 elsif line =~ /^\t(.*)/
291 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
289 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
292 identifier = ''
290 identifier = ''
293 author = ''
291 author = ''
294 end
292 end
295 end
293 end
296 blame
294 blame
297 end
295 end
298
296
299 def cat(path, identifier=nil)
297 def cat(path, identifier=nil)
300 if identifier.nil?
298 if identifier.nil?
301 identifier = 'HEAD'
299 identifier = 'HEAD'
302 end
300 end
303 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
301 cmd = "#{self.class.sq_bin} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
304 cat = nil
302 cat = nil
305 shellout(cmd) do |io|
303 shellout(cmd) do |io|
306 io.binmode
304 io.binmode
307 cat = io.read
305 cat = io.read
308 end
306 end
309 return nil if $? && $?.exitstatus != 0
307 return nil if $? && $?.exitstatus != 0
310 cat
308 cat
311 end
309 end
312
310
313 class Revision < Redmine::Scm::Adapters::Revision
311 class Revision < Redmine::Scm::Adapters::Revision
314 # Returns the readable identifier
312 # Returns the readable identifier
315 def format_identifier
313 def format_identifier
316 identifier[0,8]
314 identifier[0,8]
317 end
315 end
318 end
316 end
319
317
320 def scm_cmd(*args, &block)
318 def scm_cmd(*args, &block)
321 repo_path = root_url || url
319 repo_path = root_url || url
322 full_args = [GIT_BIN, '--git-dir', repo_path]
320 full_args = [GIT_BIN, '--git-dir', repo_path]
323 full_args += args
321 full_args += args
324 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
322 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
325 if $? && $?.exitstatus != 0
323 if $? && $?.exitstatus != 0
326 raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
324 raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
327 end
325 end
328 ret
326 ret
329 end
327 end
330 private :scm_cmd
328 private :scm_cmd
331 end
329 end
332 end
330 end
333 end
331 end
334 end
332 end
General Comments 0
You need to be logged in to leave comments. Login now