File: app/models/repository.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  class: ScmFetchError#18
inherits from
  Exception ( Builtin-Module )
  class: Repository#20
includes
  Ciphering ( Redmine )
inherits from
  Base ( ActiveRecord )
has properties
method: repo_create_validation #45
class method: human_attribute_name / 2 #51
alias: attributes_without_extra_info= attributes #59
method: attributes= / 2 #60
method: url= / 1 #82
method: root_url= / 1 #87
method: password #91
method: password= / 1 #95
method: scm_adapter #99
method: scm #103
method: scm_name (1/2) #114
method: name #118
method: identifier_param #128
method: <=> / 1 #138
class method: find_by_identifier_param / 1 #148
method: merge_extra_info / 1 #156
method: report_last_commit #163
method: supports_cat? #167
method: supports_annotate? #171
method: supports_all_revisions? #175
method: supports_directory_revisions? #179
method: supports_revision_graph? #183
method: entry / 2 #187
method: entries / 2 #191
method: branches #195
method: tags #199
method: default_branch #203
method: properties / 2 #207
method: cat / 2 #211
method: diff / 3 #215
method: diff_format_revisions / 3 #219
method: relative_path / 1 #227
method: find_changeset_by_name / 1 #232
method: latest_changeset #239
method: latest_changesets / 3 #245
method: scan_changesets_for_issue_ids (1/2) #263
method: committers #268
method: committer_ids= / 1 #274
method: find_committer_user / 1 #296
method: repo_log_encoding #316
class method: fetch_changesets #324
class method: scan_changesets_for_issue_ids (2/E) #337
class method: scm_name (2/E) #341
class method: available_scm #345
class method: factory / 2 #349
class method: scm_adapter_class #356
class method: scm_command #360
class method: scm_version_string #370
class method: scm_available #380
method: set_as_default? #390
method: check_default #396
method: clear_changesets #408

Class Hierarchy

Object ( Builtin-Module )
Exception ( Builtin-Module )
  ScmFetchError    #18
Base ( ActiveRecord )
  Repository    #20

Code

   1  # Redmine - project management software
   2  # Copyright (C) 2006-2011  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 
  23    belongs_to :project
  24    has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
  25    has_many :changes, :through => :changesets
  26 
  27    serialize :extra_info
  28 
  29    before_save :check_default
  30 
  31    # Raw SQL to delete changesets and changes in the database
  32    # has_many :changesets, :dependent => :destroy is too slow for big repositories
  33    before_destroy :clear_changesets
  34 
  35    validates_length_of :password, :maximum => 255, :allow_nil => true
  36    validates_length_of :identifier, :maximum => 255, :allow_blank => true
  37    validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
  38    validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
  39    validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
  40    # donwcase letters, digits, dashes but not digits only
  41    validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :allow_blank => true
  42    # Checks if the SCM is enabled when creating a repository
  43    validate :repo_create_validation, :on => :create
  44 
  45    def repo_create_validation
  46      unless Setting.enabled_scm.include?(self.class.name.demodulize)
  47        errors.add(:type, :invalid)
  48      end
  49    end
  50 
  51    def self.human_attribute_name(attribute_key_name, *args)
  52      attr_name = attribute_key_name.to_s
  53      if attr_name == "log_encoding"
  54        attr_name = "commit_logs_encoding"
  55      end
  56      super(attr_name, *args)
  57    end
  58 
  59    alias :attributes_without_extra_info= :attributes=
  60    def attributes=(new_attributes, guard_protected_attributes = true)
  61      return if new_attributes.nil?
  62      attributes = new_attributes.dup
  63      attributes.stringify_keys!
  64 
  65      p       = {}
  66      p_extra = {}
  67      attributes.each do |k, v|
  68        if k =~ /^extra_/
  69          p_extra[k] = v
  70        else
  71          p[k] = v
  72        end
  73      end
  74 
  75      send :attributes_without_extra_info=, p, guard_protected_attributes
  76      if p_extra.keys.any?
  77        merge_extra_info(p_extra)
  78      end
  79    end
  80 
  81    # Removes leading and trailing whitespace
  82    def url=(arg)
  83      write_attribute(:url, arg ? arg.to_s.strip : nil)
  84    end
  85 
  86    # Removes leading and trailing whitespace
  87    def root_url=(arg)
  88      write_attribute(:root_url, arg ? arg.to_s.strip : nil)
  89    end
  90 
  91    def password
  92      read_ciphered_attribute(:password)
  93    end
  94 
  95    def password=(arg)
  96      write_ciphered_attribute(:password, arg)
  97    end
  98 
  99    def scm_adapter
 100      self.class.scm_adapter_class
 101    end
 102 
 103    def scm
 104      unless @scm
 105        @scm = self.scm_adapter.new(url, root_url,
 106                                    login, password, path_encoding)
 107        if root_url.blank? && @scm.root_url.present?
 108          update_attribute(:root_url, @scm.root_url)
 109        end
 110      end
 111      @scm
 112    end
 113 
 114    def scm_name
 115      self.class.scm_name
 116    end
 117 
 118    def name
 119      if identifier.present?
 120        identifier
 121      elsif is_default?
 122        l(:field_repository_is_default)
 123      else
 124        scm_name
 125      end
 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 <=> repository.identifier
 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      scm.entries(path, identifier)
 193    end
 194 
 195    def branches
 196      scm.branches
 197    end
 198 
 199    def tags
 200      scm.tags
 201    end
 202 
 203    def default_branch
 204      nil
 205    end
 206 
 207    def properties(path, identifier=nil)
 208      scm.properties(path, identifier)
 209    end
 210 
 211    def cat(path, identifier=nil)
 212      scm.cat(path, identifier)
 213    end
 214 
 215    def diff(path, rev, rev_to)
 216      scm.diff(path, rev, rev_to)
 217    end
 218 
 219    def diff_format_revisions(cs, cs_to, sep=':')
 220      text = ""
 221      text << cs_to.format_identifier + sep if cs_to
 222      text << cs.format_identifier if cs
 223      text
 224    end
 225 
 226    # Returns a path relative to the url of the repository
 227    def relative_path(path)
 228      path
 229    end
 230 
 231    # Finds and returns a revision with a number or the beginning of a hash
 232    def find_changeset_by_name(name)
 233      return nil if name.blank?
 234      s = name.to_s
 235      changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
 236            ["revision = ?", s] : ["revision LIKE ?", s + '%']))
 237    end
 238 
 239    def latest_changeset
 240      @latest_changeset ||= changesets.find(:first)
 241    end
 242 
 243    # Returns the latest changesets for +path+
 244    # Default behaviour is to search in cached changesets
 245    def latest_changesets(path, rev, limit=10)
 246      if path.blank?
 247        changesets.find(
 248           :all,
 249           :include => :user,
 250           :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
 251           :limit => limit)
 252      else
 253        changes.find(
 254           :all,
 255           :include => {:changeset => :user},
 256           :conditions => ["path = ?", path.with_leading_slash],
 257           :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
 258           :limit => limit
 259         ).collect(&:changeset)
 260      end
 261    end
 262 
 263    def scan_changesets_for_issue_ids
 264      self.changesets.each(&:scan_comment_for_issue_ids)
 265    end
 266 
 267    # Returns an array of committers usernames and associated user_id
 268    def committers
 269      @committers ||= Changeset.connection.select_rows(
 270           "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
 271    end
 272 
 273    # Maps committers username to a user ids
 274    def committer_ids=(h)
 275      if h.is_a?(Hash)
 276        committers.each do |committer, user_id|
 277          new_user_id = h[committer]
 278          if new_user_id && (new_user_id.to_i != user_id.to_i)
 279            new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
 280            Changeset.update_all(
 281                 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
 282                 ["repository_id = ? AND committer = ?", id, committer])
 283          end
 284        end
 285        @committers            = nil
 286        @found_committer_users = nil
 287        true
 288      else
 289        false
 290      end
 291    end
 292 
 293    # Returns the Redmine User corresponding to the given +committer+
 294    # It will return nil if the committer is not yet mapped and if no User
 295    # with the same username or email was found
 296    def find_committer_user(committer)
 297      unless committer.blank?
 298        @found_committer_users ||= {}
 299        return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
 300 
 301        user = nil
 302        c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
 303        if c && c.user
 304          user = c.user
 305        elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
 306          username, email = $1.strip, $3
 307          u = User.find_by_login(username)
 308          u ||= User.find_by_mail(email) unless email.blank?
 309          user = u
 310        end
 311        @found_committer_users[committer] = user
 312        user
 313      end
 314    end
 315 
 316    def repo_log_encoding
 317      encoding = log_encoding.to_s.strip
 318      encoding.blank? ? 'UTF-8' : encoding
 319    end
 320 
 321    # Fetches new changesets for all repositories of active projects
 322    # Can be called periodically by an external script
 323    # eg. ruby script/runner "Repository.fetch_changesets"
 324    def self.fetch_changesets
 325      Project.active.has_module(:repository).all.each do |project|
 326        project.repositories.each do |repository|
 327          begin
 328            repository.fetch_changesets
 329          rescue Redmine::Scm::Adapters::CommandFailed => e
 330            logger.error "scm: error during fetching changesets: #{e.message}"
 331          end
 332        end
 333      end
 334    end
 335 
 336    # scan changeset comments to find related and fixed issues for all repositories
 337    def self.scan_changesets_for_issue_ids
 338      find(:all).each(&:scan_changesets_for_issue_ids)
 339    end
 340 
 341    def self.scm_name
 342      'Abstract'
 343    end
 344 
 345    def self.available_scm
 346      subclasses.collect {|klass| [klass.scm_name, klass.name]}
 347    end
 348 
 349    def self.factory(klass_name, *args)
 350      klass = "Repository::#{klass_name}".constantize
 351      klass.new(*args)
 352    rescue
 353      nil
 354    end
 355 
 356    def self.scm_adapter_class
 357      nil
 358    end
 359 
 360    def self.scm_command
 361      ret = ""
 362      begin
 363        ret = self.scm_adapter_class.client_command if self.scm_adapter_class
 364      rescue Exception => e
 365        logger.error "scm: error during get command: #{e.message}"
 366      end
 367      ret
 368    end
 369 
 370    def self.scm_version_string
 371      ret = ""
 372      begin
 373        ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
 374      rescue Exception => e
 375        logger.error "scm: error during get version string: #{e.message}"
 376      end
 377      ret
 378    end
 379 
 380    def self.scm_available
 381      ret = false
 382      begin
 383        ret = self.scm_adapter_class.client_available if self.scm_adapter_class
 384      rescue Exception => e
 385        logger.error "scm: error during get scm available: #{e.message}"
 386      end
 387      ret
 388    end
 389 
 390    def set_as_default?
 391      new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
 392    end
 393 
 394    protected
 395 
 396    def check_default
 397      if !is_default? && set_as_default?
 398        self.is_default = true
 399      end
 400      if is_default? && is_default_changed?
 401        Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
 402      end
 403    end
 404 
 405    private
 406 
 407    # Deletes repository data
 408    def clear_changesets
 409      cs = Changeset.table_name 
 410      ch = Change.table_name
 411      ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
 412      cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
 413 
 414      connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
 415      connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
 416      connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
 417      connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
 418    end
 419  end