File: app/models/repository/git.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  class: Repository
  class: Git#21
inherits from
  Repository   
has properties
class method: human_attribute_name / 2 #25
class method: scm_adapter_class #33
class method: scm_name #37
method: report_last_commit #41
method: extra_report_last_commit #45
method: supports_directory_revisions? #52
method: supports_revision_graph? #56
method: repo_log_encoding #60
class method: changeset_identifier / 1 #65
class method: format_changeset_identifier / 1 #70
method: branches #74
method: tags #78
method: default_branch #82
method: find_changeset_by_name / 1 #89
method: entries / 2 #96
method: fetch_changesets #130
method: save_revisions / 2 #157
method: save_revision / 1 #220
method: heads_from_branches_hash #238
method: latest_changesets / 3 #245

Class Hierarchy

Object ( Builtin-Module )
Base ( ActiveRecord )
Repository
  Git ( Repository ) #21

Code

   1  # Redmine - project management software
   2  # Copyright (C) 2006-2012  Jean-Philippe Lang
   3  # Copyright (C) 2007  Patrick Aljord patcito@ŋmail.com
   4  #
   5  # This program is free software; you can redistribute it and/or
   6  # modify it under the terms of the GNU General Public License
   7  # as published by the Free Software Foundation; either version 2
   8  # of the License, or (at your option) any later version.
   9  #
  10  # This program is distributed in the hope that it will be useful,
  11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  # GNU General Public License for more details.
  14  #
  15  # You should have received a copy of the GNU General Public License
  16  # along with this program; if not, write to the Free Software
  17  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  18 
  19  require 'redmine/scm/adapters/git_adapter'
  20 
  21  class Repository::Git < Repository
  22    attr_protected :root_url
  23    validates_presence_of :url
  24 
  25    def self.human_attribute_name(attribute_key_name, *args)
  26      attr_name = attribute_key_name.to_s
  27      if attr_name == "url"
  28        attr_name = "path_to_repository"
  29      end
  30      super(attr_name, *args)
  31    end
  32 
  33    def self.scm_adapter_class
  34      Redmine::Scm::Adapters::GitAdapter
  35    end
  36 
  37    def self.scm_name
  38      'Git'
  39    end
  40 
  41    def report_last_commit
  42      extra_report_last_commit
  43    end
  44 
  45    def extra_report_last_commit
  46      return false if extra_info.nil?
  47      v = extra_info["extra_report_last_commit"]
  48      return false if v.nil?
  49      v.to_s != '0'
  50    end
  51 
  52    def supports_directory_revisions?
  53      true
  54    end
  55 
  56    def supports_revision_graph?
  57      true
  58    end
  59 
  60    def repo_log_encoding
  61      'UTF-8'
  62    end
  63 
  64    # Returns the identifier for the given git changeset
  65    def self.changeset_identifier(changeset)
  66      changeset.scmid
  67    end
  68 
  69    # Returns the readable identifier for the given git changeset
  70    def self.format_changeset_identifier(changeset)
  71      changeset.revision[0, 8]
  72    end
  73 
  74    def branches
  75      scm.branches
  76    end
  77 
  78    def tags
  79      scm.tags
  80    end
  81 
  82    def default_branch
  83      scm.default_branch
  84    rescue Exception => e
  85      logger.error "git: error during get default branch: #{e.message}"
  86      nil
  87    end
  88 
  89    def find_changeset_by_name(name)
  90      return nil if name.nil? || name.empty?
  91      e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
  92      return e if e
  93      changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
  94    end
  95 
  96    def entries(path=nil, identifier=nil)
  97      scm.entries(path,
  98                  identifier,
  99                  options = {:report_last_commit => extra_report_last_commit})
 100    end
 101 
 102    # With SCMs that have a sequential commit numbering,
 103    # such as Subversion and Mercurial,
 104    # Redmine is able to be clever and only fetch changesets
 105    # going forward from the most recent one it knows about.
 106    #
 107    # However, Git does not have a sequential commit numbering.
 108    #
 109    # In order to fetch only new adding revisions,
 110    # Redmine needs to save "heads".
 111    #
 112    # In Git and Mercurial, revisions are not in date order.
 113    # Redmine Mercurial fixed issues.
 114    #    * Redmine Takes Too Long On Large Mercurial Repository
 115    #      http://www.redmine.org/issues/3449
 116    #    * Sorting for changesets might go wrong on Mercurial repos
 117    #      http://www.redmine.org/issues/3567
 118    #
 119    # Database revision column is text, so Redmine can not sort by revision.
 120    # Mercurial has revision number, and revision number guarantees revision order.
 121    # Redmine Mercurial model stored revisions ordered by database id to database.
 122    # So, Redmine Mercurial model can use correct ordering revisions.
 123    #
 124    # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
 125    # to get limited revisions from old to new.
 126    # But, Git 1.7.3.4 does not support --reverse with -n or --skip.
 127    #
 128    # The repository can still be fully reloaded by calling #clear_changesets
 129    # before fetching changesets (eg. for offline resync)
 130    def fetch_changesets
 131      scm_brs = branches
 132      return if scm_brs.nil? || scm_brs.empty?
 133 
 134      h1 = extra_info || {}
 135      h  = h1.dup
 136      repo_heads = scm_brs.map{ |br| br.scmid }
 137      h["heads"] ||= []
 138      prev_db_heads = h["heads"].dup
 139      if prev_db_heads.empty?
 140        prev_db_heads += heads_from_branches_hash
 141      end
 142      return if prev_db_heads.sort == repo_heads.sort
 143 
 144      h["db_consistent"]  ||= {}
 145      if changesets.count == 0
 146        h["db_consistent"]["ordering"] = 1
 147        merge_extra_info(h)
 148        self.save
 149      elsif ! h["db_consistent"].has_key?("ordering")
 150        h["db_consistent"]["ordering"] = 0
 151        merge_extra_info(h)
 152        self.save
 153      end
 154      save_revisions(prev_db_heads, repo_heads)
 155    end
 156 
 157    def save_revisions(prev_db_heads, repo_heads)
 158      h = {}
 159      opts = {}
 160      opts[:reverse]  = true
 161      opts[:excludes] = prev_db_heads
 162      opts[:includes] = repo_heads
 163 
 164      revisions = scm.revisions('', nil, nil, opts)
 165      return if revisions.blank?
 166 
 167      # Make the search for existing revisions in the database in a more sufficient manner
 168      #
 169      # Git branch is the reference to the specific revision.
 170      # Git can *delete* remote branch and *re-push* branch.
 171      #
 172      #  $ git push remote :branch
 173      #  $ git push remote branch
 174      #
 175      # After deleting branch, revisions remain in repository until "git gc".
 176      # On git 1.7.2.3, default pruning date is 2 weeks.
 177      # So, "git log --not deleted_branch_head_revision" return code is 0.
 178      #
 179      # After re-pushing branch, "git log" returns revisions which are saved in database.
 180      # So, Redmine needs to scan revisions and database every time.
 181      #
 182      # This is replacing the one-after-one queries.
 183      # Find all revisions, that are in the database, and then remove them from the revision array.
 184      # Then later we won't need any conditions for db existence.
 185      # Query for several revisions at once, and remove them from the revisions array, if they are there.
 186      # Do this in chunks, to avoid eventual memory problems (in case of tens of thousands of commits).
 187      # If there are no revisions (because the original code's algorithm filtered them),
 188      # then this part will be stepped over.
 189      # We make queries, just if there is any revision.
 190      limit = 100
 191      offset = 0
 192      revisions_copy = revisions.clone # revisions will change
 193      while offset < revisions_copy.size
 194        recent_changesets_slice = changesets.find(
 195                                       :all,
 196                                       :conditions => [
 197                                          'scmid IN (?)',
 198                                          revisions_copy.slice(offset, limit).map{|x| x.scmid}
 199                                        ]
 200                                      )
 201        # Subtract revisions that redmine already knows about
 202        recent_revisions = recent_changesets_slice.map{|c| c.scmid}
 203        revisions.reject!{|r| recent_revisions.include?(r.scmid)}
 204        offset += limit
 205      end
 206 
 207      revisions.each do |rev|
 208        transaction do
 209          # There is no search in the db for this revision, because above we ensured,
 210          # that it's not in the db.
 211          save_revision(rev)
 212        end
 213      end
 214      h["heads"] = repo_heads.dup
 215      merge_extra_info(h)
 216      self.save
 217    end
 218    private :save_revisions
 219 
 220    def save_revision(rev)
 221      parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
 222      changeset = Changeset.create(
 223                :repository   => self,
 224                :revision     => rev.identifier,
 225                :scmid        => rev.scmid,
 226                :committer    => rev.author,
 227                :committed_on => rev.time,
 228                :comments     => rev.message,
 229                :parents      => parents
 230                )
 231      unless changeset.new_record?
 232        rev.paths.each { |change| changeset.create_change(change) }
 233      end
 234      changeset
 235    end
 236    private :save_revision
 237 
 238    def heads_from_branches_hash
 239      h1 = extra_info || {}
 240      h  = h1.dup
 241      h["branches"] ||= {}
 242      h['branches'].map{|br, hs| hs['last_scmid']}
 243    end
 244 
 245    def latest_changesets(path,rev,limit=10)
 246      revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
 247      return [] if revisions.nil? || revisions.empty?
 248 
 249      changesets.find(
 250        :all,
 251        :conditions => [
 252          "scmid IN (?)",
 253          revisions.map!{|c| c.scmid}
 254        ],
 255        :order => 'committed_on DESC'
 256      )
 257    end
 258  end