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