openSUSE/open-build-service

View on GitHub
src/api/app/helpers/maintenance_helper.rb

Summary

Maintainability
D
1 day
Test Coverage
A
96%
module MaintenanceHelper
  include ValidationHelper

  class MissingAction < APIError
    setup 400, 'The request contains no actions. Submit requests without source changes may have skipped!'
  end

  class MultipleUpdateInfoTemplate < APIError; end

  def _release_product(source_package, target_project, action)
    product_package = Package.find_by_project_and_name(source_package.project.name, '_product')
    # create package container, if missing
    tpkg = create_package_container_if_missing(product_package, '_product', target_project)
    # copy sources
    release_package_copy_sources(action, product_package, '_product', target_project)
    tpkg.project.update_product_autopackages
    tpkg.sources_changed
  end

  def _release_package(source_package, target_project, target_package_name, action, relink)
    # create package container, if missing
    tpkg = create_package_container_if_missing(source_package, target_package_name, target_project)

    links_to_source = false
    if relink
      # detect local links
      begin
        link = source_package.source_file('_link')
        link = Nokogiri::XML(link, &:strict).root
        links_to_source = link['project'].nil? || link['project'] == source_package.project.name
      rescue Backend::Error
        # Ignore this exception on purpose
      end
    end
    if links_to_source
      release_package_relink(link, action, target_package_name, target_project, tpkg)
    else
      # copy sources
      release_package_copy_sources(action, source_package, target_package_name, target_project)
      tpkg.sources_changed
    end
  end

  def release_package(source_package, target, target_package_name, opts = {})
    filter_source_repository = opts[:filter_source_repository]
    filter_architecture      = opts[:filter_architecture]
    multibuild_container     = opts[:multibuild_container]
    action                   = opts[:action]
    setrelease               = opts[:setrelease]
    manual                   = opts[:manual]
    comment                  = opts[:comment]

    comment = "Release request #{action.bs_request.number}" if action && comment.nil?

    target_project = if target.is_a?(Repository)
                       target.project
                     else
                       # project
                       target
                     end
    target_project.check_write_access!
    # lock the scheduler
    target_project.suspend_scheduler(comment)

    if source_package.name.starts_with?('_product:') && target_project.packages.where(name: '_product').count.positive?
      # a master _product container exists, so we need to copy all sources
      _release_product(source_package, target_project, action)
    else
      _release_package(source_package, target_project, target_package_name, action, manual ? nil : true)
    end

    # copy binaries
    u_ids = if target.is_a?(Repository)
              copy_binaries_to_repository(filter_source_repository, filter_architecture, source_package, target, target_package_name, multibuild_container, setrelease)
            else
              copy_binaries(filter_source_repository, filter_architecture, source_package, target_package_name, target_project, multibuild_container, setrelease, manual)
            end

    # create or update main package linking to incident package
    release_package_create_main_package(action.bs_request, source_package, target_package_name, target_project) unless source_package.is_patchinfo? || manual

    # publish incident if source is read protect, but release target is not. assuming it got public now.
    f = source_package.project.flags.find_by_flag_and_status('access', 'disable')
    if f && !target_project.flags.find_by_flag_and_status('access', 'disable')
      source_package.project.flags.delete(f)
      source_package.project.store(comment: 'project becomes public on release action')
      # patchinfos stay unpublished, it is anyway too late to test them now ...
    end

    # release the scheduler lock
    target_project.resume_scheduler(comment)

    u_ids
  end

  def release_package_relink(link, action, target_package_name, target_project, tpkg)
    link.remove_attribute('project') # its a local link, project name not needed
    link['package'] = link['package'].gsub(/\..*/, '') + target_package_name.gsub(/.*\./, '.') # adapt link target with suffix
    link_xml = link.to_xml
    Backend::Connection.put Addressable::URI.escape("/source/#{target_project.name}/#{target_package_name}/_link?rev=repository&user=#{User.session!.login}"), link_xml

    md5 = Digest::MD5.hexdigest(link_xml)
    # commit with noservice parameter
    upload_params = {
      user: User.session!.login,
      cmd: 'commitfilelist',
      noservice: '1',
      comment: "Set local link to #{target_package_name} via maintenance_release request"
    }
    upload_params[:requestid] = action.bs_request.number if action
    upload_path = Addressable::URI.escape("/source/#{target_project.name}/#{target_package_name}")
    upload_path << Backend::Connection.build_query_from_hash(upload_params, %i[user comment cmd noservice requestid])
    answer = Backend::Connection.post upload_path, "<directory> <entry name=\"_link\" md5=\"#{md5}\" /> </directory>"
    tpkg.sources_changed(dir_xml: answer)
  end

  def release_package_create_main_package(request, source_package, target_package_name, target_project)
    base_package_name = target_package_name.gsub(/\.[^.]*$/, '')

    # only if package does not contain a _patchinfo file
    lpkg = nil
    if Package.exists_by_project_and_name(target_project.name, base_package_name, follow_project_links: false)
      lpkg = Package.get_by_project_and_name(target_project.name, base_package_name, use_source: false, follow_project_links: false)
    else
      lpkg = Package.new(name: base_package_name, title: source_package.title, description: source_package.description)
      target_project.packages << lpkg
      lpkg.store
    end
    upload_params = {
      user: User.session!.login,
      rev: 'repository',
      comment: "Set link to #{target_package_name} via maintenance_release request"
    }
    upload_path = Addressable::URI.escape("/source/#{target_project.name}/#{base_package_name}/_link")
    upload_path << Backend::Connection.build_query_from_hash(upload_params, %i[user rev])
    link = "<link package='#{target_package_name}' cicount='copy' />\n"
    md5 = Digest::MD5.hexdigest(link)
    Backend::Connection.put upload_path, link
    # commit
    upload_params[:cmd] = 'commitfilelist'
    upload_params[:noservice] = '1'
    upload_params[:requestid] = request.number if request
    upload_path = Addressable::URI.escape("/source/#{target_project.name}/#{base_package_name}")
    upload_path << Backend::Connection.build_query_from_hash(upload_params, %i[user comment cmd noservice requestid])
    answer = Backend::Connection.post upload_path, "<directory> <entry name=\"_link\" md5=\"#{md5}\" /> </directory>"
    lpkg.sources_changed(dir_xml: answer)
  end

  def release_package_copy_sources(action, source_package, target_package_name, target_project)
    # backend copy of current sources as full copy
    # that means the xsrcmd5 is different, but we keep the incident project anyway.
    cp_params = {
      cmd: 'copy',
      user: User.session!.login,
      oproject: source_package.project.name,
      opackage: source_package.name,
      comment: "Release from #{source_package.project.name} / #{source_package.name}",
      expand: '1',
      withvrev: '1',
      noservice: '1',
      withacceptinfo: '1'
    }
    cp_params[:requestid] = action.bs_request.number if action
    # no permission check here on purpose
    if target_project.is_maintenance_release? && source_package.is_link? && (source_package.linkinfo['project'] == target_project.name &&
             source_package.linkinfo['package'] == target_package_name.gsub(/\.[^.]*$/, ''))
      # link target is equal to release target. So we freeze our link.
      cp_params[:freezelink] = 1
    end
    cp_path = Addressable::URI.escape("/source/#{target_project.name}/#{target_package_name}")
    cp_path << Backend::Connection.build_query_from_hash(cp_params, %i[cmd user oproject
                                                                       opackage comment requestid
                                                                       expand withvrev noservice
                                                                       freezelink withacceptinfo])
    result = Backend::Connection.post(cp_path)
    result = Xmlhash.parse(result.body)
    action.set_acceptinfo(result['acceptinfo']) if action
  end

  def copy_binaries(filter_source_repository, filter_architecture, source_package, target_package_name,
                    target_project, multibuild_container, setrelease, manual)
    update_ids = []
    source_package.project.repositories.each do |source_repo|
      next if filter_source_repository && filter_source_repository != source_repo

      source_repo.release_targets.each do |releasetarget|
        next if manual && releasetarget.trigger != 'manual'

        if releasetarget.target_repository.project == target_project
          u_id = copy_binaries_to_repository(source_repo, filter_architecture, source_package, releasetarget.target_repository,
                                             target_package_name, multibuild_container, setrelease)
          update_ids << u_id if u_id
        end
        # remove maintenance release trigger in source
        next unless releasetarget.trigger == 'maintenance'

        releasetarget.trigger = nil
        releasetarget.save!
        source_repo.project.store
      end
    end
    update_ids
  end

  def copy_binaries_to_repository(source_repository, filter_architecture, source_package, target_repo, target_package_name,
                                  multibuild_container, setrelease)
    # get updateinfo id in case the source package comes from a maintenance project
    u_id = get_updateinfo_id(source_package, target_repo)
    source_package_name = source_package.name
    if multibuild_container.present?
      source_package_name << ':' << multibuild_container
      target_package_name = target_package_name.gsub(/:.*/, '') << ':' << multibuild_container
    end
    source_repository.architectures.each do |arch|
      # user architecture filter
      next if filter_architecture.present? && arch.name != filter_architecture

      # skip automatically because target lacks the architecture
      next unless target_repo.architectures.include?(arch)

      copy_single_binary(arch, target_repo, source_package.project.name, source_package_name,
                         source_repository, target_package_name, u_id, setrelease)
    end
    u_id
  end

  def copy_single_binary(arch, target_repository, source_project_name, source_package_name, source_repo,
                         target_package_name, update_info_id, setrelease)
    cp_params = {
      cmd: 'copy',
      oproject: source_project_name,
      opackage: source_package_name,
      orepository: source_repo.name,
      user: User.session!.login,
      resign: '1'
    }
    cp_params[:setupdateinfoid] = update_info_id if update_info_id
    cp_params[:setrelease] = setrelease if setrelease
    cp_params[:multibuild] = '1' unless source_package_name.include?(':')
    cp_path = Addressable::URI.escape("/build/#{target_repository.project.name}/#{target_repository.name}/#{arch.name}/#{target_package_name}")

    cp_path << Backend::Connection.build_query_from_hash(cp_params, %i[cmd oproject opackage
                                                                       orepository setupdateinfoid
                                                                       resign setrelease multibuild])
    Backend::Connection.post cp_path
  end

  def get_updateinfo_id(source_package, target_repo)
    return unless source_package.is_patchinfo?

    # check for patch name inside of _patchinfo file
    xml = Patchinfo.new.read_patchinfo_xmlhash(source_package)
    e = xml.elements('name')
    patch_name = e ? e.first : ''

    mi = MaintenanceIncident.find_by_db_project_id(source_package.project_id)
    return unless mi

    id_template = '%Y-%C'
    # check for a definition in maintenance project
    a = mi.maintenance_db_project.find_attribute('OBS', 'MaintenanceIdTemplate')
    id_template = a.values[0].value if a

    # expand a possible defined update info template in release target of channel
    project_filter = nil
    prj = source_package.project.parent
    project_filter = prj.maintained_projects.map(&:project) if prj && prj.is_maintenance?
    # prefer a channel in the source project to avoid double hits exceptions
    cts = ChannelTarget.find_by_repo(target_repo, [source_package.project])
    cts = ChannelTarget.find_by_repo(target_repo, project_filter) unless cts.any?
    first_ct = cts.first
    unless cts.all? { |c| c.id_template == first_ct.id_template }
      msg = cts.map { |cti| "#{cti.channel.package.project.name}/#{cti.channel.package.name}" }.join(', ')
      raise MultipleUpdateInfoTemplate, "Multiple channel targets found in #{msg} for repository #{target_repo.project.name}/#{target_repo.name}"
    end
    id_template = cts.first.id_template if cts.first && cts.first.id_template

    mi.getUpdateinfoId(id_template, patch_name)
  end

  def create_package_container_if_missing(source_package, target_package_name, target_project)
    tpkg = nil
    if Package.exists_by_project_and_name(target_project.name, target_package_name, follow_project_links: false)
      tpkg = Package.get_by_project_and_name(target_project.name, target_package_name, use_source: false, follow_project_links: false)
    else
      tpkg = Package.new(name: target_package_name,
                         releasename: source_package.releasename,
                         title: source_package.title,
                         description: source_package.description)
      target_project.packages << tpkg
      if source_package.is_patchinfo?
        # publish patchinfos only
        tpkg.flags.create(flag: 'publish', status: 'enable')
      end
      tpkg.store
    end
    tpkg
  end

  def import_channel(channel, pkg, target_repo = nil)
    channel = REXML::Document.new(channel)

    channel.elements['/channel'].add_element 'target', 'project' => target_repo.project.name, 'repository' => target_repo.name if target_repo

    # replace all project definitions with update projects, if they are defined
    ['//binaries', '//binary'].each do |bin|
      channel.get_elements(bin).each do |b|
        attrib = b.attributes.get_attribute('project')
        prj = Project.get_by_name(attrib.to_s) if attrib
        if defined?(prj) && prj
          a = prj.find_attribute('OBS', 'UpdateProject')
          b.attributes['project'] = a.values[0] if a && a.values[0]
        end
      end
    end

    query = { user: User.session!.login }
    query[:comment] = 'channel import function'
    Backend::Connection.put(pkg.source_path('_channel', query), channel.to_s)

    pkg.sources_changed
    # enforce updated channel list in database:
    pkg.update_backendinfo
  end

  def instantiate_container(project, opackage, opts = {})
    opkg = opackage.origin_container
    pkg_name = opkg_name = opkg.name
    if opkg.is_a?(Package) && opkg.project.is_maintenance_release?
      # strip incident suffix
      pkg_name = opkg.name.gsub(/\.[^.]*$/, '')
    end

    # target packages must not exist yet
    raise PackageAlreadyExists, "package #{opkg.name} already exists" if Package.exists_by_project_and_name(project.name, pkg_name, follow_project_links: false)

    local_linked_packages = {}
    opkg.find_project_local_linking_packages.each do |p|
      lpkg_name = p.name
      if opkg_name != pkg_name && p.is_a?(Package) && p.project.is_maintenance_release?
        # strip incident suffix
        lpkg_name = p.name.gsub(/\.[^.]*$/, '')
        # skip the base links
        next if lpkg_name == p.name
      end
      raise PackageAlreadyExists, "package #{p.name} already exists" if Package.exists_by_project_and_name(project.name, lpkg_name, follow_project_links: false)

      local_linked_packages[lpkg_name] = p
    end

    pkg = project.packages.create(name: pkg_name, title: opkg.title, description: opkg.description)
    pkg.store

    copyopts = { noservice: '1' }
    copyopts[:requestid] = opts[:request].number.to_s if opts[:request]
    copyopts[:comment] << CGI.escape(opts[:comment]) if opts[:comment]
    # makeoriginolder is a poorly choosen name meanwhile, because it is no longer used in backend
    # call. We should replace it by a "service_pack" project kind or attribute.
    if opts[:makeoriginolder]
      # versioned copy
      copyopts[:cmd] = 'copy'
      copyopts[:instantiate] = '1'
      copyopts[:withvrev]    = '1'
      copyopts[:vrevbump]    = '2'
      copyopts[:oproject]    = opkg.project.name
      copyopts[:opackage]    = opkg.name
      copyopts[:user]        = User.session!.login
      copyopts[:comment]     = 'initialize package'
    else
      # simple branch
      copyopts[:cmd] = 'branch'
      copyopts[:oproject] = opkg.project.name
      copyopts[:opackage] = opkg.name
      copyopts[:user]     = User.session!.login
      copyopts[:comment]  = 'initialize package as branch'
    end
    path = pkg.source_path
    path << Backend::Connection.build_query_from_hash(copyopts, %i[user comment cmd noservice requestid
                                                                   makeoriginolder withvrev vrevbump
                                                                   instantiate oproject opackage])
    Backend::Connection.post(path)
    pkg.sources_changed

    # and create the needed local links
    local_linked_packages.each do |lpkg_name, p|
      # create container
      lpkg = project.packages.create(name: lpkg_name, title: p.title, description: p.description)
      lpkg.store

      # copy project local linked packages
      path = lpkg.source_path
      copyopts[:cmd] = 'copy'
      copyopts[:oproject] = p.project.name
      copyopts[:opackage] = p.name
      path << Backend::Connection.build_query_from_hash(copyopts, %i[user cmd noservice requestid
                                                                     oproject opackage])
      Backend::Connection.post path
      # and fix the link
      link_xml = Nokogiri::XML(lpkg.source_file('_link'), &:strict).root
      link_xml.remove_attribute('project') # its a local link, project name not needed
      link_xml['package'] = pkg.name
      Backend::Connection.put lpkg.source_path('_link', user: User.session!.login), link_xml.to_xml
      lpkg.sources_changed
    end
  end
end