##// END OF EJS Templates
Do not parse the entire git log to fetch new commits (takes several minutes for a few thousands commits), but only 1 week before the last known commit (#4547, #4716)....
Jean-Philippe Lang -
r3280:3c20a9b0acd5
parent child
Show More
@@ -1,78 +1,81
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 # Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
3 # Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
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/git_adapter'
18 require 'redmine/scm/adapters/git_adapter'
19
19
20 class Repository::Git < Repository
20 class Repository::Git < Repository
21 attr_protected :root_url
21 attr_protected :root_url
22 validates_presence_of :url
22 validates_presence_of :url
23
23
24 def scm_adapter
24 def scm_adapter
25 Redmine::Scm::Adapters::GitAdapter
25 Redmine::Scm::Adapters::GitAdapter
26 end
26 end
27
27
28 def self.scm_name
28 def self.scm_name
29 'Git'
29 'Git'
30 end
30 end
31
31
32 def branches
32 def branches
33 scm.branches
33 scm.branches
34 end
34 end
35
35
36 def tags
36 def tags
37 scm.tags
37 scm.tags
38 end
38 end
39
39
40 # With SCM's that have a sequential commit numbering, redmine is able to be
40 # With SCM's that have a sequential commit numbering, redmine is able to be
41 # clever and only fetch changesets going forward from the most recent one
41 # clever and only fetch changesets going forward from the most recent one
42 # it knows about. However, with git, you never know if people have merged
42 # it knows about. However, with git, you never know if people have merged
43 # commits into the middle of the repository history, so we always have to
43 # commits into the middle of the repository history, so we should parse
44 # parse the entire log.
44 # the entire log. Since it's way too slow for large repositories, we only
45 # parse 1 week before the last known commit.
46 # The repository can still be fully reloaded by calling #clear_changesets
47 # before fetching changesets (eg. for offline resync)
45 def fetch_changesets
48 def fetch_changesets
46 # Save ourselves an expensive operation if we're already up to date
49 c = changesets.find(:first, :order => 'committed_on DESC')
47 return if scm.num_revisions == changesets.count
50 since = (c ? c.committed_on - 7.days : nil)
48
51
49 revisions = scm.revisions('', nil, nil, :all => true)
52 revisions = scm.revisions('', nil, nil, :all => true, :since => since)
50 return if revisions.nil? || revisions.empty?
53 return if revisions.nil? || revisions.empty?
51
54
52 # Find revisions that redmine knows about already
55 recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
53 existing_revisions = changesets.find(:all).map!{|c| c.scmid}
54
56
55 # Clean out revisions that are no longer in git
57 # Clean out revisions that are no longer in git
56 Changeset.delete_all(["scmid NOT IN (?) AND repository_id = (?)", revisions.map{|r| r.scmid}, self.id])
58 recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }}
57
59
58 # Subtract revisions that redmine already knows about
60 # Subtract revisions that redmine already knows about
59 revisions.reject!{|r| existing_revisions.include?(r.scmid)}
61 recent_revisions = recent_changesets.map{|c| c.scmid}
62 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
60
63
61 # Save the remaining ones to the database
64 # Save the remaining ones to the database
62 revisions.each{|r| r.save(self)} unless revisions.nil?
65 revisions.each{|r| r.save(self)} unless revisions.nil?
63 end
66 end
64
67
65 def latest_changesets(path,rev,limit=10)
68 def latest_changesets(path,rev,limit=10)
66 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
69 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
67 return [] if revisions.nil? || revisions.empty?
70 return [] if revisions.nil? || revisions.empty?
68
71
69 changesets.find(
72 changesets.find(
70 :all,
73 :all,
71 :conditions => [
74 :conditions => [
72 "scmid IN (?)",
75 "scmid IN (?)",
73 revisions.map!{|c| c.scmid}
76 revisions.map!{|c| c.scmid}
74 ],
77 ],
75 :order => 'committed_on DESC'
78 :order => 'committed_on DESC'
76 )
79 )
77 end
80 end
78 end
81 end
@@ -1,263 +1,259
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 # Git executable name
24 # Git executable name
25 GIT_BIN = "git"
25 GIT_BIN = "git"
26
26
27 def info
27 def info
28 begin
28 begin
29 Info.new(:root_url => url, :lastrev => lastrev('',nil))
29 Info.new(:root_url => url, :lastrev => lastrev('',nil))
30 rescue
30 rescue
31 nil
31 nil
32 end
32 end
33 end
33 end
34
34
35 def branches
35 def branches
36 branches = []
36 branches = []
37 cmd = "#{GIT_BIN} --git-dir #{target('')} branch"
37 cmd = "#{GIT_BIN} --git-dir #{target('')} branch"
38 shellout(cmd) do |io|
38 shellout(cmd) do |io|
39 io.each_line do |line|
39 io.each_line do |line|
40 branches << line.match('\s*\*?\s*(.*)$')[1]
40 branches << line.match('\s*\*?\s*(.*)$')[1]
41 end
41 end
42 end
42 end
43 branches.sort!
43 branches.sort!
44 end
44 end
45
45
46 def tags
46 def tags
47 tags = []
47 tags = []
48 cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
48 cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
49 shellout(cmd) do |io|
49 shellout(cmd) do |io|
50 io.readlines.sort!.map{|t| t.strip}
50 io.readlines.sort!.map{|t| t.strip}
51 end
51 end
52 end
52 end
53
53
54 def default_branch
54 def default_branch
55 branches.include?('master') ? 'master' : branches.first
55 branches.include?('master') ? 'master' : branches.first
56 end
56 end
57
57
58 def entries(path=nil, identifier=nil)
58 def entries(path=nil, identifier=nil)
59 path ||= ''
59 path ||= ''
60 entries = Entries.new
60 entries = Entries.new
61 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
61 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
62 cmd << shell_quote("HEAD:" + path) if identifier.nil?
62 cmd << shell_quote("HEAD:" + path) if identifier.nil?
63 cmd << shell_quote(identifier + ":" + path) if identifier
63 cmd << shell_quote(identifier + ":" + path) if identifier
64 shellout(cmd) do |io|
64 shellout(cmd) do |io|
65 io.each_line do |line|
65 io.each_line do |line|
66 e = line.chomp.to_s
66 e = line.chomp.to_s
67 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/
67 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/
68 type = $1
68 type = $1
69 sha = $2
69 sha = $2
70 size = $3
70 size = $3
71 name = $4
71 name = $4
72 full_path = path.empty? ? name : "#{path}/#{name}"
72 full_path = path.empty? ? name : "#{path}/#{name}"
73 entries << Entry.new({:name => name,
73 entries << Entry.new({:name => name,
74 :path => full_path,
74 :path => full_path,
75 :kind => (type == "tree") ? 'dir' : 'file',
75 :kind => (type == "tree") ? 'dir' : 'file',
76 :size => (type == "tree") ? nil : size,
76 :size => (type == "tree") ? nil : size,
77 :lastrev => lastrev(full_path,identifier)
77 :lastrev => lastrev(full_path,identifier)
78 }) unless entries.detect{|entry| entry.name == name}
78 }) unless entries.detect{|entry| entry.name == name}
79 end
79 end
80 end
80 end
81 end
81 end
82 return nil if $? && $?.exitstatus != 0
82 return nil if $? && $?.exitstatus != 0
83 entries.sort_by_name
83 entries.sort_by_name
84 end
84 end
85
85
86 def lastrev(path,rev)
86 def lastrev(path,rev)
87 return nil if path.nil?
87 return nil if path.nil?
88 cmd = "#{GIT_BIN} --git-dir #{target('')} log --pretty=fuller --no-merges -n 1 "
88 cmd = "#{GIT_BIN} --git-dir #{target('')} log --pretty=fuller --no-merges -n 1 "
89 cmd << " #{shell_quote rev} " if rev
89 cmd << " #{shell_quote rev} " if rev
90 cmd << "-- #{path} " unless path.empty?
90 cmd << "-- #{path} " unless path.empty?
91 shellout(cmd) do |io|
91 shellout(cmd) do |io|
92 begin
92 begin
93 id = io.gets.split[1]
93 id = io.gets.split[1]
94 author = io.gets.match('Author:\s+(.*)$')[1]
94 author = io.gets.match('Author:\s+(.*)$')[1]
95 2.times { io.gets }
95 2.times { io.gets }
96 time = io.gets.match('CommitDate:\s+(.*)$')[1]
96 time = io.gets.match('CommitDate:\s+(.*)$')[1]
97
97
98 Revision.new({
98 Revision.new({
99 :identifier => id,
99 :identifier => id,
100 :scmid => id,
100 :scmid => id,
101 :author => author,
101 :author => author,
102 :time => time,
102 :time => time,
103 :message => nil,
103 :message => nil,
104 :paths => nil
104 :paths => nil
105 })
105 })
106 rescue NoMethodError => e
106 rescue NoMethodError => e
107 logger.error("The revision '#{path}' has a wrong format")
107 logger.error("The revision '#{path}' has a wrong format")
108 return nil
108 return nil
109 end
109 end
110 end
110 end
111 end
111 end
112
112
113 def num_revisions
114 cmd = "#{GIT_BIN} --git-dir #{target('')} log --all --pretty=format:'' | wc -l"
115 shellout(cmd) {|io| io.gets.chomp.to_i + 1}
116 end
117
118 def revisions(path, identifier_from, identifier_to, options={})
113 def revisions(path, identifier_from, identifier_to, options={})
119 revisions = Revisions.new
114 revisions = Revisions.new
120
115
121 cmd = "#{GIT_BIN} --git-dir #{target('')} log --find-copies-harder --raw --date=iso --pretty=fuller"
116 cmd = "#{GIT_BIN} --git-dir #{target('')} log --find-copies-harder --raw --date=iso --pretty=fuller"
122 cmd << " --reverse" if options[:reverse]
117 cmd << " --reverse" if options[:reverse]
123 cmd << " --all" if options[:all]
118 cmd << " --all" if options[:all]
124 cmd << " -n #{options[:limit]} " if options[:limit]
119 cmd << " -n #{options[:limit]} " if options[:limit]
125 cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
120 cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
126 cmd << " #{shell_quote identifier_to} " if identifier_to
121 cmd << " #{shell_quote identifier_to} " if identifier_to
122 cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since]
127 cmd << " -- #{path}" if path && !path.empty?
123 cmd << " -- #{path}" if path && !path.empty?
128
124
129 shellout(cmd) do |io|
125 shellout(cmd) do |io|
130 files=[]
126 files=[]
131 changeset = {}
127 changeset = {}
132 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
128 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
133 revno = 1
129 revno = 1
134
130
135 io.each_line do |line|
131 io.each_line do |line|
136 if line =~ /^commit ([0-9a-f]{40})$/
132 if line =~ /^commit ([0-9a-f]{40})$/
137 key = "commit"
133 key = "commit"
138 value = $1
134 value = $1
139 if (parsing_descr == 1 || parsing_descr == 2)
135 if (parsing_descr == 1 || parsing_descr == 2)
140 parsing_descr = 0
136 parsing_descr = 0
141 revision = Revision.new({
137 revision = Revision.new({
142 :identifier => changeset[:commit],
138 :identifier => changeset[:commit],
143 :scmid => changeset[:commit],
139 :scmid => changeset[:commit],
144 :author => changeset[:author],
140 :author => changeset[:author],
145 :time => Time.parse(changeset[:date]),
141 :time => Time.parse(changeset[:date]),
146 :message => changeset[:description],
142 :message => changeset[:description],
147 :paths => files
143 :paths => files
148 })
144 })
149 if block_given?
145 if block_given?
150 yield revision
146 yield revision
151 else
147 else
152 revisions << revision
148 revisions << revision
153 end
149 end
154 changeset = {}
150 changeset = {}
155 files = []
151 files = []
156 revno = revno + 1
152 revno = revno + 1
157 end
153 end
158 changeset[:commit] = $1
154 changeset[:commit] = $1
159 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
155 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
160 key = $1
156 key = $1
161 value = $2
157 value = $2
162 if key == "Author"
158 if key == "Author"
163 changeset[:author] = value
159 changeset[:author] = value
164 elsif key == "CommitDate"
160 elsif key == "CommitDate"
165 changeset[:date] = value
161 changeset[:date] = value
166 end
162 end
167 elsif (parsing_descr == 0) && line.chomp.to_s == ""
163 elsif (parsing_descr == 0) && line.chomp.to_s == ""
168 parsing_descr = 1
164 parsing_descr = 1
169 changeset[:description] = ""
165 changeset[:description] = ""
170 elsif (parsing_descr == 1 || parsing_descr == 2) \
166 elsif (parsing_descr == 1 || parsing_descr == 2) \
171 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
167 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
172 parsing_descr = 2
168 parsing_descr = 2
173 fileaction = $1
169 fileaction = $1
174 filepath = $2
170 filepath = $2
175 files << {:action => fileaction, :path => filepath}
171 files << {:action => fileaction, :path => filepath}
176 elsif (parsing_descr == 1 || parsing_descr == 2) \
172 elsif (parsing_descr == 1 || parsing_descr == 2) \
177 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\s+(.+)$/
173 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\s+(.+)$/
178 parsing_descr = 2
174 parsing_descr = 2
179 fileaction = $1
175 fileaction = $1
180 filepath = $3
176 filepath = $3
181 files << {:action => fileaction, :path => filepath}
177 files << {:action => fileaction, :path => filepath}
182 elsif (parsing_descr == 1) && line.chomp.to_s == ""
178 elsif (parsing_descr == 1) && line.chomp.to_s == ""
183 parsing_descr = 2
179 parsing_descr = 2
184 elsif (parsing_descr == 1)
180 elsif (parsing_descr == 1)
185 changeset[:description] << line[4..-1]
181 changeset[:description] << line[4..-1]
186 end
182 end
187 end
183 end
188
184
189 if changeset[:commit]
185 if changeset[:commit]
190 revision = Revision.new({
186 revision = Revision.new({
191 :identifier => changeset[:commit],
187 :identifier => changeset[:commit],
192 :scmid => changeset[:commit],
188 :scmid => changeset[:commit],
193 :author => changeset[:author],
189 :author => changeset[:author],
194 :time => Time.parse(changeset[:date]),
190 :time => Time.parse(changeset[:date]),
195 :message => changeset[:description],
191 :message => changeset[:description],
196 :paths => files
192 :paths => files
197 })
193 })
198
194
199 if block_given?
195 if block_given?
200 yield revision
196 yield revision
201 else
197 else
202 revisions << revision
198 revisions << revision
203 end
199 end
204 end
200 end
205 end
201 end
206
202
207 return nil if $? && $?.exitstatus != 0
203 return nil if $? && $?.exitstatus != 0
208 revisions
204 revisions
209 end
205 end
210
206
211 def diff(path, identifier_from, identifier_to=nil)
207 def diff(path, identifier_from, identifier_to=nil)
212 path ||= ''
208 path ||= ''
213
209
214 if identifier_to
210 if identifier_to
215 cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}"
211 cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}"
216 else
212 else
217 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}"
213 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}"
218 end
214 end
219
215
220 cmd << " -- #{shell_quote path}" unless path.empty?
216 cmd << " -- #{shell_quote path}" unless path.empty?
221 diff = []
217 diff = []
222 shellout(cmd) do |io|
218 shellout(cmd) do |io|
223 io.each_line do |line|
219 io.each_line do |line|
224 diff << line
220 diff << line
225 end
221 end
226 end
222 end
227 return nil if $? && $?.exitstatus != 0
223 return nil if $? && $?.exitstatus != 0
228 diff
224 diff
229 end
225 end
230
226
231 def annotate(path, identifier=nil)
227 def annotate(path, identifier=nil)
232 identifier = 'HEAD' if identifier.blank?
228 identifier = 'HEAD' if identifier.blank?
233 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{shell_quote identifier} -- #{shell_quote path}"
229 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{shell_quote identifier} -- #{shell_quote path}"
234 blame = Annotate.new
230 blame = Annotate.new
235 content = nil
231 content = nil
236 shellout(cmd) { |io| io.binmode; content = io.read }
232 shellout(cmd) { |io| io.binmode; content = io.read }
237 return nil if $? && $?.exitstatus != 0
233 return nil if $? && $?.exitstatus != 0
238 # git annotates binary files
234 # git annotates binary files
239 return nil if content.is_binary_data?
235 return nil if content.is_binary_data?
240 content.split("\n").each do |line|
236 content.split("\n").each do |line|
241 next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)/
237 next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)/
242 blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip))
238 blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip))
243 end
239 end
244 blame
240 blame
245 end
241 end
246
242
247 def cat(path, identifier=nil)
243 def cat(path, identifier=nil)
248 if identifier.nil?
244 if identifier.nil?
249 identifier = 'HEAD'
245 identifier = 'HEAD'
250 end
246 end
251 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}"
247 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}"
252 cat = nil
248 cat = nil
253 shellout(cmd) do |io|
249 shellout(cmd) do |io|
254 io.binmode
250 io.binmode
255 cat = io.read
251 cat = io.read
256 end
252 end
257 return nil if $? && $?.exitstatus != 0
253 return nil if $? && $?.exitstatus != 0
258 cat
254 cat
259 end
255 end
260 end
256 end
261 end
257 end
262 end
258 end
263 end
259 end
General Comments 0
You need to be logged in to leave comments. Login now