Chris@909: # Redmine - project management software Chris@909: # Copyright (C) 2006-2011 Jean-Philippe Lang Chris@909: # Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com Chris@909: # Chris@909: # This program is free software; you can redistribute it and/or Chris@909: # modify it under the terms of the GNU General Public License Chris@909: # as published by the Free Software Foundation; either version 2 Chris@909: # of the License, or (at your option) any later version. Chris@909: # Chris@909: # This program is distributed in the hope that it will be useful, Chris@909: # but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@909: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@909: # GNU General Public License for more details. Chris@909: # Chris@909: # You should have received a copy of the GNU General Public License Chris@909: # along with this program; if not, write to the Free Software Chris@909: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Chris@909: Chris@909: require 'redmine/scm/adapters/git_adapter' Chris@909: Chris@909: class Repository::Git < Repository Chris@909: attr_protected :root_url Chris@909: validates_presence_of :url Chris@909: Chris@909: def self.human_attribute_name(attribute_key_name) Chris@909: attr_name = attribute_key_name Chris@909: if attr_name == "url" Chris@909: attr_name = "path_to_repository" Chris@909: end Chris@909: super(attr_name) Chris@909: end Chris@909: Chris@909: def self.scm_adapter_class Chris@909: Redmine::Scm::Adapters::GitAdapter Chris@909: end Chris@909: Chris@909: def self.scm_name Chris@909: 'Git' Chris@909: end Chris@909: Chris@909: def report_last_commit Chris@909: extra_report_last_commit Chris@909: end Chris@909: Chris@909: def extra_report_last_commit Chris@909: return false if extra_info.nil? Chris@909: v = extra_info["extra_report_last_commit"] Chris@909: return false if v.nil? Chris@909: v.to_s != '0' Chris@909: end Chris@909: Chris@909: def supports_directory_revisions? Chris@909: true Chris@909: end Chris@909: Chris@909: def supports_revision_graph? Chris@909: true Chris@909: end Chris@909: Chris@909: def repo_log_encoding Chris@909: 'UTF-8' Chris@909: end Chris@909: Chris@909: # Returns the identifier for the given git changeset Chris@909: def self.changeset_identifier(changeset) Chris@909: changeset.scmid Chris@909: end Chris@909: Chris@909: # Returns the readable identifier for the given git changeset Chris@909: def self.format_changeset_identifier(changeset) Chris@909: changeset.revision[0, 8] Chris@909: end Chris@909: Chris@909: def branches Chris@909: scm.branches Chris@909: end Chris@909: Chris@909: def tags Chris@909: scm.tags Chris@909: end Chris@909: Chris@909: def default_branch Chris@909: scm.default_branch Chris@909: rescue Exception => e Chris@909: logger.error "git: error during get default branch: #{e.message}" Chris@909: nil Chris@909: end Chris@909: Chris@909: def find_changeset_by_name(name) Chris@909: return nil if name.nil? || name.empty? Chris@909: e = changesets.find(:first, :conditions => ['revision = ?', name.to_s]) Chris@909: return e if e Chris@909: changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) Chris@909: end Chris@909: Chris@909: def entries(path=nil, identifier=nil) Chris@909: scm.entries(path, Chris@909: identifier, Chris@909: options = {:report_last_commit => extra_report_last_commit}) Chris@909: end Chris@909: 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@909: # Chris@909: # However, Git does not have a sequential commit numbering. Chris@909: # Chris@909: # In order to fetch only new adding revisions, Chris@909: # Redmine needs to parse revisions per branch. Chris@909: # Branch "last_scmid" is for this requirement. Chris@909: # Chris@909: # In Git and Mercurial, revisions are not in date order. Chris@909: # Redmine Mercurial fixed issues. Chris@909: # * Redmine Takes Too Long On Large Mercurial Repository Chris@909: # http://www.redmine.org/issues/3449 Chris@909: # * Sorting for changesets might go wrong on Mercurial repos Chris@909: # http://www.redmine.org/issues/3567 Chris@909: # Chris@909: # Database revision column is text, so Redmine can not sort by revision. Chris@909: # Mercurial has revision number, and revision number guarantees revision order. Chris@909: # Redmine Mercurial model stored revisions ordered by database id to database. Chris@909: # So, Redmine Mercurial model can use correct ordering revisions. Chris@909: # Chris@909: # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10" Chris@909: # to get limited revisions from old to new. Chris@909: # But, Git 1.7.3.4 does not support --reverse with -n or --skip. Chris@909: # Chris@909: # The repository can still be fully reloaded by calling #clear_changesets Chris@909: # before fetching changesets (eg. for offline resync) Chris@909: def fetch_changesets Chris@909: scm_brs = branches Chris@909: return if scm_brs.nil? || scm_brs.empty? Chris@909: h1 = extra_info || {} Chris@909: h = h1.dup Chris@909: h["branches"] ||= {} Chris@909: h["db_consistent"] ||= {} Chris@909: if changesets.count == 0 Chris@909: h["db_consistent"]["ordering"] = 1 Chris@909: merge_extra_info(h) Chris@909: self.save Chris@909: elsif ! h["db_consistent"].has_key?("ordering") Chris@909: h["db_consistent"]["ordering"] = 0 Chris@909: merge_extra_info(h) Chris@909: self.save Chris@909: end Chris@909: scm_brs.each do |br1| Chris@909: br = br1.to_s Chris@909: from_scmid = nil Chris@909: from_scmid = h["branches"][br]["last_scmid"] if h["branches"][br] Chris@909: h["branches"][br] ||= {} Chris@909: scm.revisions('', from_scmid, br, {:reverse => true}) do |rev| Chris@909: db_rev = find_changeset_by_name(rev.revision) Chris@909: transaction do Chris@909: if db_rev.nil? Chris@909: db_saved_rev = save_revision(rev) Chris@909: parents = {} Chris@909: parents[db_saved_rev] = rev.parents unless rev.parents.nil? Chris@909: parents.each do |ch, chparents| Chris@909: ch.parents = chparents.collect{|rp| find_changeset_by_name(rp)}.compact Chris@909: end Chris@909: end Chris@909: h["branches"][br]["last_scmid"] = rev.scmid Chris@909: merge_extra_info(h) Chris@909: self.save Chris@909: end Chris@909: end Chris@909: end Chris@909: end Chris@909: Chris@909: def save_revision(rev) Chris@909: changeset = Changeset.new( Chris@909: :repository => self, Chris@909: :revision => rev.identifier, Chris@909: :scmid => rev.scmid, Chris@909: :committer => rev.author, Chris@909: :committed_on => rev.time, Chris@909: :comments => rev.message Chris@909: ) Chris@909: if changeset.save Chris@909: rev.paths.each do |file| Chris@909: Change.create( Chris@909: :changeset => changeset, Chris@909: :action => file[:action], Chris@909: :path => file[:path]) Chris@909: end Chris@909: end Chris@909: changeset Chris@909: end Chris@909: private :save_revision Chris@909: Chris@909: def latest_changesets(path,rev,limit=10) Chris@909: revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false) Chris@909: return [] if revisions.nil? || revisions.empty? Chris@909: Chris@909: changesets.find( Chris@909: :all, Chris@909: :conditions => [ Chris@909: "scmid IN (?)", Chris@909: revisions.map!{|c| c.scmid} Chris@909: ], Chris@909: :order => 'committed_on DESC' Chris@909: ) Chris@909: end Chris@909: end