Chris@441: # Redmine - project management software Chris@1295: # Copyright (C) 2006-2013 Jean-Philippe Lang Chris@0: # Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com Chris@441: # Chris@0: # This program is free software; you can redistribute it and/or Chris@0: # modify it under the terms of the GNU General Public License Chris@0: # as published by the Free Software Foundation; either version 2 Chris@0: # of the License, or (at your option) any later version. Chris@441: # Chris@0: # This program is distributed in the hope that it will be useful, Chris@0: # but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@0: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@0: # GNU General Public License for more details. Chris@441: # Chris@0: # You should have received a copy of the GNU General Public License Chris@0: # along with this program; if not, write to the Free Software Chris@0: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Chris@0: chris@1136: require_dependency 'redmine/scm/adapters/git_adapter' Chris@0: Chris@0: class Repository::Git < Repository Chris@0: attr_protected :root_url Chris@0: validates_presence_of :url Chris@0: Chris@1115: def self.human_attribute_name(attribute_key_name, *args) Chris@1115: attr_name = attribute_key_name.to_s Chris@441: if attr_name == "url" Chris@441: attr_name = "path_to_repository" Chris@441: end Chris@1115: super(attr_name, *args) Chris@245: end Chris@245: Chris@245: def self.scm_adapter_class Chris@0: Redmine::Scm::Adapters::GitAdapter Chris@0: end Chris@245: Chris@0: def self.scm_name Chris@0: 'Git' Chris@0: end Chris@0: Chris@441: def report_last_commit Chris@441: extra_report_last_commit Chris@441: end Chris@441: Chris@441: def extra_report_last_commit Chris@441: return false if extra_info.nil? Chris@441: v = extra_info["extra_report_last_commit"] Chris@441: return false if v.nil? Chris@441: v.to_s != '0' Chris@441: end Chris@441: Chris@441: def supports_directory_revisions? Chris@441: true Chris@441: end Chris@441: Chris@909: def supports_revision_graph? Chris@909: true Chris@909: end Chris@909: Chris@245: def repo_log_encoding Chris@245: 'UTF-8' Chris@245: end Chris@245: Chris@117: # Returns the identifier for the given git changeset Chris@117: def self.changeset_identifier(changeset) Chris@117: changeset.scmid Chris@117: end Chris@117: Chris@117: # Returns the readable identifier for the given git changeset Chris@117: def self.format_changeset_identifier(changeset) Chris@117: changeset.revision[0, 8] Chris@117: end Chris@117: Chris@0: def branches Chris@0: scm.branches Chris@0: end Chris@0: Chris@0: def tags Chris@0: scm.tags Chris@0: end Chris@0: Chris@507: def default_branch Chris@507: scm.default_branch Chris@909: rescue Exception => e Chris@909: logger.error "git: error during get default branch: #{e.message}" Chris@909: nil Chris@507: end Chris@507: Chris@245: def find_changeset_by_name(name) Chris@1115: if name.present? Chris@1115: changesets.where(:revision => name.to_s).first || Chris@1115: changesets.where('scmid LIKE ?', "#{name}%").first Chris@1115: end Chris@245: end Chris@245: Chris@441: def entries(path=nil, identifier=nil) Chris@1115: entries = scm.entries(path, identifier, :report_last_commit => extra_report_last_commit) Chris@1115: load_entries_changesets(entries) Chris@1115: entries Chris@441: end Chris@441: Chris@909: # With SCMs that have a sequential commit numbering, Chris@909: # such as Subversion and Mercurial, Chris@909: # Redmine is able to be clever and only fetch changesets Chris@909: # going forward from the most recent one it knows about. Chris@1115: # Chris@909: # However, Git does not have a sequential commit numbering. Chris@909: # Chris@909: # In order to fetch only new adding revisions, Chris@1115: # Redmine needs to save "heads". Chris@909: # Chris@441: # In Git and Mercurial, revisions are not in date order. Chris@441: # Redmine Mercurial fixed issues. Chris@441: # * Redmine Takes Too Long On Large Mercurial Repository Chris@441: # http://www.redmine.org/issues/3449 Chris@441: # * Sorting for changesets might go wrong on Mercurial repos Chris@441: # http://www.redmine.org/issues/3567 Chris@441: # Chris@441: # Database revision column is text, so Redmine can not sort by revision. Chris@441: # Mercurial has revision number, and revision number guarantees revision order. Chris@441: # Redmine Mercurial model stored revisions ordered by database id to database. Chris@441: # So, Redmine Mercurial model can use correct ordering revisions. Chris@441: # Chris@441: # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10" Chris@441: # to get limited revisions from old to new. Chris@441: # But, Git 1.7.3.4 does not support --reverse with -n or --skip. Chris@441: # Chris@0: # The repository can still be fully reloaded by calling #clear_changesets Chris@0: # before fetching changesets (eg. for offline resync) Chris@0: def fetch_changesets Chris@441: scm_brs = branches Chris@441: return if scm_brs.nil? || scm_brs.empty? Chris@1115: Chris@441: h1 = extra_info || {} Chris@441: h = h1.dup Chris@1115: repo_heads = scm_brs.map{ |br| br.scmid } Chris@1115: h["heads"] ||= [] Chris@1115: prev_db_heads = h["heads"].dup Chris@1115: if prev_db_heads.empty? Chris@1115: prev_db_heads += heads_from_branches_hash Chris@1115: end Chris@1115: return if prev_db_heads.sort == repo_heads.sort Chris@1115: Chris@441: h["db_consistent"] ||= {} Chris@441: if changesets.count == 0 Chris@441: h["db_consistent"]["ordering"] = 1 Chris@441: merge_extra_info(h) Chris@441: self.save Chris@441: elsif ! h["db_consistent"].has_key?("ordering") Chris@441: h["db_consistent"]["ordering"] = 0 Chris@441: merge_extra_info(h) Chris@441: self.save Chris@441: end Chris@1115: save_revisions(prev_db_heads, repo_heads) Chris@1115: end Chris@1115: Chris@1115: def save_revisions(prev_db_heads, repo_heads) Chris@1115: h = {} Chris@1115: opts = {} Chris@1115: opts[:reverse] = true Chris@1115: opts[:excludes] = prev_db_heads Chris@1115: opts[:includes] = repo_heads Chris@1115: Chris@1115: revisions = scm.revisions('', nil, nil, opts) Chris@1115: return if revisions.blank? Chris@1115: Chris@1115: # Make the search for existing revisions in the database in a more sufficient manner Chris@1115: # Chris@1115: # Git branch is the reference to the specific revision. Chris@1115: # Git can *delete* remote branch and *re-push* branch. Chris@1115: # Chris@1115: # $ git push remote :branch Chris@1115: # $ git push remote branch Chris@1115: # Chris@1115: # After deleting branch, revisions remain in repository until "git gc". Chris@1115: # On git 1.7.2.3, default pruning date is 2 weeks. Chris@1115: # So, "git log --not deleted_branch_head_revision" return code is 0. Chris@1115: # Chris@1115: # After re-pushing branch, "git log" returns revisions which are saved in database. Chris@1115: # So, Redmine needs to scan revisions and database every time. Chris@1115: # Chris@1115: # This is replacing the one-after-one queries. Chris@1115: # Find all revisions, that are in the database, and then remove them from the revision array. Chris@1115: # Then later we won't need any conditions for db existence. Chris@1115: # Query for several revisions at once, and remove them from the revisions array, if they are there. Chris@1115: # Do this in chunks, to avoid eventual memory problems (in case of tens of thousands of commits). Chris@1115: # If there are no revisions (because the original code's algorithm filtered them), Chris@1115: # then this part will be stepped over. Chris@1115: # We make queries, just if there is any revision. Chris@1115: limit = 100 Chris@1115: offset = 0 Chris@1115: revisions_copy = revisions.clone # revisions will change Chris@1115: while offset < revisions_copy.size Chris@1115: recent_changesets_slice = changesets.find( Chris@1115: :all, Chris@1115: :conditions => [ Chris@1115: 'scmid IN (?)', Chris@1115: revisions_copy.slice(offset, limit).map{|x| x.scmid} Chris@1115: ] Chris@1115: ) Chris@1115: # Subtract revisions that redmine already knows about Chris@1115: recent_revisions = recent_changesets_slice.map{|c| c.scmid} Chris@1115: revisions.reject!{|r| recent_revisions.include?(r.scmid)} Chris@1115: offset += limit Chris@1115: end Chris@1115: Chris@1115: revisions.each do |rev| Chris@1115: transaction do Chris@1115: # There is no search in the db for this revision, because above we ensured, Chris@1115: # that it's not in the db. Chris@1115: save_revision(rev) Chris@245: end Chris@245: end Chris@1115: h["heads"] = repo_heads.dup Chris@1115: merge_extra_info(h) Chris@1115: self.save Chris@0: end Chris@1115: private :save_revisions Chris@0: Chris@441: def save_revision(rev) Chris@1115: parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact Chris@1115: changeset = Changeset.create( Chris@441: :repository => self, Chris@441: :revision => rev.identifier, Chris@441: :scmid => rev.scmid, Chris@441: :committer => rev.author, Chris@441: :committed_on => rev.time, Chris@1115: :comments => rev.message, Chris@1115: :parents => parents Chris@441: ) Chris@1115: unless changeset.new_record? Chris@1115: rev.paths.each { |change| changeset.create_change(change) } Chris@441: end Chris@909: changeset Chris@441: end Chris@441: private :save_revision Chris@441: Chris@1115: def heads_from_branches_hash Chris@1115: h1 = extra_info || {} Chris@1115: h = h1.dup Chris@1115: h["branches"] ||= {} Chris@1115: h['branches'].map{|br, hs| hs['last_scmid']} Chris@1115: end Chris@1115: Chris@0: def latest_changesets(path,rev,limit=10) Chris@0: revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false) Chris@0: return [] if revisions.nil? || revisions.empty? Chris@0: Chris@0: changesets.find( Chris@441: :all, Chris@0: :conditions => [ Chris@441: "scmid IN (?)", Chris@0: revisions.map!{|c| c.scmid} Chris@1295: ] Chris@0: ) Chris@0: end Chris@1115: Chris@1115: def clear_extra_info_of_changesets Chris@1115: return if extra_info.nil? Chris@1115: v = extra_info["extra_report_last_commit"] Chris@1115: write_attribute(:extra_info, nil) Chris@1115: h = {} Chris@1115: h["extra_report_last_commit"] = v Chris@1115: merge_extra_info(h) Chris@1115: self.save Chris@1115: end Chris@1115: private :clear_extra_info_of_changesets Chris@0: end