diff app/models/repository/git.rb @ 1339:c03a6c3c4db9 luisf

Merge
author luisf <luis.figueira@eecs.qmul.ac.uk>
date Thu, 20 Jun 2013 14:34:42 +0100
parents 51d7f3e06556
children 4f746d8966dd 51364c0cd58f
line wrap: on
line diff
--- a/app/models/repository/git.rb	Wed Nov 21 18:16:32 2012 +0000
+++ b/app/models/repository/git.rb	Thu Jun 20 14:34:42 2013 +0100
@@ -1,5 +1,5 @@
 # Redmine - project management software
-# Copyright (C) 2006-2011  Jean-Philippe Lang
+# Copyright (C) 2006-2012  Jean-Philippe Lang
 # Copyright (C) 2007  Patrick Aljord patcito@ŋmail.com
 #
 # This program is free software; you can redistribute it and/or
@@ -16,18 +16,18 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
-require 'redmine/scm/adapters/git_adapter'
+require_dependency 'redmine/scm/adapters/git_adapter'
 
 class Repository::Git < Repository
   attr_protected :root_url
   validates_presence_of :url
 
-  def self.human_attribute_name(attribute_key_name)
-    attr_name = attribute_key_name
+  def self.human_attribute_name(attribute_key_name, *args)
+    attr_name = attribute_key_name.to_s
     if attr_name == "url"
       attr_name = "path_to_repository"
     end
-    super(attr_name)
+    super(attr_name, *args)
   end
 
   def self.scm_adapter_class
@@ -87,28 +87,27 @@
   end
 
   def find_changeset_by_name(name)
-    return nil if name.nil? || name.empty?
-    e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
-    return e if e
-    changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
+    if name.present?
+      changesets.where(:revision => name.to_s).first ||
+        changesets.where('scmid LIKE ?', "#{name}%").first
+    end
   end
 
   def entries(path=nil, identifier=nil)
-    scm.entries(path,
-                identifier,
-                options = {:report_last_commit => extra_report_last_commit})
+    entries = scm.entries(path, identifier, :report_last_commit => extra_report_last_commit)
+    load_entries_changesets(entries)
+    entries
   end
 
   # With SCMs that have a sequential commit numbering,
   # such as Subversion and Mercurial,
   # Redmine is able to be clever and only fetch changesets
   # going forward from the most recent one it knows about.
-  # 
+  #
   # However, Git does not have a sequential commit numbering.
   #
   # In order to fetch only new adding revisions,
-  # Redmine needs to parse revisions per branch.
-  # Branch "last_scmid" is for this requirement.
+  # Redmine needs to save "heads".
   #
   # In Git and Mercurial, revisions are not in date order.
   # Redmine Mercurial fixed issues.
@@ -131,9 +130,17 @@
   def fetch_changesets
     scm_brs = branches
     return if scm_brs.nil? || scm_brs.empty?
+
     h1 = extra_info || {}
     h  = h1.dup
-    h["branches"]       ||= {}
+    repo_heads = scm_brs.map{ |br| br.scmid }
+    h["heads"] ||= []
+    prev_db_heads = h["heads"].dup
+    if prev_db_heads.empty?
+      prev_db_heads += heads_from_branches_hash
+    end
+    return if prev_db_heads.sort == repo_heads.sort
+
     h["db_consistent"]  ||= {}
     if changesets.count == 0
       h["db_consistent"]["ordering"] = 1
@@ -144,51 +151,97 @@
       merge_extra_info(h)
       self.save
     end
-    scm_brs.each do |br1|
-      br = br1.to_s
-      from_scmid = nil
-      from_scmid = h["branches"][br]["last_scmid"] if h["branches"][br]
-      h["branches"][br] ||= {}
-      scm.revisions('', from_scmid, br, {:reverse => true}) do |rev|
-        db_rev = find_changeset_by_name(rev.revision)
-        transaction do
-          if db_rev.nil?
-            db_saved_rev = save_revision(rev)
-            parents = {}
-            parents[db_saved_rev] = rev.parents unless rev.parents.nil?
-            parents.each do |ch, chparents|
-              ch.parents = chparents.collect{|rp| find_changeset_by_name(rp)}.compact
-            end
-          end
-          h["branches"][br]["last_scmid"] = rev.scmid
-          merge_extra_info(h)
-          self.save
-        end
+    save_revisions(prev_db_heads, repo_heads)
+  end
+
+  def save_revisions(prev_db_heads, repo_heads)
+    h = {}
+    opts = {}
+    opts[:reverse]  = true
+    opts[:excludes] = prev_db_heads
+    opts[:includes] = repo_heads
+
+    revisions = scm.revisions('', nil, nil, opts)
+    return if revisions.blank?
+
+    # Make the search for existing revisions in the database in a more sufficient manner
+    #
+    # Git branch is the reference to the specific revision.
+    # Git can *delete* remote branch and *re-push* branch.
+    #
+    #  $ git push remote :branch
+    #  $ git push remote branch
+    #
+    # After deleting branch, revisions remain in repository until "git gc".
+    # On git 1.7.2.3, default pruning date is 2 weeks.
+    # So, "git log --not deleted_branch_head_revision" return code is 0.
+    #
+    # After re-pushing branch, "git log" returns revisions which are saved in database.
+    # So, Redmine needs to scan revisions and database every time.
+    #
+    # This is replacing the one-after-one queries.
+    # Find all revisions, that are in the database, and then remove them from the revision array.
+    # Then later we won't need any conditions for db existence.
+    # Query for several revisions at once, and remove them from the revisions array, if they are there.
+    # Do this in chunks, to avoid eventual memory problems (in case of tens of thousands of commits).
+    # If there are no revisions (because the original code's algorithm filtered them),
+    # then this part will be stepped over.
+    # We make queries, just if there is any revision.
+    limit = 100
+    offset = 0
+    revisions_copy = revisions.clone # revisions will change
+    while offset < revisions_copy.size
+      recent_changesets_slice = changesets.find(
+                                     :all,
+                                     :conditions => [
+                                        'scmid IN (?)',
+                                        revisions_copy.slice(offset, limit).map{|x| x.scmid}
+                                      ]
+                                    )
+      # Subtract revisions that redmine already knows about
+      recent_revisions = recent_changesets_slice.map{|c| c.scmid}
+      revisions.reject!{|r| recent_revisions.include?(r.scmid)}
+      offset += limit
+    end
+
+    revisions.each do |rev|
+      transaction do
+        # There is no search in the db for this revision, because above we ensured,
+        # that it's not in the db.
+        save_revision(rev)
       end
     end
+    h["heads"] = repo_heads.dup
+    merge_extra_info(h)
+    self.save
   end
+  private :save_revisions
 
   def save_revision(rev)
-    changeset = Changeset.new(
+    parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
+    changeset = Changeset.create(
               :repository   => self,
               :revision     => rev.identifier,
               :scmid        => rev.scmid,
               :committer    => rev.author,
               :committed_on => rev.time,
-              :comments     => rev.message
+              :comments     => rev.message,
+              :parents      => parents
               )
-    if changeset.save
-      rev.paths.each do |file|
-        Change.create(
-                  :changeset => changeset,
-                  :action    => file[:action],
-                  :path      => file[:path])
-      end
+    unless changeset.new_record?
+      rev.paths.each { |change| changeset.create_change(change) }
     end
     changeset
   end
   private :save_revision
 
+  def heads_from_branches_hash
+    h1 = extra_info || {}
+    h  = h1.dup
+    h["branches"] ||= {}
+    h['branches'].map{|br, hs| hs['last_scmid']}
+  end
+
   def latest_changesets(path,rev,limit=10)
     revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
     return [] if revisions.nil? || revisions.empty?
@@ -202,4 +255,15 @@
       :order => 'committed_on DESC'
     )
   end
+
+  def clear_extra_info_of_changesets
+    return if extra_info.nil?
+    v = extra_info["extra_report_last_commit"]
+    write_attribute(:extra_info, nil)
+    h = {}
+    h["extra_report_last_commit"] = v
+    merge_extra_info(h)
+    self.save
+  end
+  private :clear_extra_info_of_changesets
 end