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