You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

git.rb 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2017 Jean-Philippe Lang
  4. # Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License
  8. # as published by the Free Software Foundation; either version 2
  9. # of the License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. require 'redmine/scm/adapters/git_adapter'
  20. class Repository::Git < Repository
  21. validates_presence_of :url
  22. safe_attributes 'report_last_commit'
  23. def self.human_attribute_name(attribute_key_name, *args)
  24. attr_name = attribute_key_name.to_s
  25. if attr_name == "url"
  26. attr_name = "path_to_repository"
  27. end
  28. super(attr_name, *args)
  29. end
  30. def self.scm_adapter_class
  31. Redmine::Scm::Adapters::GitAdapter
  32. end
  33. def self.scm_name
  34. 'Git'
  35. end
  36. def report_last_commit
  37. return false if extra_info.nil?
  38. v = extra_info["extra_report_last_commit"]
  39. return false if v.nil?
  40. v.to_s != '0'
  41. end
  42. def report_last_commit=(arg)
  43. merge_extra_info "extra_report_last_commit" => arg
  44. end
  45. def supports_directory_revisions?
  46. true
  47. end
  48. def supports_revision_graph?
  49. true
  50. end
  51. def repo_log_encoding
  52. 'UTF-8'
  53. end
  54. # Returns the identifier for the given git changeset
  55. def self.changeset_identifier(changeset)
  56. changeset.scmid
  57. end
  58. # Returns the readable identifier for the given git changeset
  59. def self.format_changeset_identifier(changeset)
  60. changeset.revision[0, 8]
  61. end
  62. def branches
  63. scm.branches
  64. end
  65. def tags
  66. scm.tags
  67. end
  68. def default_branch
  69. scm.default_branch
  70. rescue => e
  71. logger.error "git: error during get default branch: #{e.message}"
  72. nil
  73. end
  74. def find_changeset_by_name(name)
  75. if name.present?
  76. changesets.find_by(:revision => name.to_s) ||
  77. changesets.where('scmid LIKE ?', "#{name}%").first
  78. end
  79. end
  80. def scm_entries(path=nil, identifier=nil)
  81. scm.entries(path, identifier, :report_last_commit => report_last_commit)
  82. end
  83. protected :scm_entries
  84. # With SCMs that have a sequential commit numbering,
  85. # such as Subversion and Mercurial,
  86. # Redmine is able to be clever and only fetch changesets
  87. # going forward from the most recent one it knows about.
  88. #
  89. # However, Git does not have a sequential commit numbering.
  90. #
  91. # In order to fetch only new adding revisions,
  92. # Redmine needs to save "heads".
  93. #
  94. # In Git and Mercurial, revisions are not in date order.
  95. # Redmine Mercurial fixed issues.
  96. # * Redmine Takes Too Long On Large Mercurial Repository
  97. # http://www.redmine.org/issues/3449
  98. # * Sorting for changesets might go wrong on Mercurial repos
  99. # http://www.redmine.org/issues/3567
  100. #
  101. # Database revision column is text, so Redmine can not sort by revision.
  102. # Mercurial has revision number, and revision number guarantees revision order.
  103. # Redmine Mercurial model stored revisions ordered by database id to database.
  104. # So, Redmine Mercurial model can use correct ordering revisions.
  105. #
  106. # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
  107. # to get limited revisions from old to new.
  108. # But, Git 1.7.3.4 does not support --reverse with -n or --skip.
  109. #
  110. # The repository can still be fully reloaded by calling #clear_changesets
  111. # before fetching changesets (eg. for offline resync)
  112. def fetch_changesets
  113. scm_brs = branches
  114. return if scm_brs.nil? || scm_brs.empty?
  115. h1 = extra_info || {}
  116. h = h1.dup
  117. repo_heads = scm_brs.map{ |br| br.scmid }
  118. h["heads"] ||= []
  119. prev_db_heads = h["heads"].dup
  120. if prev_db_heads.empty?
  121. prev_db_heads += heads_from_branches_hash
  122. end
  123. return if prev_db_heads.sort == repo_heads.sort
  124. h["db_consistent"] ||= {}
  125. if ! changesets.exists?
  126. h["db_consistent"]["ordering"] = 1
  127. merge_extra_info(h)
  128. self.save
  129. elsif ! h["db_consistent"].has_key?("ordering")
  130. h["db_consistent"]["ordering"] = 0
  131. merge_extra_info(h)
  132. self.save
  133. end
  134. save_revisions(prev_db_heads, repo_heads)
  135. end
  136. def save_revisions(prev_db_heads, repo_heads)
  137. h = {}
  138. opts = {}
  139. opts[:reverse] = true
  140. opts[:excludes] = prev_db_heads
  141. opts[:includes] = repo_heads
  142. revisions = scm.revisions('', nil, nil, opts)
  143. return if revisions.blank?
  144. # Make the search for existing revisions in the database in a more sufficient manner
  145. #
  146. # Git branch is the reference to the specific revision.
  147. # Git can *delete* remote branch and *re-push* branch.
  148. #
  149. # $ git push remote :branch
  150. # $ git push remote branch
  151. #
  152. # After deleting branch, revisions remain in repository until "git gc".
  153. # On git 1.7.2.3, default pruning date is 2 weeks.
  154. # So, "git log --not deleted_branch_head_revision" return code is 0.
  155. #
  156. # After re-pushing branch, "git log" returns revisions which are saved in database.
  157. # So, Redmine needs to scan revisions and database every time.
  158. #
  159. # This is replacing the one-after-one queries.
  160. # Find all revisions, that are in the database, and then remove them
  161. # from the revision array.
  162. # Then later we won't need any conditions for db existence.
  163. # Query for several revisions at once, and remove them
  164. # from the revisions array, if they are there.
  165. # Do this in chunks, to avoid eventual memory problems
  166. # (in case of tens of thousands of commits).
  167. # If there are no revisions (because the original code's algorithm filtered them),
  168. # then this part will be stepped over.
  169. # We make queries, just if there is any revision.
  170. limit = 100
  171. offset = 0
  172. revisions_copy = revisions.clone # revisions will change
  173. while offset < revisions_copy.size
  174. scmids = revisions_copy.slice(offset, limit).map{|x| x.scmid}
  175. recent_changesets_slice = changesets.where(:scmid => scmids)
  176. # Subtract revisions that redmine already knows about
  177. recent_revisions = recent_changesets_slice.map{|c| c.scmid}
  178. revisions.reject!{|r| recent_revisions.include?(r.scmid)}
  179. offset += limit
  180. end
  181. revisions.each do |rev|
  182. transaction do
  183. # There is no search in the db for this revision, because above we ensured,
  184. # that it's not in the db.
  185. save_revision(rev)
  186. end
  187. end
  188. h["heads"] = repo_heads.dup
  189. merge_extra_info(h)
  190. save(:validate => false)
  191. end
  192. private :save_revisions
  193. def save_revision(rev)
  194. parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
  195. changeset = Changeset.create(
  196. :repository => self,
  197. :revision => rev.identifier,
  198. :scmid => rev.scmid,
  199. :committer => rev.author,
  200. :committed_on => rev.time,
  201. :comments => rev.message,
  202. :parents => parents
  203. )
  204. unless changeset.new_record?
  205. rev.paths.each { |change| changeset.create_change(change) }
  206. end
  207. changeset
  208. end
  209. private :save_revision
  210. def heads_from_branches_hash
  211. h1 = extra_info || {}
  212. h = h1.dup
  213. h["branches"] ||= {}
  214. h['branches'].map{|br, hs| hs['last_scmid']}
  215. end
  216. def latest_changesets(path,rev,limit=10)
  217. revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
  218. return [] if revisions.nil? || revisions.empty?
  219. changesets.where(:scmid => revisions.map {|c| c.scmid}).to_a
  220. end
  221. def clear_extra_info_of_changesets
  222. return if extra_info.nil?
  223. v = extra_info["extra_report_last_commit"]
  224. write_attribute(:extra_info, nil)
  225. h = {}
  226. h["extra_report_last_commit"] = v
  227. merge_extra_info(h)
  228. save(:validate => false)
  229. end
  230. private :clear_extra_info_of_changesets
  231. def clear_changesets
  232. super
  233. clear_extra_info_of_changesets
  234. end
  235. private :clear_changesets
  236. end