To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / app / models / repository.rb @ 1270:b2f7f52a164d

History | View | Annotate | Download (11.5 KB)

1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
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
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

    
18
class ScmFetchError < Exception; end
19

    
20
class Repository < ActiveRecord::Base
21
  include Redmine::Ciphering
22
  include Redmine::SafeAttributes
23

    
24
  # Maximum length for repository identifiers
25
  IDENTIFIER_MAX_LENGTH = 255
26

    
27
  belongs_to :project
28
  has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
29
  has_many :filechanges, :class_name => 'Change', :through => :changesets
30

    
31
  serialize :extra_info
32

    
33
  before_save :check_default
34

    
35
  # Raw SQL to delete changesets and changes in the database
36
  # has_many :changesets, :dependent => :destroy is too slow for big repositories
37
  before_destroy :clear_changesets
38

    
39
  validates_length_of :password, :maximum => 255, :allow_nil => true
40
  validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
41
  validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
42
  validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
43
  validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
44
  # donwcase letters, digits, dashes, underscores but not digits only
45
  validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :allow_blank => true
46
  # Checks if the SCM is enabled when creating a repository
47
  validate :repo_create_validation, :on => :create
48

    
49
  safe_attributes 'identifier',
50
    'login',
51
    'password',
52
    'path_encoding',
53
    'log_encoding',
54
    'is_default'
55

    
56
  safe_attributes 'url',
57
    :if => lambda {|repository, user| repository.new_record?}
58

    
59
  def repo_create_validation
60
    unless Setting.enabled_scm.include?(self.class.name.demodulize)
61
      errors.add(:type, :invalid)
62
    end
63
  end
64

    
65
  def self.human_attribute_name(attribute_key_name, *args)
66
    attr_name = attribute_key_name.to_s
67
    if attr_name == "log_encoding"
68
      attr_name = "commit_logs_encoding"
69
    end
70
    super(attr_name, *args)
71
  end
72

    
73
  # Removes leading and trailing whitespace
74
  def url=(arg)
75
    write_attribute(:url, arg ? arg.to_s.strip : nil)
76
  end
77

    
78
  # Removes leading and trailing whitespace
79
  def root_url=(arg)
80
    write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81
  end
82

    
83
  def password
84
    read_ciphered_attribute(:password)
85
  end
86

    
87
  def password=(arg)
88
    write_ciphered_attribute(:password, arg)
89
  end
90

    
91
  def scm_adapter
92
    self.class.scm_adapter_class
93
  end
94

    
95
  def scm
96
    unless @scm
97
      @scm = self.scm_adapter.new(url, root_url,
98
                                  login, password, path_encoding)
99
      if root_url.blank? && @scm.root_url.present?
100
        update_attribute(:root_url, @scm.root_url)
101
      end
102
    end
103
    @scm
104
  end
105

    
106
  def scm_name
107
    self.class.scm_name
108
  end
109

    
110
  def name
111
    if identifier.present?
112
      identifier
113
    elsif is_default?
114
      l(:field_repository_is_default)
115
    else
116
      scm_name
117
    end
118
  end
119

    
120
  def identifier=(identifier)
121
    super unless identifier_frozen?
122
  end
123

    
124
  def identifier_frozen?
125
    errors[:identifier].blank? && !(new_record? || identifier.blank?)
126
  end
127

    
128
  def identifier_param
129
    if is_default?
130
      nil
131
    elsif identifier.present?
132
      identifier
133
    else
134
      id.to_s
135
    end
136
  end
137

    
138
  def <=>(repository)
139
    if is_default?
140
      -1
141
    elsif repository.is_default?
142
      1
143
    else
144
      identifier.to_s <=> repository.identifier.to_s
145
    end
146
  end
147

    
148
  def self.find_by_identifier_param(param)
149
    if param.to_s =~ /^\d+$/
150
      find_by_id(param)
151
    else
152
      find_by_identifier(param)
153
    end
154
  end
155

    
156
  def merge_extra_info(arg)
157
    h = extra_info || {}
158
    return h if arg.nil?
159
    h.merge!(arg)
160
    write_attribute(:extra_info, h)
161
  end
162

    
163
  def report_last_commit
164
    true
165
  end
166

    
167
  def supports_cat?
168
    scm.supports_cat?
169
  end
170

    
171
  def supports_annotate?
172
    scm.supports_annotate?
173
  end
174

    
175
  def supports_all_revisions?
176
    true
177
  end
178

    
179
  def supports_directory_revisions?
180
    false
181
  end
182

    
183
  def supports_revision_graph?
184
    false
185
  end
186

    
187
  def entry(path=nil, identifier=nil)
188
    scm.entry(path, identifier)
189
  end
190

    
191
  def entries(path=nil, identifier=nil)
192
    entries = scm.entries(path, identifier)
193
    load_entries_changesets(entries)
194
    entries
195
  end
196

    
197
  def branches
198
    scm.branches
199
  end
200

    
201
  def tags
202
    scm.tags
203
  end
204

    
205
  def default_branch
206
    nil
207
  end
208

    
209
  def properties(path, identifier=nil)
210
    scm.properties(path, identifier)
211
  end
212

    
213
  def cat(path, identifier=nil)
214
    scm.cat(path, identifier)
215
  end
216

    
217
  def diff(path, rev, rev_to)
218
    scm.diff(path, rev, rev_to)
219
  end
220

    
221
  def diff_format_revisions(cs, cs_to, sep=':')
222
    text = ""
223
    text << cs_to.format_identifier + sep if cs_to
224
    text << cs.format_identifier if cs
225
    text
226
  end
227

    
228
  # Returns a path relative to the url of the repository
229
  def relative_path(path)
230
    path
231
  end
232

    
233
  # Finds and returns a revision with a number or the beginning of a hash
234
  def find_changeset_by_name(name)
235
    return nil if name.blank?
236
    s = name.to_s
237
    changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
238
          ["revision = ?", s] : ["revision LIKE ?", s + '%']))
239
  end
240

    
241
  def latest_changeset
242
    @latest_changeset ||= changesets.find(:first)
243
  end
244

    
245
  # Returns the latest changesets for +path+
246
  # Default behaviour is to search in cached changesets
247
  def latest_changesets(path, rev, limit=10)
248
    if path.blank?
249
      changesets.find(
250
         :all,
251
         :include => :user,
252
         :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
253
         :limit => limit)
254
    else
255
      filechanges.find(
256
         :all,
257
         :include => {:changeset => :user},
258
         :conditions => ["path = ?", path.with_leading_slash],
259
         :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
260
         :limit => limit
261
       ).collect(&:changeset)
262
    end
263
  end
264

    
265
  def scan_changesets_for_issue_ids
266
    self.changesets.each(&:scan_comment_for_issue_ids)
267
  end
268

    
269
  # Returns an array of committers usernames and associated user_id
270
  def committers
271
    @committers ||= Changeset.connection.select_rows(
272
         "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
273
  end
274

    
275
  # Maps committers username to a user ids
276
  def committer_ids=(h)
277
    if h.is_a?(Hash)
278
      committers.each do |committer, user_id|
279
        new_user_id = h[committer]
280
        if new_user_id && (new_user_id.to_i != user_id.to_i)
281
          new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
282
          Changeset.update_all(
283
               "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
284
               ["repository_id = ? AND committer = ?", id, committer])
285
        end
286
      end
287
      @committers            = nil
288
      @found_committer_users = nil
289
      true
290
    else
291
      false
292
    end
293
  end
294

    
295
  # Returns the Redmine User corresponding to the given +committer+
296
  # It will return nil if the committer is not yet mapped and if no User
297
  # with the same username or email was found
298
  def find_committer_user(committer)
299
    unless committer.blank?
300
      @found_committer_users ||= {}
301
      return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
302

    
303
      user = nil
304
      c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
305
      if c && c.user
306
        user = c.user
307
      elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
308
        username, email = $1.strip, $3
309
        u = User.find_by_login(username)
310
        u ||= User.find_by_mail(email) unless email.blank?
311
        user = u
312
      end
313
      @found_committer_users[committer] = user
314
      user
315
    end
316
  end
317

    
318
  def repo_log_encoding
319
    encoding = log_encoding.to_s.strip
320
    encoding.blank? ? 'UTF-8' : encoding
321
  end
322

    
323
  # Fetches new changesets for all repositories of active projects
324
  # Can be called periodically by an external script
325
  # eg. ruby script/runner "Repository.fetch_changesets"
326
  def self.fetch_changesets
327
    Project.active.has_module(:repository).all.each do |project|
328
      project.repositories.each do |repository|
329
        begin
330
          repository.fetch_changesets
331
        rescue Redmine::Scm::Adapters::CommandFailed => e
332
          logger.error "scm: error during fetching changesets: #{e.message}"
333
        end
334
      end
335
    end
336
  end
337

    
338
  # scan changeset comments to find related and fixed issues for all repositories
339
  def self.scan_changesets_for_issue_ids
340
    find(:all).each(&:scan_changesets_for_issue_ids)
341
  end
342

    
343
  def self.scm_name
344
    'Abstract'
345
  end
346

    
347
  def self.available_scm
348
    subclasses.collect {|klass| [klass.scm_name, klass.name]}
349
  end
350

    
351
  def self.factory(klass_name, *args)
352
    klass = "Repository::#{klass_name}".constantize
353
    klass.new(*args)
354
  rescue
355
    nil
356
  end
357

    
358
  def clear_cache
359
    clear_changesets
360
  end
361
    
362
  def self.scm_adapter_class
363
    nil
364
  end
365

    
366
  def self.scm_command
367
    ret = ""
368
    begin
369
      ret = self.scm_adapter_class.client_command if self.scm_adapter_class
370
    rescue Exception => e
371
      logger.error "scm: error during get command: #{e.message}"
372
    end
373
    ret
374
  end
375

    
376
  def self.scm_version_string
377
    ret = ""
378
    begin
379
      ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
380
    rescue Exception => e
381
      logger.error "scm: error during get version string: #{e.message}"
382
    end
383
    ret
384
  end
385

    
386
  def self.scm_available
387
    ret = false
388
    begin
389
      ret = self.scm_adapter_class.client_available if self.scm_adapter_class
390
    rescue Exception => e
391
      logger.error "scm: error during get scm available: #{e.message}"
392
    end
393
    ret
394
  end
395

    
396
  def set_as_default?
397
    new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
398
  end
399

    
400
  protected
401

    
402
  def check_default
403
    if !is_default? && set_as_default?
404
      self.is_default = true
405
    end
406
    if is_default? && is_default_changed?
407
      Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
408
    end
409
  end
410

    
411
  def load_entries_changesets(entries)
412
    if entries
413
      entries.each do |entry|
414
        if entry.lastrev && entry.lastrev.identifier
415
          entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
416
        end
417
      end
418
    end
419
  end
420

    
421
  private
422

    
423
  # Deletes repository data
424
  def clear_changesets
425
    cs = Changeset.table_name
426
    ch = Change.table_name
427
    ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
428
    cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
429

    
430
    connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
431
    connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
432
    connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
433
    connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
434
    clear_extra_info_of_changesets
435
  end
436

    
437
  def clear_extra_info_of_changesets
438
  end
439
end