##// END OF EJS Templates
scm: set supporting directory revisions or not at scm level....
Toshi MARUYAMA -
r5024:bae1763a09ae
parent child
Show More
@@ -1,286 +1,290
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 class Repository < ActiveRecord::Base
18 class Repository < ActiveRecord::Base
19 include Redmine::Ciphering
19 include Redmine::Ciphering
20
20
21 belongs_to :project
21 belongs_to :project
22 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
22 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
23 has_many :changes, :through => :changesets
23 has_many :changes, :through => :changesets
24
24
25 # Raw SQL to delete changesets and changes in the database
25 # Raw SQL to delete changesets and changes in the database
26 # has_many :changesets, :dependent => :destroy is too slow for big repositories
26 # has_many :changesets, :dependent => :destroy is too slow for big repositories
27 before_destroy :clear_changesets
27 before_destroy :clear_changesets
28
28
29 validates_length_of :password, :maximum => 255, :allow_nil => true
29 validates_length_of :password, :maximum => 255, :allow_nil => true
30 # Checks if the SCM is enabled when creating a repository
30 # Checks if the SCM is enabled when creating a repository
31 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
31 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
32
32
33 # Removes leading and trailing whitespace
33 # Removes leading and trailing whitespace
34 def url=(arg)
34 def url=(arg)
35 write_attribute(:url, arg ? arg.to_s.strip : nil)
35 write_attribute(:url, arg ? arg.to_s.strip : nil)
36 end
36 end
37
37
38 # Removes leading and trailing whitespace
38 # Removes leading and trailing whitespace
39 def root_url=(arg)
39 def root_url=(arg)
40 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
40 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
41 end
41 end
42
42
43 def password
43 def password
44 read_ciphered_attribute(:password)
44 read_ciphered_attribute(:password)
45 end
45 end
46
46
47 def password=(arg)
47 def password=(arg)
48 write_ciphered_attribute(:password, arg)
48 write_ciphered_attribute(:password, arg)
49 end
49 end
50
50
51 def scm_adapter
51 def scm_adapter
52 self.class.scm_adapter_class
52 self.class.scm_adapter_class
53 end
53 end
54
54
55 def scm
55 def scm
56 @scm ||= self.scm_adapter.new(url, root_url,
56 @scm ||= self.scm_adapter.new(url, root_url,
57 login, password, path_encoding)
57 login, password, path_encoding)
58 update_attribute(:root_url, @scm.root_url) if root_url.blank?
58 update_attribute(:root_url, @scm.root_url) if root_url.blank?
59 @scm
59 @scm
60 end
60 end
61
61
62 def scm_name
62 def scm_name
63 self.class.scm_name
63 self.class.scm_name
64 end
64 end
65
65
66 def supports_cat?
66 def supports_cat?
67 scm.supports_cat?
67 scm.supports_cat?
68 end
68 end
69
69
70 def supports_annotate?
70 def supports_annotate?
71 scm.supports_annotate?
71 scm.supports_annotate?
72 end
72 end
73
73
74 def supports_all_revisions?
74 def supports_all_revisions?
75 true
75 true
76 end
76 end
77
77
78 def supports_directory_revisions?
79 false
80 end
81
78 def entry(path=nil, identifier=nil)
82 def entry(path=nil, identifier=nil)
79 scm.entry(path, identifier)
83 scm.entry(path, identifier)
80 end
84 end
81
85
82 def entries(path=nil, identifier=nil)
86 def entries(path=nil, identifier=nil)
83 scm.entries(path, identifier)
87 scm.entries(path, identifier)
84 end
88 end
85
89
86 def branches
90 def branches
87 scm.branches
91 scm.branches
88 end
92 end
89
93
90 def tags
94 def tags
91 scm.tags
95 scm.tags
92 end
96 end
93
97
94 def default_branch
98 def default_branch
95 scm.default_branch
99 scm.default_branch
96 end
100 end
97
101
98 def properties(path, identifier=nil)
102 def properties(path, identifier=nil)
99 scm.properties(path, identifier)
103 scm.properties(path, identifier)
100 end
104 end
101
105
102 def cat(path, identifier=nil)
106 def cat(path, identifier=nil)
103 scm.cat(path, identifier)
107 scm.cat(path, identifier)
104 end
108 end
105
109
106 def diff(path, rev, rev_to)
110 def diff(path, rev, rev_to)
107 scm.diff(path, rev, rev_to)
111 scm.diff(path, rev, rev_to)
108 end
112 end
109
113
110 def diff_format_revisions(cs, cs_to, sep=':')
114 def diff_format_revisions(cs, cs_to, sep=':')
111 text = ""
115 text = ""
112 text << cs_to.format_identifier + sep if cs_to
116 text << cs_to.format_identifier + sep if cs_to
113 text << cs.format_identifier if cs
117 text << cs.format_identifier if cs
114 text
118 text
115 end
119 end
116
120
117 # Returns a path relative to the url of the repository
121 # Returns a path relative to the url of the repository
118 def relative_path(path)
122 def relative_path(path)
119 path
123 path
120 end
124 end
121
125
122 # Finds and returns a revision with a number or the beginning of a hash
126 # Finds and returns a revision with a number or the beginning of a hash
123 def find_changeset_by_name(name)
127 def find_changeset_by_name(name)
124 return nil if name.blank?
128 return nil if name.blank?
125 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
129 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
126 end
130 end
127
131
128 def latest_changeset
132 def latest_changeset
129 @latest_changeset ||= changesets.find(:first)
133 @latest_changeset ||= changesets.find(:first)
130 end
134 end
131
135
132 # Returns the latest changesets for +path+
136 # Returns the latest changesets for +path+
133 # Default behaviour is to search in cached changesets
137 # Default behaviour is to search in cached changesets
134 def latest_changesets(path, rev, limit=10)
138 def latest_changesets(path, rev, limit=10)
135 if path.blank?
139 if path.blank?
136 changesets.find(:all, :include => :user,
140 changesets.find(:all, :include => :user,
137 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
141 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
138 :limit => limit)
142 :limit => limit)
139 else
143 else
140 changes.find(:all, :include => {:changeset => :user},
144 changes.find(:all, :include => {:changeset => :user},
141 :conditions => ["path = ?", path.with_leading_slash],
145 :conditions => ["path = ?", path.with_leading_slash],
142 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
146 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
143 :limit => limit).collect(&:changeset)
147 :limit => limit).collect(&:changeset)
144 end
148 end
145 end
149 end
146
150
147 def scan_changesets_for_issue_ids
151 def scan_changesets_for_issue_ids
148 self.changesets.each(&:scan_comment_for_issue_ids)
152 self.changesets.each(&:scan_comment_for_issue_ids)
149 end
153 end
150
154
151 # Returns an array of committers usernames and associated user_id
155 # Returns an array of committers usernames and associated user_id
152 def committers
156 def committers
153 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
157 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
154 end
158 end
155
159
156 # Maps committers username to a user ids
160 # Maps committers username to a user ids
157 def committer_ids=(h)
161 def committer_ids=(h)
158 if h.is_a?(Hash)
162 if h.is_a?(Hash)
159 committers.each do |committer, user_id|
163 committers.each do |committer, user_id|
160 new_user_id = h[committer]
164 new_user_id = h[committer]
161 if new_user_id && (new_user_id.to_i != user_id.to_i)
165 if new_user_id && (new_user_id.to_i != user_id.to_i)
162 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
166 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
163 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
167 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
164 end
168 end
165 end
169 end
166 @committers = nil
170 @committers = nil
167 @found_committer_users = nil
171 @found_committer_users = nil
168 true
172 true
169 else
173 else
170 false
174 false
171 end
175 end
172 end
176 end
173
177
174 # Returns the Redmine User corresponding to the given +committer+
178 # Returns the Redmine User corresponding to the given +committer+
175 # It will return nil if the committer is not yet mapped and if no User
179 # It will return nil if the committer is not yet mapped and if no User
176 # with the same username or email was found
180 # with the same username or email was found
177 def find_committer_user(committer)
181 def find_committer_user(committer)
178 unless committer.blank?
182 unless committer.blank?
179 @found_committer_users ||= {}
183 @found_committer_users ||= {}
180 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
184 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
181
185
182 user = nil
186 user = nil
183 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
187 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
184 if c && c.user
188 if c && c.user
185 user = c.user
189 user = c.user
186 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
190 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
187 username, email = $1.strip, $3
191 username, email = $1.strip, $3
188 u = User.find_by_login(username)
192 u = User.find_by_login(username)
189 u ||= User.find_by_mail(email) unless email.blank?
193 u ||= User.find_by_mail(email) unless email.blank?
190 user = u
194 user = u
191 end
195 end
192 @found_committer_users[committer] = user
196 @found_committer_users[committer] = user
193 user
197 user
194 end
198 end
195 end
199 end
196
200
197 def repo_log_encoding
201 def repo_log_encoding
198 encoding = log_encoding.to_s.strip
202 encoding = log_encoding.to_s.strip
199 encoding.blank? ? 'UTF-8' : encoding
203 encoding.blank? ? 'UTF-8' : encoding
200 end
204 end
201
205
202 # Fetches new changesets for all repositories of active projects
206 # Fetches new changesets for all repositories of active projects
203 # Can be called periodically by an external script
207 # Can be called periodically by an external script
204 # eg. ruby script/runner "Repository.fetch_changesets"
208 # eg. ruby script/runner "Repository.fetch_changesets"
205 def self.fetch_changesets
209 def self.fetch_changesets
206 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
210 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
207 if project.repository
211 if project.repository
208 begin
212 begin
209 project.repository.fetch_changesets
213 project.repository.fetch_changesets
210 rescue Redmine::Scm::Adapters::CommandFailed => e
214 rescue Redmine::Scm::Adapters::CommandFailed => e
211 logger.error "scm: error during fetching changesets: #{e.message}"
215 logger.error "scm: error during fetching changesets: #{e.message}"
212 end
216 end
213 end
217 end
214 end
218 end
215 end
219 end
216
220
217 # scan changeset comments to find related and fixed issues for all repositories
221 # scan changeset comments to find related and fixed issues for all repositories
218 def self.scan_changesets_for_issue_ids
222 def self.scan_changesets_for_issue_ids
219 find(:all).each(&:scan_changesets_for_issue_ids)
223 find(:all).each(&:scan_changesets_for_issue_ids)
220 end
224 end
221
225
222 def self.scm_name
226 def self.scm_name
223 'Abstract'
227 'Abstract'
224 end
228 end
225
229
226 def self.available_scm
230 def self.available_scm
227 subclasses.collect {|klass| [klass.scm_name, klass.name]}
231 subclasses.collect {|klass| [klass.scm_name, klass.name]}
228 end
232 end
229
233
230 def self.factory(klass_name, *args)
234 def self.factory(klass_name, *args)
231 klass = "Repository::#{klass_name}".constantize
235 klass = "Repository::#{klass_name}".constantize
232 klass.new(*args)
236 klass.new(*args)
233 rescue
237 rescue
234 nil
238 nil
235 end
239 end
236
240
237 def self.scm_adapter_class
241 def self.scm_adapter_class
238 nil
242 nil
239 end
243 end
240
244
241 def self.scm_command
245 def self.scm_command
242 ret = ""
246 ret = ""
243 begin
247 begin
244 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
248 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
245 rescue Redmine::Scm::Adapters::CommandFailed => e
249 rescue Redmine::Scm::Adapters::CommandFailed => e
246 logger.error "scm: error during get command: #{e.message}"
250 logger.error "scm: error during get command: #{e.message}"
247 end
251 end
248 ret
252 ret
249 end
253 end
250
254
251 def self.scm_version_string
255 def self.scm_version_string
252 ret = ""
256 ret = ""
253 begin
257 begin
254 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
258 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
255 rescue Redmine::Scm::Adapters::CommandFailed => e
259 rescue Redmine::Scm::Adapters::CommandFailed => e
256 logger.error "scm: error during get version string: #{e.message}"
260 logger.error "scm: error during get version string: #{e.message}"
257 end
261 end
258 ret
262 ret
259 end
263 end
260
264
261 def self.scm_available
265 def self.scm_available
262 ret = false
266 ret = false
263 begin
267 begin
264 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
268 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
265 rescue Redmine::Scm::Adapters::CommandFailed => e
269 rescue Redmine::Scm::Adapters::CommandFailed => e
266 logger.error "scm: error during get scm available: #{e.message}"
270 logger.error "scm: error during get scm available: #{e.message}"
267 end
271 end
268 ret
272 ret
269 end
273 end
270
274
271 private
275 private
272
276
273 def before_save
277 def before_save
274 # Strips url and root_url
278 # Strips url and root_url
275 url.strip!
279 url.strip!
276 root_url.strip!
280 root_url.strip!
277 true
281 true
278 end
282 end
279
283
280 def clear_changesets
284 def clear_changesets
281 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
285 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
282 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
286 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
283 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
287 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
284 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
288 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
285 end
289 end
286 end
290 end
@@ -1,130 +1,134
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 ATTRIBUTE_KEY_NAMES = {
24 ATTRIBUTE_KEY_NAMES = {
25 "url" => "Path to repository",
25 "url" => "Path to repository",
26 }
26 }
27 def self.human_attribute_name(attribute_key_name)
27 def self.human_attribute_name(attribute_key_name)
28 ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
28 ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
29 end
29 end
30
30
31 def self.scm_adapter_class
31 def self.scm_adapter_class
32 Redmine::Scm::Adapters::GitAdapter
32 Redmine::Scm::Adapters::GitAdapter
33 end
33 end
34
34
35 def self.scm_name
35 def self.scm_name
36 'Git'
36 'Git'
37 end
37 end
38
38
39 def supports_directory_revisions?
40 true
41 end
42
39 def repo_log_encoding
43 def repo_log_encoding
40 'UTF-8'
44 'UTF-8'
41 end
45 end
42
46
43 # Returns the identifier for the given git changeset
47 # Returns the identifier for the given git changeset
44 def self.changeset_identifier(changeset)
48 def self.changeset_identifier(changeset)
45 changeset.scmid
49 changeset.scmid
46 end
50 end
47
51
48 # Returns the readable identifier for the given git changeset
52 # Returns the readable identifier for the given git changeset
49 def self.format_changeset_identifier(changeset)
53 def self.format_changeset_identifier(changeset)
50 changeset.revision[0, 8]
54 changeset.revision[0, 8]
51 end
55 end
52
56
53 def branches
57 def branches
54 scm.branches
58 scm.branches
55 end
59 end
56
60
57 def tags
61 def tags
58 scm.tags
62 scm.tags
59 end
63 end
60
64
61 def find_changeset_by_name(name)
65 def find_changeset_by_name(name)
62 return nil if name.nil? || name.empty?
66 return nil if name.nil? || name.empty?
63 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
67 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
64 return e if e
68 return e if e
65 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
69 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
66 end
70 end
67
71
68 # With SCM's that have a sequential commit numbering, redmine is able to be
72 # With SCM's that have a sequential commit numbering, redmine is able to be
69 # clever and only fetch changesets going forward from the most recent one
73 # clever and only fetch changesets going forward from the most recent one
70 # it knows about. However, with git, you never know if people have merged
74 # it knows about. However, with git, you never know if people have merged
71 # commits into the middle of the repository history, so we should parse
75 # commits into the middle of the repository history, so we should parse
72 # the entire log. Since it's way too slow for large repositories, we only
76 # the entire log. Since it's way too slow for large repositories, we only
73 # parse 1 week before the last known commit.
77 # parse 1 week before the last known commit.
74 # The repository can still be fully reloaded by calling #clear_changesets
78 # The repository can still be fully reloaded by calling #clear_changesets
75 # before fetching changesets (eg. for offline resync)
79 # before fetching changesets (eg. for offline resync)
76 def fetch_changesets
80 def fetch_changesets
77 c = changesets.find(:first, :order => 'committed_on DESC')
81 c = changesets.find(:first, :order => 'committed_on DESC')
78 since = (c ? c.committed_on - 7.days : nil)
82 since = (c ? c.committed_on - 7.days : nil)
79
83
80 revisions = scm.revisions('', nil, nil, {:all => true, :since => since, :reverse => true})
84 revisions = scm.revisions('', nil, nil, {:all => true, :since => since, :reverse => true})
81 return if revisions.nil? || revisions.empty?
85 return if revisions.nil? || revisions.empty?
82
86
83 recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
87 recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
84
88
85 # Clean out revisions that are no longer in git
89 # Clean out revisions that are no longer in git
86 recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }}
90 recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }}
87
91
88 # Subtract revisions that redmine already knows about
92 # Subtract revisions that redmine already knows about
89 recent_revisions = recent_changesets.map{|c| c.scmid}
93 recent_revisions = recent_changesets.map{|c| c.scmid}
90 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
94 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
91
95
92 # Save the remaining ones to the database
96 # Save the remaining ones to the database
93 unless revisions.nil?
97 unless revisions.nil?
94 revisions.each do |rev|
98 revisions.each do |rev|
95 transaction do
99 transaction do
96 changeset = Changeset.new(
100 changeset = Changeset.new(
97 :repository => self,
101 :repository => self,
98 :revision => rev.identifier,
102 :revision => rev.identifier,
99 :scmid => rev.scmid,
103 :scmid => rev.scmid,
100 :committer => rev.author,
104 :committer => rev.author,
101 :committed_on => rev.time,
105 :committed_on => rev.time,
102 :comments => rev.message)
106 :comments => rev.message)
103
107
104 if changeset.save
108 if changeset.save
105 rev.paths.each do |file|
109 rev.paths.each do |file|
106 Change.create(
110 Change.create(
107 :changeset => changeset,
111 :changeset => changeset,
108 :action => file[:action],
112 :action => file[:action],
109 :path => file[:path])
113 :path => file[:path])
110 end
114 end
111 end
115 end
112 end
116 end
113 end
117 end
114 end
118 end
115 end
119 end
116
120
117 def latest_changesets(path,rev,limit=10)
121 def latest_changesets(path,rev,limit=10)
118 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
122 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
119 return [] if revisions.nil? || revisions.empty?
123 return [] if revisions.nil? || revisions.empty?
120
124
121 changesets.find(
125 changesets.find(
122 :all,
126 :all,
123 :conditions => [
127 :conditions => [
124 "scmid IN (?)",
128 "scmid IN (?)",
125 revisions.map!{|c| c.scmid}
129 revisions.map!{|c| c.scmid}
126 ],
130 ],
127 :order => 'committed_on DESC'
131 :order => 'committed_on DESC'
128 )
132 )
129 end
133 end
130 end
134 end
@@ -1,138 +1,142
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/mercurial_adapter'
18 require 'redmine/scm/adapters/mercurial_adapter'
19
19
20 class Repository::Mercurial < Repository
20 class Repository::Mercurial < Repository
21 # sort changesets by revision number
21 # sort changesets by revision number
22 has_many :changesets, :order => "#{Changeset.table_name}.id DESC", :foreign_key => 'repository_id'
22 has_many :changesets, :order => "#{Changeset.table_name}.id DESC", :foreign_key => 'repository_id'
23
23
24 attr_protected :root_url
24 attr_protected :root_url
25 validates_presence_of :url
25 validates_presence_of :url
26
26
27 FETCH_AT_ONCE = 100 # number of changesets to fetch at once
27 FETCH_AT_ONCE = 100 # number of changesets to fetch at once
28
28
29 ATTRIBUTE_KEY_NAMES = {
29 ATTRIBUTE_KEY_NAMES = {
30 "url" => "Root directory",
30 "url" => "Root directory",
31 }
31 }
32 def self.human_attribute_name(attribute_key_name)
32 def self.human_attribute_name(attribute_key_name)
33 ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
33 ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
34 end
34 end
35
35
36 def self.scm_adapter_class
36 def self.scm_adapter_class
37 Redmine::Scm::Adapters::MercurialAdapter
37 Redmine::Scm::Adapters::MercurialAdapter
38 end
38 end
39
39
40 def self.scm_name
40 def self.scm_name
41 'Mercurial'
41 'Mercurial'
42 end
42 end
43
43
44 def supports_directory_revisions?
45 true
46 end
47
44 def repo_log_encoding
48 def repo_log_encoding
45 'UTF-8'
49 'UTF-8'
46 end
50 end
47
51
48 # Returns the readable identifier for the given mercurial changeset
52 # Returns the readable identifier for the given mercurial changeset
49 def self.format_changeset_identifier(changeset)
53 def self.format_changeset_identifier(changeset)
50 "#{changeset.revision}:#{changeset.scmid}"
54 "#{changeset.revision}:#{changeset.scmid}"
51 end
55 end
52
56
53 # Returns the identifier for the given Mercurial changeset
57 # Returns the identifier for the given Mercurial changeset
54 def self.changeset_identifier(changeset)
58 def self.changeset_identifier(changeset)
55 changeset.scmid
59 changeset.scmid
56 end
60 end
57
61
58 def diff_format_revisions(cs, cs_to, sep=':')
62 def diff_format_revisions(cs, cs_to, sep=':')
59 super(cs, cs_to, ' ')
63 super(cs, cs_to, ' ')
60 end
64 end
61
65
62 # Finds and returns a revision with a number or the beginning of a hash
66 # Finds and returns a revision with a number or the beginning of a hash
63 def find_changeset_by_name(name)
67 def find_changeset_by_name(name)
64 return nil if name.nil? || name.empty?
68 return nil if name.nil? || name.empty?
65 if /[^\d]/ =~ name or name.to_s.size > 8
69 if /[^\d]/ =~ name or name.to_s.size > 8
66 e = changesets.find(:first, :conditions => ['scmid = ?', name.to_s])
70 e = changesets.find(:first, :conditions => ['scmid = ?', name.to_s])
67 else
71 else
68 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
72 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
69 end
73 end
70 return e if e
74 return e if e
71 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) # last ditch
75 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) # last ditch
72 end
76 end
73
77
74 # Returns the latest changesets for +path+; sorted by revision number
78 # Returns the latest changesets for +path+; sorted by revision number
75 #
79 #
76 # Because :order => 'id DESC' is defined at 'has_many',
80 # Because :order => 'id DESC' is defined at 'has_many',
77 # there is no need to set 'order'.
81 # there is no need to set 'order'.
78 # But, MySQL test fails.
82 # But, MySQL test fails.
79 # Sqlite3 and PostgreSQL pass.
83 # Sqlite3 and PostgreSQL pass.
80 # Is this MySQL bug?
84 # Is this MySQL bug?
81 def latest_changesets(path, rev, limit=10)
85 def latest_changesets(path, rev, limit=10)
82 changesets.find(:all, :include => :user,
86 changesets.find(:all, :include => :user,
83 :conditions => latest_changesets_cond(path, rev, limit),
87 :conditions => latest_changesets_cond(path, rev, limit),
84 :limit => limit, :order => "#{Changeset.table_name}.id DESC")
88 :limit => limit, :order => "#{Changeset.table_name}.id DESC")
85 end
89 end
86
90
87 def latest_changesets_cond(path, rev, limit)
91 def latest_changesets_cond(path, rev, limit)
88 cond, args = [], []
92 cond, args = [], []
89 if scm.branchmap.member? rev
93 if scm.branchmap.member? rev
90 # Mercurial named branch is *stable* in each revision.
94 # Mercurial named branch is *stable* in each revision.
91 # So, named branch can be stored in database.
95 # So, named branch can be stored in database.
92 # Mercurial provides *bookmark* which is equivalent with git branch.
96 # Mercurial provides *bookmark* which is equivalent with git branch.
93 # But, bookmark is not implemented.
97 # But, bookmark is not implemented.
94 cond << "#{Changeset.table_name}.scmid IN (?)"
98 cond << "#{Changeset.table_name}.scmid IN (?)"
95 # Revisions in root directory and sub directory are not equal.
99 # Revisions in root directory and sub directory are not equal.
96 # So, in order to get correct limit, we need to get all revisions.
100 # So, in order to get correct limit, we need to get all revisions.
97 # But, it is very heavy.
101 # But, it is very heavy.
98 args << scm.nodes_in_branch(rev, :limit => limit)
102 args << scm.nodes_in_branch(rev, :limit => limit)
99 elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
103 elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
100 cond << "#{Changeset.table_name}.id <= ?"
104 cond << "#{Changeset.table_name}.id <= ?"
101 args << last.id
105 args << last.id
102 end
106 end
103
107
104 unless path.blank?
108 unless path.blank?
105 cond << "EXISTS (SELECT * FROM #{Change.table_name}
109 cond << "EXISTS (SELECT * FROM #{Change.table_name}
106 WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id
110 WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id
107 AND (#{Change.table_name}.path = ?
111 AND (#{Change.table_name}.path = ?
108 OR #{Change.table_name}.path LIKE ? ESCAPE ?))"
112 OR #{Change.table_name}.path LIKE ? ESCAPE ?))"
109 args << path.with_leading_slash
113 args << path.with_leading_slash
110 args << "#{path.with_leading_slash.gsub(/[%_\\]/) { |s| "\\#{s}" }}/%" << '\\'
114 args << "#{path.with_leading_slash.gsub(/[%_\\]/) { |s| "\\#{s}" }}/%" << '\\'
111 end
115 end
112
116
113 [cond.join(' AND '), *args] unless cond.empty?
117 [cond.join(' AND '), *args] unless cond.empty?
114 end
118 end
115 private :latest_changesets_cond
119 private :latest_changesets_cond
116
120
117 def fetch_changesets
121 def fetch_changesets
118 scm_rev = scm.info.lastrev.revision.to_i
122 scm_rev = scm.info.lastrev.revision.to_i
119 db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
123 db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
120 return unless db_rev < scm_rev # already up-to-date
124 return unless db_rev < scm_rev # already up-to-date
121
125
122 logger.debug "Fetching changesets for repository #{url}" if logger
126 logger.debug "Fetching changesets for repository #{url}" if logger
123 (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
127 (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
124 transaction do
128 transaction do
125 scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
129 scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
126 cs = Changeset.create(:repository => self,
130 cs = Changeset.create(:repository => self,
127 :revision => re.revision,
131 :revision => re.revision,
128 :scmid => re.scmid,
132 :scmid => re.scmid,
129 :committer => re.author,
133 :committer => re.author,
130 :committed_on => re.time,
134 :committed_on => re.time,
131 :comments => re.message)
135 :comments => re.message)
132 re.paths.each { |e| cs.create_change(e) }
136 re.paths.each { |e| cs.create_change(e) }
133 end
137 end
134 end
138 end
135 end
139 end
136 self
140 self
137 end
141 end
138 end
142 end
@@ -1,89 +1,93
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/subversion_adapter'
18 require 'redmine/scm/adapters/subversion_adapter'
19
19
20 class Repository::Subversion < Repository
20 class Repository::Subversion < Repository
21 attr_protected :root_url
21 attr_protected :root_url
22 validates_presence_of :url
22 validates_presence_of :url
23 validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
23 validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
24
24
25 def self.scm_adapter_class
25 def self.scm_adapter_class
26 Redmine::Scm::Adapters::SubversionAdapter
26 Redmine::Scm::Adapters::SubversionAdapter
27 end
27 end
28
28
29 def self.scm_name
29 def self.scm_name
30 'Subversion'
30 'Subversion'
31 end
31 end
32
32
33 def supports_directory_revisions?
34 true
35 end
36
33 def repo_log_encoding
37 def repo_log_encoding
34 'UTF-8'
38 'UTF-8'
35 end
39 end
36
40
37 def latest_changesets(path, rev, limit=10)
41 def latest_changesets(path, rev, limit=10)
38 revisions = scm.revisions(path, rev, nil, :limit => limit)
42 revisions = scm.revisions(path, rev, nil, :limit => limit)
39 revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
43 revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
40 end
44 end
41
45
42 # Returns a path relative to the url of the repository
46 # Returns a path relative to the url of the repository
43 def relative_path(path)
47 def relative_path(path)
44 path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
48 path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
45 end
49 end
46
50
47 def fetch_changesets
51 def fetch_changesets
48 scm_info = scm.info
52 scm_info = scm.info
49 if scm_info
53 if scm_info
50 # latest revision found in database
54 # latest revision found in database
51 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
55 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
52 # latest revision in the repository
56 # latest revision in the repository
53 scm_revision = scm_info.lastrev.identifier.to_i
57 scm_revision = scm_info.lastrev.identifier.to_i
54 if db_revision < scm_revision
58 if db_revision < scm_revision
55 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
59 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
56 identifier_from = db_revision + 1
60 identifier_from = db_revision + 1
57 while (identifier_from <= scm_revision)
61 while (identifier_from <= scm_revision)
58 # loads changesets by batches of 200
62 # loads changesets by batches of 200
59 identifier_to = [identifier_from + 199, scm_revision].min
63 identifier_to = [identifier_from + 199, scm_revision].min
60 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
64 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
61 revisions.reverse_each do |revision|
65 revisions.reverse_each do |revision|
62 transaction do
66 transaction do
63 changeset = Changeset.create(:repository => self,
67 changeset = Changeset.create(:repository => self,
64 :revision => revision.identifier,
68 :revision => revision.identifier,
65 :committer => revision.author,
69 :committer => revision.author,
66 :committed_on => revision.time,
70 :committed_on => revision.time,
67 :comments => revision.message)
71 :comments => revision.message)
68
72
69 revision.paths.each do |change|
73 revision.paths.each do |change|
70 changeset.create_change(change)
74 changeset.create_change(change)
71 end unless changeset.new_record?
75 end unless changeset.new_record?
72 end
76 end
73 end unless revisions.nil?
77 end unless revisions.nil?
74 identifier_from = identifier_to + 1
78 identifier_from = identifier_to + 1
75 end
79 end
76 end
80 end
77 end
81 end
78 end
82 end
79
83
80 private
84 private
81
85
82 # Returns the relative url of the repository
86 # Returns the relative url of the repository
83 # Eg: root_url = file:///var/svn/foo
87 # Eg: root_url = file:///var/svn/foo
84 # url = file:///var/svn/foo/bar
88 # url = file:///var/svn/foo/bar
85 # => returns /bar
89 # => returns /bar
86 def relative_url
90 def relative_url
87 @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '')
91 @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '')
88 end
92 end
89 end
93 end
General Comments 0
You need to be logged in to leave comments. Login now