src/api/app/models/bs_request_action.rb
class BsRequestAction < ApplicationRecord
#### Includes and extends
include ParsePackageDiff
include BsRequestAction::Errors
#### Constants
VALID_SOURCEUPDATE_OPTIONS = %w[update noupdate cleanup].freeze
TYPES = %w[set_bugowner change_devel delete maintenance_incident maintenance_release release add_role submit].freeze
#### Self config
#### Attributes
#### Associations macros (Belongs to, Has one, Has many)
belongs_to :bs_request, touch: true, optional: true
has_one :bs_request_action_accept_info, dependent: :delete
has_many :comments, as: :commentable, dependent: :destroy
has_one :comment_lock, as: :commentable, dependent: :destroy
belongs_to :target_package_object, class_name: 'Package', foreign_key: 'target_package_id', optional: true
belongs_to :target_project_object, class_name: 'Project', foreign_key: 'target_project_id', optional: true
has_many :bs_request_actions_seen_by_users, dependent: :nullify
has_many :seen_by_users, through: :bs_request_actions_seen_by_users, source: :user
scope :bs_request_ids_of_involved_projects, ->(project_ids) { where(target_project_id: project_ids).select(:bs_request_id) }
scope :bs_request_ids_of_involved_packages, ->(package_ids) { where(target_package_id: package_ids).select(:bs_request_id) }
scope :bs_request_ids_by_source_projects, ->(project_name) { where(source_project: project_name).select(:bs_request_id) }
scope :with_target_package, -> { where.not(target_package_id: nil) }
scope :with_target_project, -> { where.not(target_project_id: nil) }
#### Callbacks macros: before_save, after_save, etc.
#### Scopes (first the default_scope macro if is used)
#### Validations macros
validates :sourceupdate, inclusion: { in: VALID_SOURCEUPDATE_OPTIONS, allow_nil: true }
validate :check_sanity
validates :type, presence: true
before_validation :set_target_associations
after_create :cache_diffs
#### Class methods using self. (public and then private)
def self.type_to_class_name(type_name)
"BsRequestAction#{type_name.classify}".constantize
end
def self.find_sti_class(type_name)
return super if type_name.nil?
type_to_class_name(type_name) || super
end
def self.new_from_xml_hash(hash)
classname = type_to_class_name(hash.delete('type'))
raise ArgumentError, 'unknown type' unless classname
a = classname.new
# now remove things from hash
a.store_from_xml(hash)
raise ArgumentError, "too much information #{hash.inspect}" if hash.present?
a
end
#### To define class methods as private use private_class_method
#### private
#### Instance methods (public and then protected/private)
def minimum_priority
nil
end
def check_sanity
if action_type.in?(%i[submit release maintenance_incident maintenance_release change_devel])
errors.add(:source_project, "should not be empty for #{action_type} requests") if source_project.blank?
errors.add(:source_package, "should not be empty for #{action_type} requests") if !is_maintenance_incident? && source_package.blank?
errors.add(:target_project, "should not be empty for #{action_type} requests") if target_project.blank?
errors.add(:target_package, 'No source changes are allowed, if source and target is identical') if source_package == target_package && source_project == target_project && (sourceupdate || updatelink)
end
errors.add(:target_package, 'is invalid package name') if target_package && !Package.valid_name?(target_package)
errors.add(:source_package, 'is invalid package name') if source_package && !Package.valid_name?(source_package)
errors.add(:target_project, 'is invalid project name') if target_project && !Project.valid_name?(target_project)
errors.add(:source_project, 'is invalid project name') if source_project && !Project.valid_name?(source_project)
errors.add(:source_rev, 'should not be upload') if source_rev == 'upload'
# TODO: to be continued
end
def action_type
self.class.sti_name
end
# convenience functions to check types
def is_submit?
false
end
def is_release?
false
end
def is_maintenance_release?
false
end
def is_maintenance_incident?
false
end
def matches_package?(source_or_target, pkg)
send(:"#{source_or_target}_project") == pkg.project.name && send(:"#{source_or_target}_package") == pkg.name
end
def is_from_remote?
Project.unscoped.is_remote_project?(source_project, skip_access: true)
end
def store_from_xml(hash)
source = hash.delete('source')
if source
self.source_package = source.delete('package')
self.source_project = source.delete('project')
self.source_rev = source.delete('rev')
raise ArgumentError, "too much information #{source.inspect}" if source.present?
end
target = hash.delete('target')
if target
self.target_package = target.delete('package')
self.target_project = target.delete('project')
self.target_releaseproject = target.delete('releaseproject')
self.target_repository = target.delete('repository')
raise ArgumentError, "too much information #{target.inspect}" if target.present?
end
ai = hash.delete('acceptinfo')
if ai
self.bs_request_action_accept_info = BsRequestActionAcceptInfo.new
bs_request_action_accept_info.rev = ai.delete('rev')
bs_request_action_accept_info.srcmd5 = ai.delete('srcmd5')
bs_request_action_accept_info.osrcmd5 = ai.delete('osrcmd5')
bs_request_action_accept_info.xsrcmd5 = ai.delete('xsrcmd5')
bs_request_action_accept_info.oxsrcmd5 = ai.delete('oxsrcmd5')
raise ArgumentError, "too much information #{ai.inspect}" if ai.present?
end
o = hash.delete('options')
if o
self.sourceupdate = o.delete('sourceupdate')
# old form
self.sourceupdate = 'update' if sourceupdate == '1'
# there is mess in old data ;(
self.sourceupdate = nil unless sourceupdate.in?(VALID_SOURCEUPDATE_OPTIONS)
self.updatelink = true if o.delete('updatelink') == 'true'
self.makeoriginolder = o.delete('makeoriginolder')
raise ArgumentError, "too much information #{s.inspect}" if o.present?
end
p = hash.delete('person')
if p
self.person_name = p.delete('name') { raise ArgumentError, 'a person without name' }
self.role = p.delete('role')
raise ArgumentError, "too much information #{p.inspect}" if p.present?
end
g = hash.delete('group')
return unless g
self.group_name = g.delete('name') { raise ArgumentError, 'a group without name' }
raise ArgumentError, 'role already taken' if role
self.role = g.delete('role')
raise ArgumentError, "too much information #{g.inspect}" if g.present?
end
def xml_package_attributes(source_or_target)
attributes = {}
value = send(:"#{source_or_target}_project")
attributes[:project] = value if value.present?
value = send(:"#{source_or_target}_package")
attributes[:package] = value if value.present?
attributes
end
def render_xml_target(node)
attributes = xml_package_attributes('target')
attributes[:releaseproject] = target_releaseproject if target_releaseproject.present?
attributes[:repository] = target_repository if target_repository.present?
node.target(attributes)
end
def render_xml_attributes(node)
return unless action_type.in?(%i[submit release maintenance_incident maintenance_release change_devel])
render_xml_source(node)
render_xml_target(node)
end
def render_xml(builder)
builder.action(type: action_type) do |action|
render_xml_attributes(action)
if sourceupdate || updatelink || makeoriginolder
action.options do
action.sourceupdate(sourceupdate) if sourceupdate
action.updatelink('true') if updatelink
action.makeoriginolder('true') if makeoriginolder
end
end
bs_request_action_accept_info.render_xml(builder) unless bs_request_action_accept_info.nil?
end
end
def fill_acceptinfo(ai)
self.bs_request_action_accept_info = BsRequestActionAcceptInfo.create(ai)
end
def notify_params(ret = {})
ret[:action_id] = id
ret[:type] = action_type.to_s
ret[:sourceproject] = source_project
ret[:sourcepackage] = source_package
ret[:sourcerevision] = source_rev
ret[:person] = person_name
ret[:group] = group_name
ret[:role] = role
ret[:targetproject] = target_project
ret[:targetpackage] = target_package
ret[:targetrepository] = target_repository
ret[:target_releaseproject] = target_releaseproject
ret[:sourceupdate] = sourceupdate
ret[:makeoriginolder] = makeoriginolder
ret[:targetpackage] ||= source_package if action_type == :change_devel
ret.keys.each do |k|
ret.delete(k) if ret[k].nil?
end
ret
end
def contains_change?
sourcediff(nodiff: 1).present?
rescue BsRequestAction::Errors::DiffError
# if the diff can'be created we can't say
# but let's assume the reason for the problem lies in the change
true
end
def sourcediff(_opts = {})
''
end
# Serve the sourcediff to the webui
def webui_sourcediff(opts = {})
opts[:superseded_bs_request_action] = find_action_with_same_target(opts[:diff_to_superseded]) if opts[:diff_to_superseded]
begin
opts[:view] = 'xml'
opts[:withissues] = 1
sd = sourcediff(opts)
rescue DiffError, Project::UnknownObjectError, Package::UnknownObjectError => e
return [{ error: e.message }]
end
sorted_filenames_from_sourcediff(sd)
end
def diff_not_cached(opts = {})
sourcediff_results = webui_sourcediff({ cacheonly: 1, diff_to_superseded: opts[:diff_to_superseded] })
return false if sourcediff_results.present?
errors = sourcediff_results.pluck(:error).compact
errors.any? { |e| e.include?('diff not yet in cache') }
end
def find_action_with_same_target(other_bs_request)
return nil if other_bs_request.blank?
other_bs_request.bs_request_actions.find do |other_bs_request_action|
target_project == other_bs_request_action.target_project &&
target_package == other_bs_request_action.target_package
end
end
def default_reviewers
reviews = []
return reviews unless target_project
tprj = Project.get_by_name(target_project)
raise RemoteTarget, 'No support to target to remote projects. Create a request in remote instance instead.' if tprj.instance_of?(String)
tpkg = nil
if target_package
tpkg = if is_maintenance_release?
# use orignal/stripped name and also GA projects for maintenance packages.
# But do not follow project links, if we have a branch target project, like in Evergreen case
if tprj.find_attribute('OBS', 'BranchTarget')
tprj.packages.find_by_name(target_package.gsub(/\.[^.]*$/, ''))
else
tprj.find_package(target_package.gsub(/\.[^.]*$/, ''))
end
elsif action_type.in?(%i[set_bugowner add_role change_devel delete])
# target must exists
tprj.packages.find_by_name!(target_package)
else
# just the direct affected target
tprj.packages.find_by_name(target_package)
end
elsif source_package
tpkg = tprj.packages.find_by_name(source_package)
end
if source_project
# if the user is not a maintainer if current devel package, the current maintainer gets added as reviewer of this request
reviews.push(tpkg.develpackage) if action_type == :change_devel && tpkg.develpackage && !User.session!.can_modify?(tpkg.develpackage, 1)
unless is_maintenance_release?
# Creating requests from packages where no maintainer right exists will enforce a maintainer review
# to avoid that random people can submit versions without talking to the maintainers
# projects may skip this by setting OBS:ApprovedRequestSource attributes
if source_package
spkg = Package.find_by_project_and_name(source_project, source_package)
if spkg && !User.session!.can_modify?(spkg) && (!spkg.project.find_attribute('OBS', 'ApprovedRequestSource') &&
!spkg.find_attribute('OBS', 'ApprovedRequestSource'))
reviews.push(spkg)
end
else
sprj = Project.find_by_name(source_project)
reviews.push(sprj) if sprj && !User.session!.can_modify?(sprj) && !sprj.find_attribute('OBS', 'ApprovedRequestSource') && !sprj.find_attribute('OBS', 'ApprovedRequestSource')
end
end
end
# find reviewers in target package
# package may exist in linked projects, we take the reviewers from there as default
# avoid the project level maintainers for maintenance_release request, the need to be
# defined in update project
reviews += find_reviewers(tpkg, disable_project: is_maintenance_release?) if tpkg
# project reviewers get added additionaly - might be dups
reviews += find_reviewers(tprj) if tprj
reviews.uniq
end
def request_changes_state(_state)
# only groups care for now
end
def check_maintenance_release(pkg, repo, arch)
binaries = Xmlhash.parse(Backend::Api::BuildResults::Binaries.files(pkg.project.name, repo.name, arch.name, pkg.name))
binary_elements = binaries.elements('binary')
raise BuildNotFinished, "patchinfo #{pkg.name} is not yet build for repository '#{repo.name}'" if binary_elements.empty?
# check that we did not skip a source change of patchinfo
data = Directory.hashed(project: pkg.project.name, package: pkg.name, expand: 1)
verifymd5 = data['srcmd5']
history = Xmlhash.parse(Backend::Api::BuildResults::Binaries.history(pkg.project.name, repo.name, pkg.name, arch.name))
last = history.elements('entry').last
return if last && last['srcmd5'].to_s == verifymd5.to_s
raise BuildNotFinished, "last patchinfo #{pkg.name} is not yet build for repository '#{repo.name}'"
end
def get_releaseproject(_pkg, _tprj)
# only needed for maintenance incidents
nil
end
def execute_accept(_opts)
raise 'Needs to be reimplemented in subclass'
end
# after all actions are executed, the controller calls into every action a cleanup
# the actions can "cache" in the opts their state to avoid duplicated work
def per_request_cleanup(_opts)
# does nothing by default
end
# this is called per action once it's verified that all actions in a request are
# permitted.
def create_post_permissions_hook
# does nothing by default
end
# general source cleanup, used in submit and maintenance_incident actions
def source_cleanup
source_project = Project.find_by_name(self.source_project)
return unless source_project
if (source_project.packages.count == 1 && ::Configuration.cleanup_empty_projects) || !source_package
# remove source project, if this is the only package and not a user's home project
splits = self.source_project.split(':')
return if splits.count == 2 && splits[0] == 'home'
source_project.commit_opts = { comment: bs_request.description, request: bs_request }
source_project.destroy
return "/source/#{self.source_project}"
end
# just remove one package
source_package = source_project.packages.find_by_name!(self.source_package)
source_package.commit_opts = { comment: bs_request.description, request: bs_request }
source_package.destroy
Package.source_path(self.source_project, self.source_package)
end
def create_expand_package(packages, opts = {})
newactions = []
incident_suffix = ''
# The maintenance ID is always the sub project name of the maintenance project
incident_suffix = ".#{source_project.gsub(/.*:/, '')}" if is_maintenance_release?
found_patchinfo = false
new_packages = []
new_targets = []
packages.each do |pkg|
raise RemoteSource unless pkg.is_a?(Package)
# find target via linkinfo or submit to all.
# this is handling local project links for packages with multiple spec files.
tpkg = ltpkg = pkg.name
data = nil
missing_ok_link = false
suffix = ''
tprj = pkg.project
unless is_release?
while tprj == pkg.project
data = Directory.hashed(project: tprj.name, package: ltpkg)
data_linkinfo = data['linkinfo']
tprj = if data_linkinfo
suffix = ltpkg.gsub(/^#{Regexp.escape(data_linkinfo['package'])}/, '')
ltpkg = data_linkinfo['package']
missing_ok_link = true if data_linkinfo['missingok']
Project.get_by_name(data_linkinfo['project'])
end
end
end
tpkg = if target_package
# manual specified
target_package
elsif pkg.releasename && is_maintenance_release?
# incidents created since OBS 2.8 should have this information already.
pkg.releasename
elsif tprj.try(:is_maintenance_incident?) && is_maintenance_release?
# fallback, how can we get rid of it?
data = Directory.hashed(project: tprj.name, package: ltpkg)
data_linkinfo = data['linkinfo']
data_linkinfo['package'] if data_linkinfo
else
# we need to get rid of it again ...
tpkg.gsub(/#{Regexp.escape(suffix)}$/, '') # strip distro specific extension
end
# maintenance incident actions need a releasetarget
releaseproject = get_releaseproject(pkg, tprj)
# overwrite target if defined
tprj = Project.get_by_name(target_project) if target_project
raise UnknownTargetProject, "Package #{pkg.project.name} / #{pkg.name} has no target" unless tprj || is_maintenance_release? || is_release?
# do not allow release requests without binaries
if is_maintenance_release? && pkg.is_patchinfo? && data && !opts[:ignore_build_state]
# check for build state and binaries
results = pkg.project.build_results
raise BuildNotFinished, "The project'#{pkg.project.name}' has no building repositories" unless results
found_patchinfo = check_patchinfo(pkg)
versrel = {}
results.each do |result|
repo = result.attributes['repository']
arch = result.attributes['arch']
if result.attributes['dirty']
raise BuildNotFinished, "The repository '#{pkg.project.name}' / '#{repo}' / #{arch} " \
'needs recalculation by the schedulers'
end
check_repository_published!(result.attributes['state'].value, pkg, repo, arch)
# all versrel are the same
versrel[repo] ||= {}
result.search('status').each do |status|
package = status.attributes['package']
next unless status.attributes['versrel']
vrel = status.attributes['versrel'].value
raise VersionReleaseDiffers, "#{package} has a different version release in same repository" if versrel[repo][package] && versrel[repo][package] != vrel
versrel[repo][package] ||= vrel
end
end
end
# re-route (for the kgraft case building against GM or former incident)
if is_maintenance_release? && tprj
tprj = tprj.update_instance_or_self
if tprj.is_maintenance_incident?
release_target = nil
pkg.project.repositories.includes(:release_targets).find_each do |repo|
repo.release_targets.each do |rt|
next if rt.trigger != 'maintenance'
next unless rt.target_repository.project.is_maintenance_release?
raise MultipleReleaseTargets if release_target && release_target != rt.target_repository.project
release_target = rt.target_repository.project
end
end
raise InvalidReleaseTarget unless release_target
tprj = release_target
end
end
# Will this be a new package ?
# check if the main package container exists in target.
# take into account that an additional local link with spec file might got added
if !missing_ok_link && !(data_linkinfo && tprj && tprj.exists_package?(ltpkg, follow_project_links: true, allow_remote_packages: false))
if is_maintenance_release? || is_release?
pkg.project.repositories.includes(:release_targets).find_each do |repo|
repo.release_targets.each do |rt|
new_targets << rt.target_repository.project
end
end
new_packages << pkg
next
end
raise UnknownTargetPackage if !is_maintenance_incident? && !is_submit?
end
# call dup to work on a copy of self
new_action = dup
new_action.source_package = pkg.name
if is_maintenance_incident?
new_targets << tprj if tprj
new_action.target_releaseproject = releaseproject.name if releaseproject
elsif !pkg.is_channel?
new_targets << tprj
new_action.target_project = tprj.name
new_action.target_package = tpkg + incident_suffix
end
if is_maintenance_release? || is_release?
if pkg.is_channel?
if new_action.source_rev.blank?
# set revision
dir = Xmlhash.parse(Backend::Api::Sources::Package.files(new_action.source_project, new_action.source_package, { expand: 1 }))
new_action.source_rev = dir['srcmd5']
end
# create submit request for possible changes in the _channel file
submit_action = create_submit_action(source_package: new_action.source_package, source_project: new_action.source_project,
target_package: tpkg, target_project: tprj.name, source_rev: new_action.source_rev)
# replace the new action
new_action.destroy
new_action = submit_action
else # non-channel package
next unless has_matching_target?(pkg.project, tprj)
unless pkg.project.can_be_released_to_project?(tprj)
raise WrongLinkedPackageSource, 'According to the source link of package ' \
"#{pkg.project.name}/#{pkg.name} it would go to project" \
"#{tprj.name} which is not specified as release target."
end
end
end
# no action, nothing to do
next unless new_action
# check if the source contains really a diff or we can skip the entire action
if new_action.action_type.in?(%i[submit maintenance_incident]) && !new_action.contains_change?
# submit contains no diff, drop it again
new_action.destroy
else
newactions << new_action
end
end
raise MissingPatchinfo if is_maintenance_release? && !found_patchinfo && !opts[:ignore_build_state]
# new packages (eg patchinfos) go to all target projects by default in maintenance requests
new_targets.uniq!
new_packages.uniq!
new_packages.each do |pkg|
release_targets = pkg.is_patchinfo? ? Patchinfo.new.fetch_release_targets(pkg) : nil
new_targets.each do |new_target_project|
next if release_targets.present? && !release_targets.any? { |rt| rt['project'] == new_target_project.name }
# skip if there is no active maintenance trigger for this package
next if is_maintenance_release? && !has_matching_target?(pkg.project, new_target_project)
if is_release?
# unfiltered release actions got to all release targets in addition
pkg.project.repositories.includes(:release_targets).find_each do |repo|
repo.release_targets.each do |rt|
next unless rt.trigger == 'manual'
next if target_project.present? && rt.target_repository.project.name != target_project
new_action = dup
new_action.source_project = pkg.project.name
new_action.source_package = pkg.name
new_action.target_project = rt.target_repository.project.name
new_action.target_package = pkg.name
new_action.target_repository = rt.target_repository.name
newactions << new_action
end
end
else
new_action = dup
new_action.source_package = pkg.name
unless is_maintenance_incident?
new_action.target_project = new_target_project
new_action.target_package = pkg.name + incident_suffix
end
newactions << new_action
end
end
end
newactions
end
def check_action_permission!(skip_source = nil)
# find objects if specified or report error
role = nil
sprj = nil
if person_name
# validate user object
User.find_by_login!(person_name)
end
if group_name
# validate group object
Group.find_by_title!(group_name)
end
if self.role
# validate role object
role = Role.find_by_title!(self.role)
end
sprj = check_action_permission_source! unless skip_source
tprj = check_action_permission_target!
# Type specific checks
if action_type == :delete || action_type == :add_role || action_type == :set_bugowner
# check existence of target
raise UnknownProject, 'No target project specified' unless tprj
raise UnknownRole, 'No role specified' if action_type == :add_role && !role
elsif action_type.in?(%i[submit change_devel maintenance_release maintenance_incident release])
# check existence of source
unless sprj || skip_source
# no support for remote projects yet, it needs special support during accept as well
raise UnknownProject, 'No target project specified'
end
if is_maintenance_incident?
raise IllegalRequest, 'Maintenance requests accept only projects as target' if target_package
raise 'We should have expanded a target_project' unless target_project
# validate project type
prj = Project.get_by_name(target_project)
raise IncidentHasNoMaintenanceProject, 'incident projects shall only create below maintenance projects' unless prj.kind.in?(%w[maintenance maintenance_incident])
end
if action_type == :submit && tprj.is_a?(Project)
at = AttribType.find_by_namespace_and_name!('OBS', 'MakeOriginOlder')
self.makeoriginolder = true if tprj.attribs.find_by(attrib_type: at)
end
# allow cleanup only, if no devel package reference
raise NotSupported, "Source project #{source_project} is not a local project. cleanup is not supported." if sourceupdate == 'cleanup' && sprj.class != Project && !skip_source
raise UnknownPackage, 'No target package specified' if action_type == :change_devel && !target_package
end
check_permissions!
end
def expand_targets(ignore_build_state, ignore_delegate)
expand_target_project if action_type == :submit && ignore_delegate.blank? && target_project.present?
# empty submission protection
if action_type.in?(%i[submit maintenance_incident]) && (target_package &&
Package.exists_by_project_and_name(target_project, target_package, follow_project_links: false))
raise MissingAction unless contains_change?
return
end
# complete in formation available already?
return if action_type.in?(%i[submit release maintenance_release]) && target_package
if action_type.in?(%i[release maintenance_incident]) && target_releaseproject && source_package
pkg = Package.get_by_project_and_name(source_project, source_package)
prj = Project.get_by_name(target_releaseproject).update_instance_or_self
self.target_releaseproject = prj.name
get_releaseproject(pkg, prj) if pkg
return
end
if action_type.in?(%i[submit release maintenance_release maintenance_incident])
packages = []
if source_package
packages << Package.get_by_project_and_name(source_project, source_package)
else
packages = Project.get_by_name(source_project).packages
end
return create_expand_package(packages, ignore_build_state: ignore_build_state)
end
nil
end
# Follow project links for a target project that delegates requests
def expand_target_project
tprj = Project.get_by_name(target_project)
return unless tprj.is_a?(Project) && tprj.delegates_requests?
return unless Package.exists_by_project_and_name(target_project,
target_package || source_package,
{ follow_multibuild: true,
check_update_project: true })
tpkg = Package.get_by_project_and_name(target_project,
target_package || source_package,
{ follow_multibuild: true,
check_update_project: true })
self.target_project = tpkg.project.update_instance_or_self.name
end
def source_access_check!
sp = Package.find_by_project_and_name(source_project, source_package)
if sp.nil?
# either not there or read permission problem
if Package.exists_on_backend?(source_package, source_project)
# user is not allowed to read the source, but when they can write
# the target, the request creator (who must have permissions to read source)
# wanted the target owner to review it
tprj = Project.find_by_name(target_project)
if tprj.nil? || !User.possibly_nobody.can_modify?(tprj)
# produce an error for the source
Package.get_by_project_and_name(source_project, source_package)
end
return
end
if Project.exists_by_name(source_project)
# it is a remote project
return
end
# produce the same exception for webui
Package.get_by_project_and_name(source_project, source_package)
end
if sp.instance_of?(String)
# a remote package
return
end
sp.check_source_access!
end
def check_for_expand_errors!(add_revision)
return unless action_type.in?(%i[submit release maintenance_incident maintenance_release])
# validate that the sources are not broken
begin
query = {}
query[:expand] = 1 unless updatelink
query[:rev] = source_rev if source_rev
dir = Xmlhash.parse(Backend::Api::Sources::Package.files(source_project, source_package, query))
# Enforce revisions?
tprj = Project.get_by_name(target_project)
if tprj.instance_of?(Project) && tprj.find_attribute('OBS', 'EnforceRevisionsInRequests').present?
raise ExpandError, 'updatelink option is forbidden for requests against projects with the attribute OBS:EnforceRevisionsInRequests' if updatelink
# fix the revision to the expanded sources at the time of submission
self.source_rev = dir['srcmd5']
end
if add_revision && !source_rev
if action_type == :maintenance_release && dir.elements('entry').any? { |e| e['name'] == '_patchinfo' }
# patchinfos in release requests get not frozen to allow to modify meta data
return
end
self.source_rev = dir['srcmd5']
end
rescue Backend::Error
revision_message = " for revision #{source_rev}" if source_rev
raise ExpandError, "The source of package #{source_project}/#{source_package}#{revision_message} is broken"
end
end
def is_source_maintainer?(user)
user && user.can_modify?(source_package_object || source_project_object)
end
def is_target_maintainer?(user)
user && user.can_modify?(target_package_object || target_project_object)
end
def cleanup_sourceupdate(user)
return if sourceupdate || %i[submit maintenance_incident].exclude?(action_type)
update(sourceupdate: 'cleanup') if target_project && user.branch_project_name(target_project) == source_project
end
def uniq_key
' '
end
def name
uniq_key
end
def short_name
''
end
def commit_details
package = Package.find_by_project_and_name(source_project, source_package)
return nil if package.nil? || source_rev.nil?
package.commit(source_rev) || package.commit
end
def toggle_seen_by(user)
return unless user.is_a?(User)
if seen_by_users.exists?({ id: user.id })
seen_by_users.destroy(user)
else
seen_by_users << user
end
end
def event_parameters
params = { id: bs_request.id,
number: bs_request.number,
description: bs_request.description,
state: bs_request.state,
when: bs_request.updated_when.strftime('%Y-%m-%dT%H:%M:%S'),
comment: bs_request.comment,
author: bs_request.creator,
namespace: bs_request.namespace }
params[:oldstate] = bs_request.state_was if bs_request.state_changed?
params[:who] = bs_request.commenter if bs_request.commenter.present?
params[:actions] = [notify_params]
params
end
def tab_visibility
BsRequestActionTabVisibility.new(self)
end
def involves_hidden_project?
Project.unscoped.find_by(name: source_project)&.disabled_for?('access', nil, nil)
end
def embargo_date
Project.unscoped.find_by(name: source_project)&.embargo_date
end
private
def cache_diffs
# It's to avoid unnecessary backend calls in test suite. If `global_write_through` is enabled, it will affect a major
# part of the test suite and requires to update 100's of VCR cassettes.
# global_write_through is only disabled in test env. Otherwise, it's always enabled.
return unless CONFIG['global_write_through']
cleanup_sourceupdate(User.session!)
BsRequestActionWebuiInfosJob.perform_later(self)
end
def create_submit_action(source_package:, source_project:, target_package:, target_project:,
source_rev:)
submit_action = BsRequestActionSubmit.new
submit_action.source_package = source_package
submit_action.source_project = source_project
submit_action.target_package = target_package
submit_action.target_project = target_project
submit_action.source_rev = source_rev
submit_action
end
def check_patchinfo(pkg)
pkg.project.repositories.collect do |repo|
firstarch = repo.architectures.first
next unless firstarch
# skip excluded patchinfos
status = pkg.project.project_state.search("/resultlist/result[@repository='#{repo.name}' and @arch='#{firstarch.name}']").first
if status
s = status.search("status[@package='#{pkg.name}']").first
next if s && s.attributes['code'].value == 'excluded'
raise BuildNotFinished, "patchinfo #{pkg.name} is broken" if s && s.attributes['code'].value == 'broken'
end
check_maintenance_release(pkg, repo, firstarch)
true
end.any?
end
def check_repository_published!(state, pkg, repo, arch)
raise BuildNotFinished, check_repository_published_error_message('publish', pkg.project.name, repo, arch) if state.in?(%w[finished publishing])
raise BuildNotFinished, check_repository_published_error_message('build', pkg.project.name, repo, arch) unless state.in?(%w[published unpublished])
end
def check_repository_published_error_message(state, prj, repo, arch)
"The repository '#{prj}' / '#{repo}' / #{arch} did not finish the #{state} yet"
end
def has_matching_target?(source_project, target_project)
ReleaseTarget.exists?(repository: source_project.repositories,
target_repository: target_project.repositories,
trigger: 'maintenance')
end
def check_action_permission_source!
return unless source_project
sprj = Project.get_by_name(source_project)
raise UnknownProject, "Unknown source project #{source_project}" unless sprj
raise NotSupported, "Source project #{source_project} is not a local project. This is not supported yet." unless sprj.instance_of?(Project) || action_type.in?(%i[submit maintenance_incident])
if source_package
spkg = Package.get_by_project_and_name(source_project, source_package)
spkg.check_weak_dependencies! if spkg && sourceupdate == 'cleanup'
end
check_permissions_for_sources!
sprj
end
def check_action_permission_target!
return unless target_project
tprj = Project.get_by_name(target_project)
if tprj.is_a?(Project)
if tprj.is_maintenance_release? && action_type == :submit &&
!tprj.find_attribute('OBS', 'AllowSubmitToMaintenanceRelease')
raise SubmitRequestRejected, "The target project #{target_project} is a maintenance release project, " \
'a submit self is not possible, please use the maintenance workflow instead.'
end
if tprj.scmsync.present?
raise RequestRejected,
"The target project #{target_project} is managed in an external SCM: #{tprj.scmsync}"
end
a = tprj.find_attribute('OBS', 'RejectRequests')
raise RequestRejected, "The target project #{target_project} is not accepting requests because: #{a.values.first.value}" if a && a.values.first && (a.values.length < 2 || a.values.find_by_value(action_type))
end
if target_package
if Package.exists_by_project_and_name(target_project, target_package) ||
action_type.in?(%i[delete change_devel add_role set_bugowner])
tpkg = Package.get_by_project_and_name(target_project, target_package)
end
if defined?(tpkg) && tpkg
if tpkg.scmsync.present?
raise RequestRejected,
"The target package #{target_project} #{target_package} is managed in an external SCM: #{tpkg.scmsync}"
end
a = tpkg.find_attribute('OBS', 'RejectRequests')
if a && a.values.first && (a.values.length < 2 || a.values.find_by_value(action_type))
raise RequestRejected, "The target package #{target_project} / #{target_package} is not accepting " \
"requests because: #{a.values.first.value}"
end
end
end
tprj
end
def check_permissions!
# to be overloaded in action classes if needed
end
def check_permissions_for_sources!
return unless sourceupdate.in?(%w[update cleanup]) || updatelink
source_object = Package.find_by_project_and_name(source_project, source_package) ||
Project.get_by_name(source_project)
raise LackingMaintainership if !source_object.is_a?(String) && !User.possibly_nobody.can_modify?(source_object)
end
# find default reviewers of a project/package via role
def find_reviewers(obj, disable_project: false)
# obj can be a project or package object
reviewer_id = Role.hashed['reviewer'].id
# check for reviewers in a package first
reviewers = obj.relationships.users.where(role_id: reviewer_id).pluck(:user_id).map do |r|
User.find(r)
end
obj.relationships.groups.where(role_id: reviewer_id).pluck(:group_id).each do |r|
reviewers << Group.find(r)
end
reviewers += find_reviewers(obj.project) if obj.instance_of?(Package) && !disable_project
reviewers
end
def render_xml_source(node)
attributes = xml_package_attributes('source')
attributes[:rev] = source_rev if source_rev.present?
node.source(attributes)
end
def source_package_object
@source_package_object ||= Package.find_by_project_and_name(source_project, source_package)
end
def source_project_object
@source_project_object ||= Project.find_by_name(source_project)
end
def set_target_associations
self.target_package_object = Package.find_by_project_and_name(target_project, target_package)
self.target_project_object = Project.find_by_name(target_project)
end
#### Alias of methods
end
# == Schema Information
#
# Table name: bs_request_actions
#
# id :integer not null, primary key
# group_name :string(255)
# makeoriginolder :boolean default(FALSE)
# person_name :string(255)
# role :string(255)
# source_package :string(255) indexed
# source_project :string(255) indexed
# source_rev :string(255)
# sourceupdate :string(255)
# target_package :string(255) indexed
# target_project :string(255) indexed
# target_releaseproject :string(255)
# target_repository :string(255)
# type :string(255)
# updatelink :boolean default(FALSE)
# created_at :datetime
# bs_request_id :integer indexed, indexed => [target_package_id], indexed => [target_project_id]
# target_package_id :integer indexed => [bs_request_id], indexed
# target_project_id :integer indexed => [bs_request_id], indexed
#
# Indexes
#
# bs_request_id (bs_request_id)
# index_bs_request_actions_on_bs_request_id_and_target_package_id (bs_request_id,target_package_id)
# index_bs_request_actions_on_bs_request_id_and_target_project_id (bs_request_id,target_project_id)
# index_bs_request_actions_on_source_package (source_package)
# index_bs_request_actions_on_source_project (source_project)
# index_bs_request_actions_on_target_package (target_package)
# index_bs_request_actions_on_target_package_id (target_package_id)
# index_bs_request_actions_on_target_project (target_project)
# index_bs_request_actions_on_target_project_id (target_project_id)
#
# Foreign Keys
#
# bs_request_actions_ibfk_1 (bs_request_id => bs_requests.id)
#