123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- # frozen_string_literal: true
-
- # Redmine - project management software
- # Copyright (C) 2006-2017 Jean-Philippe Lang
- # Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License
- # as published by the Free Software Foundation; either version 2
- # of the License, or (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # 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'
-
- class Repository::Git < Repository
- validates_presence_of :url
-
- safe_attributes 'report_last_commit'
-
- 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, *args)
- end
-
- def self.scm_adapter_class
- Redmine::Scm::Adapters::GitAdapter
- end
-
- def self.scm_name
- 'Git'
- end
-
- def report_last_commit
- return false if extra_info.nil?
- v = extra_info["extra_report_last_commit"]
- return false if v.nil?
- v.to_s != '0'
- end
-
- def report_last_commit=(arg)
- merge_extra_info "extra_report_last_commit" => arg
- end
-
- def supports_directory_revisions?
- true
- end
-
- def supports_revision_graph?
- true
- end
-
- def repo_log_encoding
- 'UTF-8'
- end
-
- # Returns the identifier for the given git changeset
- def self.changeset_identifier(changeset)
- changeset.scmid
- end
-
- # Returns the readable identifier for the given git changeset
- def self.format_changeset_identifier(changeset)
- changeset.revision[0, 8]
- end
-
- def branches
- scm.branches
- end
-
- def tags
- scm.tags
- end
-
- def default_branch
- scm.default_branch
- rescue => e
- logger.error "git: error during get default branch: #{e.message}"
- nil
- end
-
- def find_changeset_by_name(name)
- if name.present?
- changesets.find_by(:revision => name.to_s) ||
- changesets.where('scmid LIKE ?', "#{name}%").first
- end
- end
-
- def scm_entries(path=nil, identifier=nil)
- scm.entries(path, identifier, :report_last_commit => report_last_commit)
- end
- protected :scm_entries
-
- # 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 save "heads".
- #
- # In Git and Mercurial, revisions are not in date order.
- # Redmine Mercurial fixed issues.
- # * Redmine Takes Too Long On Large Mercurial Repository
- # http://www.redmine.org/issues/3449
- # * Sorting for changesets might go wrong on Mercurial repos
- # http://www.redmine.org/issues/3567
- #
- # Database revision column is text, so Redmine can not sort by revision.
- # Mercurial has revision number, and revision number guarantees revision order.
- # Redmine Mercurial model stored revisions ordered by database id to database.
- # So, Redmine Mercurial model can use correct ordering revisions.
- #
- # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
- # to get limited revisions from old to new.
- # But, Git 1.7.3.4 does not support --reverse with -n or --skip.
- #
- # The repository can still be fully reloaded by calling #clear_changesets
- # before fetching changesets (eg. for offline resync)
- def fetch_changesets
- scm_brs = branches
- return if scm_brs.nil? || scm_brs.empty?
-
- h1 = extra_info || {}
- h = h1.dup
- 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.exists?
- h["db_consistent"]["ordering"] = 1
- merge_extra_info(h)
- self.save
- elsif ! h["db_consistent"].has_key?("ordering")
- h["db_consistent"]["ordering"] = 0
- 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
- scmids = revisions_copy.slice(offset, limit).map{|x| x.scmid}
- recent_changesets_slice = changesets.where(:scmid => scmids)
- # 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)
- save(:validate => false)
- end
- private :save_revisions
-
- def save_revision(rev)
- 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,
- :parents => parents
- )
- 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?
- changesets.where(:scmid => revisions.map {|c| c.scmid}).to_a
- 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)
- save(:validate => false)
- end
- private :clear_extra_info_of_changesets
-
- def clear_changesets
- super
- clear_extra_info_of_changesets
- end
- private :clear_changesets
- end
|