PageRenderTime 58ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/app/models/repository.rb

https://gitlab.com/MichelZuniga/openproject
Ruby | 417 lines | 262 code | 68 blank | 87 comment | 21 complexity | 9b95aa2dc00a8bd7cd93d46345cfb36a MD5 | raw file
  1. #-- encoding: UTF-8
  2. #-- copyright
  3. # OpenProject is a project management system.
  4. # Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
  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 version 3.
  8. #
  9. # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
  10. # Copyright (C) 2006-2013 Jean-Philippe Lang
  11. # Copyright (C) 2010-2013 the ChiliProject Team
  12. #
  13. # This program is free software; you can redistribute it and/or
  14. # modify it under the terms of the GNU General Public License
  15. # as published by the Free Software Foundation; either version 2
  16. # of the License, or (at your option) any later version.
  17. #
  18. # This program is distributed in the hope that it will be useful,
  19. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. # GNU General Public License for more details.
  22. #
  23. # You should have received a copy of the GNU General Public License
  24. # along with this program; if not, write to the Free Software
  25. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  26. #
  27. # See doc/COPYRIGHT.rdoc for more details.
  28. #++
  29. class Repository < ActiveRecord::Base
  30. include Redmine::Ciphering
  31. include OpenProject::Scm::ManageableRepository
  32. belongs_to :project
  33. has_many :changesets, -> {
  34. order("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC")
  35. }
  36. before_save :sanitize_urls
  37. # Managed repository lifetime
  38. after_create :create_managed_repository, if: Proc.new { |repo| repo.managed? }
  39. after_destroy :delete_managed_repository, if: Proc.new { |repo| repo.managed? }
  40. # Raw SQL to delete changesets and changes in the database
  41. # has_many :changesets, :dependent => :destroy is too slow for big repositories
  42. before_destroy :clear_changesets
  43. attr_protected :project_id
  44. validates_length_of :password, maximum: 255, allow_nil: true
  45. validate :validate_enabled_scm, on: :create
  46. def changes
  47. Change.where(changeset_id: changesets).joins(:changeset)
  48. end
  49. # Checks if the SCM is enabled when creating a repository
  50. def validate_enabled_scm
  51. errors.add(:type, :invalid) unless Setting.enabled_scm.include?(self.class.name.demodulize)
  52. end
  53. # Removes leading and trailing whitespace
  54. def url=(arg)
  55. write_attribute(:url, arg ? arg.to_s.strip : nil)
  56. end
  57. # Removes leading and trailing whitespace
  58. def root_url=(arg)
  59. write_attribute(:root_url, arg ? arg.to_s.strip : nil)
  60. end
  61. def password
  62. read_ciphered_attribute(:password)
  63. end
  64. def password=(arg)
  65. write_ciphered_attribute(:password, arg)
  66. end
  67. def scm_adapter
  68. self.class.scm_adapter_class
  69. end
  70. def scm
  71. @scm ||= scm_adapter.new(url, root_url,
  72. login, password, path_encoding)
  73. # override the adapter's root url with the full url
  74. # if none other was set.
  75. unless @scm.root_url.present?
  76. @scm.root_url = root_url.presence || url
  77. end
  78. @scm
  79. end
  80. def self.scm_config
  81. scm_adapter_class.config
  82. end
  83. def self.available_types
  84. supported_types - disabled_types
  85. end
  86. ##
  87. # Retrieves the :disabled_types setting from `configuration.yml
  88. def self.disabled_types
  89. scm_config[:disabled_types] || []
  90. end
  91. def vendor
  92. self.class.vendor
  93. end
  94. def supports_cat?
  95. scm.supports_cat?
  96. end
  97. def supports_annotate?
  98. scm.supports_annotate?
  99. end
  100. def supports_all_revisions?
  101. true
  102. end
  103. def supports_directory_revisions?
  104. false
  105. end
  106. def entry(path = nil, identifier = nil)
  107. scm.entry(path, identifier)
  108. end
  109. def entries(path = nil, identifier = nil)
  110. scm.entries(path, identifier)
  111. end
  112. def branches
  113. scm.branches
  114. end
  115. def tags
  116. scm.tags
  117. end
  118. def default_branch
  119. scm.default_branch
  120. end
  121. def properties(path, identifier = nil)
  122. scm.properties(path, identifier)
  123. end
  124. def cat(path, identifier = nil)
  125. scm.cat(path, identifier)
  126. end
  127. def diff(path, rev, rev_to)
  128. scm.diff(path, rev, rev_to)
  129. end
  130. def diff_format_revisions(cs, cs_to, sep = ':')
  131. text = ''
  132. text << cs_to.format_identifier + sep if cs_to
  133. text << cs.format_identifier if cs
  134. text
  135. end
  136. # Returns a path relative to the url of the repository
  137. def relative_path(path)
  138. path
  139. end
  140. ##
  141. # Update the required storage information, when necessary.
  142. # Returns whether an asynchronous count refresh has been requested.
  143. def update_required_storage
  144. if scm.storage_available?
  145. oldest_cachable_time = Setting.repository_storage_cache_minutes.to_i.minutes.ago
  146. if storage_updated_at.nil? ||
  147. storage_updated_at < oldest_cachable_time
  148. Delayed::Job.enqueue ::Scm::StorageUpdaterJob.new(self)
  149. return true
  150. end
  151. end
  152. false
  153. end
  154. # Finds and returns a revision with a number or the beginning of a hash
  155. def find_changeset_by_name(name)
  156. name = name.to_s
  157. return nil if name.blank?
  158. changesets.where((name.match(/\A\d*\z/) ? ['revision = ?', name] : ['revision LIKE ?', name + '%'])).first
  159. end
  160. def latest_changeset
  161. @latest_changeset ||= changesets.first
  162. end
  163. # Returns the latest changesets for +path+
  164. # Default behaviour is to search in cached changesets
  165. def latest_changesets(path, _rev, limit = 10)
  166. if path.blank?
  167. changesets.includes(:user)
  168. .order("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC")
  169. .limit(limit)
  170. else
  171. changesets.includes(changeset: :user)
  172. .where(['path = ?', path.with_leading_slash])
  173. .order("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC")
  174. .limit(limit)
  175. .map(&:changeset)
  176. end
  177. end
  178. def scan_changesets_for_work_package_ids
  179. changesets.each(&:scan_comment_for_work_package_ids)
  180. end
  181. # Returns an array of committers usernames and associated user_id
  182. def committers
  183. @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
  184. end
  185. # Maps committers username to a user ids
  186. def committer_ids=(h)
  187. if h.is_a?(Hash)
  188. committers.each do |committer, user_id|
  189. new_user_id = h[committer]
  190. if new_user_id && (new_user_id.to_i != user_id.to_i)
  191. new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
  192. Changeset.where(['repository_id = ? AND committer = ?', id, committer])
  193. .update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
  194. end
  195. end
  196. @committers = nil
  197. @found_committer_users = nil
  198. true
  199. else
  200. false
  201. end
  202. end
  203. # Returns the Redmine User corresponding to the given +committer+
  204. # It will return nil if the committer is not yet mapped and if no User
  205. # with the same username or email was found
  206. def find_committer_user(committer)
  207. unless committer.blank?
  208. @found_committer_users ||= {}
  209. return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
  210. user = nil
  211. c = changesets.includes(:user).references(:users).find_by(committer: committer)
  212. if c && c.user
  213. user = c.user
  214. elsif committer.strip =~ /\A([^<]+)(<(.*)>)?\z/
  215. username = $1.strip
  216. email = $3
  217. u = User.find_by_login(username)
  218. u ||= User.find_by_mail(email) unless email.blank?
  219. user = u
  220. end
  221. @found_committer_users[committer] = user
  222. user
  223. end
  224. end
  225. def repo_log_encoding
  226. encoding = log_encoding.to_s.strip
  227. encoding.blank? ? 'UTF-8' : encoding
  228. end
  229. # Fetches new changesets for all repositories of active projects
  230. # Can be called periodically by an external script
  231. # eg. ruby script/runner "Repository.fetch_changesets"
  232. def self.fetch_changesets
  233. Project.active.has_module(:repository).includes(:repository).each do |project|
  234. if project.repository
  235. begin
  236. project.repository.fetch_changesets
  237. rescue OpenProject::Scm::Exceptions::CommandFailed => e
  238. logger.error "scm: error during fetching changesets: #{e.message}"
  239. end
  240. end
  241. end
  242. end
  243. # scan changeset comments to find related and fixed work packages for all repositories
  244. def self.scan_changesets_for_work_package_ids
  245. all.each(&:scan_changesets_for_work_package_ids)
  246. end
  247. ##
  248. # Builds a model instance of type +Repository::#{vendor}+ with the given parameters.
  249. #
  250. # @param [Project] project The project this repository belongs to.
  251. # @param [String] vendor The SCM vendor name (e.g., Git, Subversion)
  252. # @param [Hash] params Custom parameters for this SCM as delivered from the repository
  253. # field.
  254. #
  255. # @param [Symbol] type SCM tag to determine the type this repository should be built as
  256. #
  257. # @raise [OpenProject::Scm::RepositoryBuildError]
  258. # Raised when the instance could not be built
  259. # given the parameters.
  260. # @raise [::NameError] Raised when the given +vendor+ could not be resolved to a class.
  261. def self.build(project, vendor, params, type)
  262. klass = build_scm_class(vendor)
  263. # We can't possibly know the form fields this particular vendor
  264. # desires, so we allow it to filter them from raw params
  265. # before building the instance with it.
  266. args = klass.permitted_params(params)
  267. repository = klass.new(args)
  268. repository.attributes = args
  269. repository.project = project
  270. set_verified_type!(repository, type) unless type.nil?
  271. repository.configure(type, args)
  272. repository
  273. end
  274. ##
  275. # Build a temporary model instance of the given vendor for temporary use in forms.
  276. # Will not receive any args.
  277. def self.build_scm_class(vendor)
  278. klass = OpenProject::Scm::Manager.registered[vendor]
  279. if klass.nil?
  280. raise OpenProject::Scm::Exceptions::RepositoryBuildError.new(
  281. I18n.t('repositories.errors.disabled_or_unknown_vendor', vendor: vendor)
  282. )
  283. else
  284. klass
  285. end
  286. end
  287. ##
  288. # Verifies that the chosen scm type can be selected
  289. def self.set_verified_type!(repository, type)
  290. if repository.class.available_types.include? type
  291. repository.scm_type = type
  292. else
  293. raise OpenProject::Scm::Exceptions::RepositoryBuildError.new(
  294. I18n.t('repositories.errors.disabled_or_unknown_type',
  295. type: type,
  296. vendor: repository.vendor)
  297. )
  298. end
  299. end
  300. ##
  301. # Allow global permittible params. May be overridden by plugins
  302. def self.permitted_params(params)
  303. params.permit(:url)
  304. end
  305. def self.scm_adapter_class
  306. nil
  307. end
  308. def self.vendor
  309. name.demodulize
  310. end
  311. # Strips url and root_url
  312. def sanitize_urls
  313. url.strip! if url.present?
  314. root_url.strip! if root_url.present?
  315. true
  316. end
  317. def clear_changesets
  318. cs = Changeset.table_name
  319. ch = Change.table_name
  320. ci = "#{table_name_prefix}changesets_work_packages#{table_name_suffix}"
  321. self.class.connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
  322. self.class.connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
  323. self.class.connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
  324. end
  325. private
  326. ##
  327. # Create local managed repository request when the built instance
  328. # is managed by OpenProject
  329. def create_managed_repository
  330. service = Scm::CreateManagedRepositoryService.new(self)
  331. if service.call
  332. true
  333. else
  334. raise OpenProject::Scm::Exceptions::RepositoryBuildError.new(
  335. service.localized_rejected_reason
  336. )
  337. end
  338. end
  339. ##
  340. # Destroy local managed repository request when the built instance
  341. # is managed by OpenProject
  342. def delete_managed_repository
  343. service = Scm::DeleteManagedRepositoryService.new(self)
  344. # Even if the service can't remove the physical repository,
  345. # we should continue removing the associated instance.
  346. service.call
  347. true
  348. end
  349. end