##// END OF EJS Templates
scm: git: use the model value of whether reporting last commit in repository tree (#7047)....
Toshi MARUYAMA -
r5537:114e3f3b826c
parent child
Show More
@@ -1,167 +1,169
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 # Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
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/git_adapter'
19 19
20 20 class Repository::Git < Repository
21 21 attr_protected :root_url
22 22 validates_presence_of :url
23 23
24 24 def self.human_attribute_name(attribute_key_name)
25 25 attr_name = attribute_key_name
26 26 if attr_name == "url"
27 27 attr_name = "path_to_repository"
28 28 end
29 29 super(attr_name)
30 30 end
31 31
32 32 def self.scm_adapter_class
33 33 Redmine::Scm::Adapters::GitAdapter
34 34 end
35 35
36 36 def self.scm_name
37 37 'Git'
38 38 end
39 39
40 40 def report_last_commit
41 41 true
42 42 end
43 43
44 44 def supports_directory_revisions?
45 45 true
46 46 end
47 47
48 48 def repo_log_encoding
49 49 'UTF-8'
50 50 end
51 51
52 52 # Returns the identifier for the given git changeset
53 53 def self.changeset_identifier(changeset)
54 54 changeset.scmid
55 55 end
56 56
57 57 # Returns the readable identifier for the given git changeset
58 58 def self.format_changeset_identifier(changeset)
59 59 changeset.revision[0, 8]
60 60 end
61 61
62 62 def branches
63 63 scm.branches
64 64 end
65 65
66 66 def tags
67 67 scm.tags
68 68 end
69 69
70 70 def find_changeset_by_name(name)
71 71 return nil if name.nil? || name.empty?
72 72 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
73 73 return e if e
74 74 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
75 75 end
76 76
77 77 def entries(path=nil, identifier=nil)
78 scm.entries(path, identifier)
78 scm.entries(path,
79 identifier,
80 options = {:report_last_commit => report_last_commit})
79 81 end
80 82
81 83 # In Git and Mercurial, revisions are not in date order.
82 84 # Mercurial fixed issues.
83 85 # * Redmine Takes Too Long On Large Mercurial Repository
84 86 # http://www.redmine.org/issues/3449
85 87 # * Sorting for changesets might go wrong on Mercurial repos
86 88 # http://www.redmine.org/issues/3567
87 89 # Database revision column is text, so Redmine can not sort by revision.
88 90 # Mercurial has revision number, and revision number guarantees revision order.
89 91 # Mercurial adapter uses "hg log -r 0:tip --limit 10"
90 92 # to get limited revisions from old to new.
91 93 # And Mercurial model stored revisions ordered by database id in database.
92 94 # So, Mercurial can use correct order revisions.
93 95 #
94 96 # But, Git 1.7.3.4 does not support --reverse with -n or --skip.
95 97 #
96 98 # With SCM's that have a sequential commit numbering, redmine is able to be
97 99 # clever and only fetch changesets going forward from the most recent one
98 100 # it knows about.
99 101 # However, with git, you never know if people have merged
100 102 # commits into the middle of the repository history, so we should parse
101 103 # the entire log.
102 104 #
103 105 # Since it's way too slow for large repositories,
104 106 # we only parse 1 week before the last known commit.
105 107 #
106 108 # The repository can still be fully reloaded by calling #clear_changesets
107 109 # before fetching changesets (eg. for offline resync)
108 110 def fetch_changesets
109 111 c = changesets.find(:first, :order => 'committed_on DESC')
110 112 since = (c ? c.committed_on - 7.days : nil)
111 113
112 114 revisions = scm.revisions('', nil, nil, {:all => true, :since => since, :reverse => true})
113 115 return if revisions.nil? || revisions.empty?
114 116
115 117 recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
116 118
117 119 # Clean out revisions that are no longer in git
118 120 recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }}
119 121
120 122 # Subtract revisions that redmine already knows about
121 123 recent_revisions = recent_changesets.map{|c| c.scmid}
122 124 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
123 125
124 126 # Save the remaining ones to the database
125 127 unless revisions.nil?
126 128 revisions.each do |rev|
127 129 transaction do
128 130 save_revision(rev)
129 131 end
130 132 end
131 133 end
132 134 end
133 135
134 136 def save_revision(rev)
135 137 changeset = Changeset.new(
136 138 :repository => self,
137 139 :revision => rev.identifier,
138 140 :scmid => rev.scmid,
139 141 :committer => rev.author,
140 142 :committed_on => rev.time,
141 143 :comments => rev.message
142 144 )
143 145 if changeset.save
144 146 rev.paths.each do |file|
145 147 Change.create(
146 148 :changeset => changeset,
147 149 :action => file[:action],
148 150 :path => file[:path])
149 151 end
150 152 end
151 153 end
152 154 private :save_revision
153 155
154 156 def latest_changesets(path,rev,limit=10)
155 157 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
156 158 return [] if revisions.nil? || revisions.empty?
157 159
158 160 changesets.find(
159 161 :all,
160 162 :conditions => [
161 163 "scmid IN (?)",
162 164 revisions.map!{|c| c.scmid}
163 165 ],
164 166 :order => 'committed_on DESC'
165 167 )
166 168 end
167 169 end
@@ -1,382 +1,374
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 SCM_GIT_REPORT_LAST_COMMIT = true
26
27 25 # Git executable name
28 26 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
29 27
30 28 # raised if scm command exited with error, e.g. unknown revision.
31 29 class ScmCommandAborted < CommandFailed; end
32 30
33 31 class << self
34 32 def client_command
35 33 @@bin ||= GIT_BIN
36 34 end
37 35
38 36 def sq_bin
39 37 @@sq_bin ||= shell_quote(GIT_BIN)
40 38 end
41 39
42 40 def client_version
43 41 @@client_version ||= (scm_command_version || [])
44 42 end
45 43
46 44 def client_available
47 45 !client_version.empty?
48 46 end
49 47
50 48 def scm_command_version
51 49 scm_version = scm_version_from_command_line.dup
52 50 if scm_version.respond_to?(:force_encoding)
53 51 scm_version.force_encoding('ASCII-8BIT')
54 52 end
55 53 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
56 54 m[2].scan(%r{\d+}).collect(&:to_i)
57 55 end
58 56 end
59 57
60 58 def scm_version_from_command_line
61 59 shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
62 60 end
63 61 end
64 62
65 63 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
66 64 super
67 65 @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
68 @flag_report_last_commit = SCM_GIT_REPORT_LAST_COMMIT
69 66 end
70 67
71 68 def info
72 69 begin
73 70 Info.new(:root_url => url, :lastrev => lastrev('',nil))
74 71 rescue
75 72 nil
76 73 end
77 74 end
78 75
79 76 def branches
80 77 return @branches if @branches
81 78 @branches = []
82 79 cmd_args = %w|branch --no-color|
83 80 scm_cmd(*cmd_args) do |io|
84 81 io.each_line do |line|
85 82 @branches << line.match('\s*\*?\s*(.*)$')[1]
86 83 end
87 84 end
88 85 @branches.sort!
89 86 rescue ScmCommandAborted
90 87 nil
91 88 end
92 89
93 90 def tags
94 91 return @tags if @tags
95 92 cmd_args = %w|tag|
96 93 scm_cmd(*cmd_args) do |io|
97 94 @tags = io.readlines.sort!.map{|t| t.strip}
98 95 end
99 96 rescue ScmCommandAborted
100 97 nil
101 98 end
102 99
103 100 def default_branch
104 101 bras = self.branches
105 102 return nil if bras.nil?
106 103 bras.include?('master') ? 'master' : bras.first
107 104 end
108 105
109 106 def entry(path=nil, identifier=nil)
110 107 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
111 108 search_path = parts[0..-2].join('/')
112 109 search_name = parts[-1]
113 110 if search_path.blank? && search_name.blank?
114 111 # Root entry
115 112 Entry.new(:path => '', :kind => 'dir')
116 113 else
117 114 # Search for the entry in the parent directory
118 es = entries_git(search_path, identifier)
115 es = entries(search_path, identifier,
116 options = {:report_last_commit => false})
119 117 es ? es.detect {|e| e.name == search_name} : nil
120 118 end
121 119 end
122 120
123 121 def entries(path=nil, identifier=nil, options={})
124 entries_git(path, identifier,
125 {:report_last_commit => @flag_report_last_commit})
126 end
127
128 def entries_git(path=nil, identifier=nil, options={})
129 122 path ||= ''
130 123 p = scm_iconv(@path_encoding, 'UTF-8', path)
131 124 entries = Entries.new
132 125 cmd_args = %w|ls-tree -l|
133 126 cmd_args << "HEAD:#{p}" if identifier.nil?
134 127 cmd_args << "#{identifier}:#{p}" if identifier
135 128 scm_cmd(*cmd_args) do |io|
136 129 io.each_line do |line|
137 130 e = line.chomp.to_s
138 131 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
139 132 type = $1
140 133 sha = $2
141 134 size = $3
142 135 name = $4
143 136 if name.respond_to?(:force_encoding)
144 137 name.force_encoding(@path_encoding)
145 138 end
146 139 full_path = p.empty? ? name : "#{p}/#{name}"
147 140 n = scm_iconv('UTF-8', @path_encoding, name)
148 141 full_p = scm_iconv('UTF-8', @path_encoding, full_path)
149 142 entries << Entry.new({:name => n,
150 143 :path => full_p,
151 144 :kind => (type == "tree") ? 'dir' : 'file',
152 145 :size => (type == "tree") ? nil : size,
153 146 :lastrev => options[:report_last_commit] ?
154 147 lastrev(full_path, identifier) : Revision.new
155 148 }) unless entries.detect{|entry| entry.name == name}
156 149 end
157 150 end
158 151 end
159 152 entries.sort_by_name
160 153 rescue ScmCommandAborted
161 154 nil
162 155 end
163 private :entries_git
164 156
165 157 def lastrev(path, rev)
166 158 return nil if path.nil?
167 159 cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
168 160 cmd_args << rev if rev
169 161 cmd_args << "--" << path unless path.empty?
170 162 lines = []
171 163 scm_cmd(*cmd_args) { |io| lines = io.readlines }
172 164 begin
173 165 id = lines[0].split[1]
174 166 author = lines[1].match('Author:\s+(.*)$')[1]
175 167 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
176 168
177 169 Revision.new({
178 170 :identifier => id,
179 171 :scmid => id,
180 172 :author => author,
181 173 :time => time,
182 174 :message => nil,
183 175 :paths => nil
184 176 })
185 177 rescue NoMethodError => e
186 178 logger.error("The revision '#{path}' has a wrong format")
187 179 return nil
188 180 end
189 181 rescue ScmCommandAborted
190 182 nil
191 183 end
192 184
193 185 def revisions(path, identifier_from, identifier_to, options={})
194 186 revisions = Revisions.new
195 187 cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller|
196 188 cmd_args << "--reverse" if options[:reverse]
197 189 cmd_args << "--all" if options[:all]
198 190 cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
199 191 from_to = ""
200 192 from_to << "#{identifier_from}.." if identifier_from
201 193 from_to << "#{identifier_to}" if identifier_to
202 194 cmd_args << from_to if !from_to.empty?
203 195 cmd_args << "--since=#{options[:since].strftime("%Y-%m-%d %H:%M:%S")}" if options[:since]
204 196 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
205 197
206 198 scm_cmd *cmd_args do |io|
207 199 files=[]
208 200 changeset = {}
209 201 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
210 202
211 203 io.each_line do |line|
212 204 if line =~ /^commit ([0-9a-f]{40})$/
213 205 key = "commit"
214 206 value = $1
215 207 if (parsing_descr == 1 || parsing_descr == 2)
216 208 parsing_descr = 0
217 209 revision = Revision.new({
218 210 :identifier => changeset[:commit],
219 211 :scmid => changeset[:commit],
220 212 :author => changeset[:author],
221 213 :time => Time.parse(changeset[:date]),
222 214 :message => changeset[:description],
223 215 :paths => files
224 216 })
225 217 if block_given?
226 218 yield revision
227 219 else
228 220 revisions << revision
229 221 end
230 222 changeset = {}
231 223 files = []
232 224 end
233 225 changeset[:commit] = $1
234 226 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
235 227 key = $1
236 228 value = $2
237 229 if key == "Author"
238 230 changeset[:author] = value
239 231 elsif key == "CommitDate"
240 232 changeset[:date] = value
241 233 end
242 234 elsif (parsing_descr == 0) && line.chomp.to_s == ""
243 235 parsing_descr = 1
244 236 changeset[:description] = ""
245 237 elsif (parsing_descr == 1 || parsing_descr == 2) \
246 238 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
247 239 parsing_descr = 2
248 240 fileaction = $1
249 241 filepath = $2
250 242 p = scm_iconv('UTF-8', @path_encoding, filepath)
251 243 files << {:action => fileaction, :path => p}
252 244 elsif (parsing_descr == 1 || parsing_descr == 2) \
253 245 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
254 246 parsing_descr = 2
255 247 fileaction = $1
256 248 filepath = $3
257 249 p = scm_iconv('UTF-8', @path_encoding, filepath)
258 250 files << {:action => fileaction, :path => p}
259 251 elsif (parsing_descr == 1) && line.chomp.to_s == ""
260 252 parsing_descr = 2
261 253 elsif (parsing_descr == 1)
262 254 changeset[:description] << line[4..-1]
263 255 end
264 256 end
265 257
266 258 if changeset[:commit]
267 259 revision = Revision.new({
268 260 :identifier => changeset[:commit],
269 261 :scmid => changeset[:commit],
270 262 :author => changeset[:author],
271 263 :time => Time.parse(changeset[:date]),
272 264 :message => changeset[:description],
273 265 :paths => files
274 266 })
275 267
276 268 if block_given?
277 269 yield revision
278 270 else
279 271 revisions << revision
280 272 end
281 273 end
282 274 end
283 275 revisions
284 276 rescue ScmCommandAborted
285 277 revisions
286 278 end
287 279
288 280 def diff(path, identifier_from, identifier_to=nil)
289 281 path ||= ''
290 282 cmd_args = []
291 283 if identifier_to
292 284 cmd_args << "diff" << "--no-color" << identifier_to << identifier_from
293 285 else
294 286 cmd_args << "show" << "--no-color" << identifier_from
295 287 end
296 288 cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
297 289 diff = []
298 290 scm_cmd *cmd_args do |io|
299 291 io.each_line do |line|
300 292 diff << line
301 293 end
302 294 end
303 295 diff
304 296 rescue ScmCommandAborted
305 297 nil
306 298 end
307 299
308 300 def annotate(path, identifier=nil)
309 301 identifier = 'HEAD' if identifier.blank?
310 302 cmd_args = %w|blame|
311 303 cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
312 304 blame = Annotate.new
313 305 content = nil
314 306 scm_cmd(*cmd_args) { |io| io.binmode; content = io.read }
315 307 # git annotates binary files
316 308 return nil if content.is_binary_data?
317 309 identifier = ''
318 310 # git shows commit author on the first occurrence only
319 311 authors_by_commit = {}
320 312 content.split("\n").each do |line|
321 313 if line =~ /^([0-9a-f]{39,40})\s.*/
322 314 identifier = $1
323 315 elsif line =~ /^author (.+)/
324 316 authors_by_commit[identifier] = $1.strip
325 317 elsif line =~ /^\t(.*)/
326 318 blame.add_line($1, Revision.new(
327 319 :identifier => identifier,
328 320 :revision => identifier,
329 321 :scmid => identifier,
330 322 :author => authors_by_commit[identifier]
331 323 ))
332 324 identifier = ''
333 325 author = ''
334 326 end
335 327 end
336 328 blame
337 329 rescue ScmCommandAborted
338 330 nil
339 331 end
340 332
341 333 def cat(path, identifier=nil)
342 334 if identifier.nil?
343 335 identifier = 'HEAD'
344 336 end
345 337 cmd_args = %w|show --no-color|
346 338 cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
347 339 cat = nil
348 340 scm_cmd(*cmd_args) do |io|
349 341 io.binmode
350 342 cat = io.read
351 343 end
352 344 cat
353 345 rescue ScmCommandAborted
354 346 nil
355 347 end
356 348
357 349 class Revision < Redmine::Scm::Adapters::Revision
358 350 # Returns the readable identifier
359 351 def format_identifier
360 352 identifier[0,8]
361 353 end
362 354 end
363 355
364 356 def scm_cmd(*args, &block)
365 357 repo_path = root_url || url
366 358 full_args = [GIT_BIN, '--git-dir', repo_path]
367 359 if self.class.client_version_above?([1, 7, 2])
368 360 full_args << '-c' << 'core.quotepath=false'
369 361 full_args << '-c' << 'log.decorate=no'
370 362 end
371 363 full_args += args
372 364 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
373 365 if $? && $?.exitstatus != 0
374 366 raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
375 367 end
376 368 ret
377 369 end
378 370 private :scm_cmd
379 371 end
380 372 end
381 373 end
382 374 end
General Comments 0
You need to be logged in to leave comments. Login now