#1478 Simple build from distgit(s), and new copr-distgit-client (sub)package
Merged 3 years ago by praiskup. Opened 3 years ago by praiskup.
Unknown source copr-dist-git-client  into  master

@@ -29,6 +29,10 @@

          # ignore missing-function-docstring in migrations

          function.doc = "fake docs"

  

+     if function.name == "step_impl":

+         # behave step definition

+         function.doc = "fake docs"

+ 

      if is_test_method(function):

          function.doc = "fake docs"

  

@@ -30,7 +30,7 @@

  

  MAX_HOST_ATTEMPTS = 3

  MAX_SSH_ATTEMPTS = 5

- MIN_BUILDER_VERSION = "0.39"

+ MIN_BUILDER_VERSION = "0.40.1.dev"

  CANCEL_CHECK_PERIOD = 5

  

  MESSAGES = {

@@ -10,7 +10,7 @@

      && if test "$COPR_PACKAGES" = devel; then dnf -y copr enable @copr/copr-dev; fi \

      && dnf -y install htop net-tools iputils vim mlocate git sudo \

                openssh-server psmisc python-jedi procps-ng findutils tmux \

-               expect \

+               expect python3-behave python3-hamcrest \

      && dnf -y install python3-copr rpm-build copr-cli jq \

      && dnf -y install rhts-test-env beakerlib \

      && dnf -y clean all

file modified
+1
@@ -83,6 +83,7 @@

  - rpm-build

  - jq

  - python3-copr and copr-cli (tested version)

+ - python3-behave and python3-hamcrest (for the BDD tests)

  

  Next, you cannot run Sanity/copr-cli-basic-operations/runtest.sh which needs

  a root access for many tasks.  That's why you might want to run the test inside

@@ -0,0 +1,25 @@

+ #! /bin/bash

+ #

+ # Copyright (c) 2020 Red Hat, Inc.

+ #

+ # 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, see http://www.gnu.org/licenses/.

+ # Load config settings

+ 

+ HERE=$(dirname "$(realpath "$0")")

+ source "$HERE/config"

+ source "$HERE/helpers"

+ 

+ set -x

+ cd "$HERE/../../../behave" || exit 1

+ behave --tags distgit

@@ -280,15 +280,6 @@

          rlAssertEquals "package.source_type == \"rubygems\"" `cat $OUTPUT | jq '.source_type'` '"rubygems"'

          rlAssertEquals "package.source_dict.gem_name == \"zzz\"" `cat $SOURCE_DICT | jq '.gem_name'` '"zzz"'

  

-         # Packages having all sort of symbols in name, these succeed ..

-         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name gcc-c++ --gem yyy"

-         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name python3-ndg_httpsclient --gem yyy"

-         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name python-boolean.py --gem yyy"

- 

-         # .. and these fail.

-         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name x:x --gem yyy" 1

-         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name x@x --gem yyy" 1

- 

          ## Package listing

          rlAssertEquals "len(package_list) == 2" `copr-cli list-packages ${NAME_PREFIX}Project4 | jq '. | length'` 2

  
@@ -326,6 +317,17 @@

          ## Package listing

          rlAssertEquals "len(package_list) == 3" `copr-cli list-packages ${NAME_PREFIX}Project4 | jq '. | length'` 3

  

+         # Packages having all sort of symbols in name, these succeed ..

+         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name gcc-c++ --gem yyy"

+         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name python3-ndg_httpsclient --gem yyy"

+         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name python-boolean.py --gem yyy"

+ 

+         # .. and these fail.

+         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name x:x --gem yyy" 1

+         rlRun "copr-cli add-package-rubygems ${NAME_PREFIX}Project4 --name x@x --gem yyy" 1

+ 

+         rlAssertEquals "len(package_list) == 3" `copr-cli list-packages ${NAME_PREFIX}Project4 | jq '. | length'` 6

+ 

          ## test package building

          # create special repo for our test

          rlRun "copr-cli create --chroot $CHROOT --chroot fedora-rawhide-x86_64 ${NAME_PREFIX}Project6"

file added
+1
@@ -0,0 +1,1 @@

+ reports/

empty or binary file added
@@ -0,0 +1,144 @@

+ """ helpers for Copr BDD tests """

+ 

+ from contextlib import contextmanager

+ import io

+ import json

+ import pipes

+ import subprocess

+ import sys

+ from urllib.parse import urlparse

+ 

+ 

+ @contextmanager

+ def no_output():

+     """

+     Suppress stdout/stderr when it is not captured by behave

+     https://github.com/behave/behave/issues/863

+     """

+     real_out = sys.stdout, sys.stderr

+     sys.stdout = io.StringIO()

+     sys.stderr = io.StringIO()

+     yield

+     sys.stdout, sys.stderr = real_out

+ 

+ 

+ def quoted_cmd(cmd):

+     """ shell quoted cmd array as string """

+     return " ".join(pipes.quote(arg) for arg in cmd)

+ 

+ 

+ def run(cmd):

+     """

+     Return exitcode, stdout, stderr.  It's bad there's no such thing in behave

+     directly.

+     """

+     process = subprocess.Popen(

+         cmd,

+         stdout=subprocess.PIPE,

+         stderr=subprocess.PIPE,

+         universal_newlines=True,

+     )

+     stdout, stderr = process.communicate()

+     print("Command exit status {} in: {}".format(

+         process.returncode,

+         quoted_cmd(cmd),

+     ))

+     if stdout:

+         print("stdout:")

+         print(stdout)

+     if stderr:

+         print("stderr:")

+         print(stderr)

+     return process.returncode, stdout, stderr

+ 

+ 

+ def run_check(cmd):

+     """

+     run() wrapper with assert on non-zero exit status

+     """

+     rc, out, err = run(cmd)

+     assert rc == 0

+     return out, err

+ 

+ 

+ class CoprCli:

+     """ shortcut for copr --config <config> """

+     def __init__(self, context):

+         self.context = context

+ 

+     @property

+     def _base(self):

+         return ["copr", "--config", self.context.copr_cli_config]

+ 

+     def run(self, args):

+         """

+         Run any cli command.

+         """

+         rc, out, err = run(self._base + args)

+         if rc:

+             print(err)

+         return rc, out, err

+ 

+     def run_build(self, args):

+         """

+         Start build on background, and return build-id

+         """

+         cmd = self._base + args + ["--nowait"]

+         (out, err) = run_check(cmd)

+         for line in out.splitlines():

+             if not line.startswith("Created builds: "):

+                 continue

+             _, _, build_id = line.split(" ")

+             build_id = int(build_id)

+             self.context.builds.append(build_id)

+             return build_id

+         print("stderr:")

+         print(err)

+         raise RuntimeError("can't create build")

+ 

+     def wait_build(self, build_id):

+         """ Wait for the build to finish """

+         cmd = self._base + ["watch-build", str(build_id)]

+         return run(cmd)

+ 

+     def wait_success_build(self, build_id):

+         """ Wait for a successful build to finish """

+         cmd = self._base + ["watch-build", str(build_id)]

+         return run_check(cmd)

+ 

+     def whoami(self):

+         """ get the currently configured user name """

+         out, _ = run_check(self._base + ["whoami"])

+         return out.strip()

+ 

+     def dnf_copr_project(self, owner, project):

+         """ Get the ID we can `dnf copr enable` easily """

+         host = urlparse(self.context.frontend_url).hostname

+         return "{}/{}/{}".format(host, owner, project)

+ 

+     def get_latest_pkg_builds(self, owner, project):

+         """ Get the list of <name>-<version> strings inside copr from builds """

+         cmd = self._base + ["list-packages", "{}/{}".format(owner, project),

+                             "--with-latest-build"]

+         out, _ = run_check(cmd)

+         print("list-packages output:")

+         print(out)

+         packages = []

+         for package in json.loads(out):

+             found_package = package['name']

+             if package.get('latest_build'):

+                 version = package['latest_build']['source_package']['version']

+                 # version has '-RELEASE' suffix

+                 version = version.split('-')[0]

+                 packages.append(found_package + "-" + version)

+                 continue

+             packages.append(found_package)

+         print("Found packages: {}".format(" ".join(packages)))

+         return packages

+ 

+ 

+ def assert_is_subset(set_a, set_b):

+     """ assert that SET_A is subset of SET_B """

+     if set_a.issubset(set_b):

+         return

+     raise AssertionError("Set {} is not a subset of {}".format(set_a, set_b))

@@ -0,0 +1,22 @@

+ @distgit

+ Feature: Building from external DistGit instances

+ 

+     Background:

+         Given a project that builds packages for this system

+ 

+     @builds

+     Scenario: Test that dist-git builds work

+         When a build of Fedora DistGit hello package from master branch is done

+         Then the build results are distributed

+ 

+     @packages

+     Scenario: Test that we can edit dist-git packages

+         When a DistGit CentOS "tar" package from branch "c7" is added

+         And the DistGit package is modified to build from branch "c8"

+         Then the package is configured to build from distgit branch "c8"

+ 

+     @packages @builds

+     Scenario: Test that we can add and build dist-git packages

+         When a DistGit CentOS "setup" package from branch "c8" is added

+         And the package build is requested

+         Then the build results are distributed

@@ -0,0 +1,52 @@

+ """ Configuration for Copr BDD tests. """

+ 

+ import configparser

+ import os

+ import time

+ from urllib.parse import urlparse

+ 

+ from copr_behave_lib import run_check, CoprCli

+ 

+ def _get_mock_target_for_host():

+     cmd = ["rpm", "--eval",

+            "%{?fedora:fedora}%{?rhel:epel}-%{?fedora}%{?rhel}-%_arch"]

+     (out, _) = run_check(cmd)

+     return out.strip()

+ 

+ 

+ def before_all(context):

+     """ Execute once per behave run """

+     context.started = time.time()

+ 

+     context.system_chroot = _get_mock_target_for_host()

+ 

+     context.frontend_url = os.environ.get(

+         "FRONTEND_URL",

+         "https://copr-fe-dev.cloud.fedoraproject.org")

+     context.backend_url = os.environ.get(

+         "BACKEND_URL",

+         "https://copr-be-dev.cloud.fedoraproject.org")

+     context.copr_cli_config = os.environ.get(

+         "COPR_CLI_CONFIG",

+         "~/.config/copr")

+ 

+     context.cli = CoprCli(context)

+     context.builds = []

+     context.last_project_name = None

+     context.last_package_name = None

+ 

+     # check that API points to valid frontend

+     parsed_fronted = urlparse(context.frontend_url)

+     context.copr_cli_config = os.path.expanduser(context.copr_cli_config)

+     if not os.path.exists(context.copr_cli_config):

+         raise Exception("Missing {}".format(context.copr_cli_config))

+     parser = configparser.ConfigParser()

+     parser.read(context.copr_cli_config)

+     api_frontend_url = parser['copr-cli']['copr_url']

+     parsed_api = urlparse(api_frontend_url)

+     if parsed_api.hostname != parsed_fronted.hostname:

+         raise Exception("Url {} from ~/.config/copr isn't {}".format(

+             parsed_api.hostname, parsed_fronted.hostname))

+ 

+ def after_scenario(_context, _scenario):

+     """ hook called after each scenario, hit a debugger here """

@@ -0,0 +1,53 @@

+ """

+ Steps related to Copr builds

+ """

+ 

+ from behave import when, then  # pylint: disable=no-name-in-module

+ 

+ from copr_behave_lib import run_check, run, assert_is_subset

+ 

+ 

+ @when(u'a build of {distgit} DistGit {package_name} package from '

+       u'{committish} {committish_type} is done')

+ def step_impl(context, distgit, package_name, committish, committish_type):

+     _ = (committish_type)

+     distgit = distgit.lower()

+     build = context.cli.run_build([

+         "build-distgit",

+         "--name", package_name,

+         "--distgit", distgit,

+         "--commit", committish,

+         context.last_project_name,

+     ])

+     context.cli.wait_success_build(build)

+ 

+ 

+ @then(u'the build results are distributed')

+ def step_impl(context):

+     # TODO: can we run this in container, and not rely on root access?

+     owner = context.cli.whoami()

+     project = context.last_project_name

+     project_id = context.cli.dnf_copr_project(owner, project)

+     packages = set(context.cli.get_latest_pkg_builds(owner, project))

+     try:

+         run_check(['sudo', 'dnf', '-y', 'copr', 'enable', project_id])

+         (out, _) = run_check([

+             "sudo", "dnf", "repoquery", "--disablerepo=*",

+             "--enablerepo=*{}*".format(project), "--available",

+             "--qf", "%{NAME}-%{VERSION}", "--noplugins",

+         ])

+         packages_found = set(out.strip().splitlines())

+         assert_is_subset(packages, packages_found)

+     finally:

+         # do the tests

+         run(['sudo', 'dnf', 'copr', 'remove', project_id])

+ 

+ 

+ @when(u'the package build is requested')

+ def step_impl(context):

+     build = context.cli.run_build([

+         "build-package",

+         "--name", context.last_package_name,

+         context.last_project_name,

+     ])

+     context.cli.wait_success_build(build)

@@ -0,0 +1,47 @@

+ """

+ Steps that create Copr packages

+ """

+ 

+ import json

+ 

+ from hamcrest import assert_that, equal_to

+ 

+ from behave import when, then  # pylint: disable=no-name-in-module

+ 

+ 

+ @when(u'a DistGit {distgit} "{package}" package from '

+       u'{committype} "{commit}" is added')

+ def step_impl(context, distgit, package, committype, commit):

+     _unused = committype

+     distgit = distgit.strip().lower()

+     status, _, _ = context.cli.run([

+         'add-package-distgit', '--distgit', distgit, '--name', package,

+         '--commit', commit, context.last_project_name])

+     context.last_package_name = package

+     assert status == 0

+ 

+ @when(u'the DistGit package is modified to build from {committype} "{commit}"')

+ def step_impl(context, committype, commit):

+     _unused = committype

+     # a relatively easy package from non-default branch

+     status, _, _ = context.cli.run([

+         'edit-package-distgit', '--distgit', 'centos', '--name',

+         context.last_package_name, '--commit', commit,

+         context.last_project_name])

+     assert status == 0

+ 

+ @then(u'the package is configured to build from distgit {committype} "{commit}"')

+ def step_impl(context, committype, commit):

+     _unused = committype

+     package = context.last_package_name

+     status, out, _ = context.cli.run(['get-package', context.last_project_name,

+                                       '--name', package])

+     assert not status

+     package_info = json.loads(out)

+     assert package_info['source_type'] == "distgit"

+     assert_that(

+         package_info['source_dict'],

+         equal_to({

+             "clone_url": "https://git.centos.org/rpms/{}.git".format(package),

+             "committish": commit, "distgit": "centos",

+         }))

@@ -0,0 +1,30 @@

+ """

+ Steps that create Copr projects

+ """

+ 

+ from behave import given  # pylint: disable=no-name-in-module

+ 

+ from copr_behave_lib import no_output

+ 

+ 

+ def clean_project(context, project):

+     """ Clean copr project by copr-cli """

+     with no_output():

+         rc, out, err = context.cli.run(["delete", project])

+     if rc != 0:

+         print("can not delete the project")

+         if out:

+             print("stdout:\n" + out)

+         if err:

+             print("stderr:\n" + err)

+         assert AssertionError("cli returned {}".format(rc))

+ 

+ 

+ @given(u'a project that builds packages for this system')

+ def step_impl(context):

+     name = context.scenario.name.replace(" ", "-").lower()

+     name = "{}-{}".format(name, context.started)

+     cmd = ["create", name, "--chroot", context.system_chroot]

+     context.cli.run(cmd)

+     context.add_cleanup(clean_project, context, name)

+     context.last_project_name = name

file modified
+1
@@ -66,6 +66,7 @@

  

  subdirs=(

      -e ^backend$

+     -e ^behave$

      -e ^frontend$

      -e ^distgit$

      -e ^cli$

file modified
+16 -9
@@ -133,14 +133,16 @@

          return cmd, env

  

  

- def get_rename_map(options):

+ def get_rename_map(options, subdir):

      """

      Using the os.getcwd() and 'git diff --namestatus', generate list of

      files to analyze with possible overrides.  The returned format is

      dict of format 'new_file' -> 'old_file'.

      """

      cmd = ['git', 'diff', '--name-status', '-C', options.compare_against,

-            '--numstat','--relative', '.']

+            '--numstat', '--relative', os.path.join(".", subdir)]

+ 

+     log.debug("running: %s", " ".join(cmd))

      # current file -> old_name

      return_map = {}

      output = check_output(cmd).decode('utf-8')
@@ -159,6 +161,7 @@

              log.debug("new: %s", parts[1])

              return_map[parts[1]] = None

          elif mode == 'M':

+             log.debug("modified: %s", parts[1])

              return_map[parts[1]] = parts[1]

          else:

              log.info("skipping diff mode %s for file %s", mode, parts[1])
@@ -170,6 +173,7 @@

      gitroot = None

      # relative path within gitroot to the sub-project

      projectdir = None

+     projectsubdir = None

      workdir = None

      checkout = None

      linters = [PylintLinter]
@@ -189,10 +193,12 @@

          def rel_projdir(projdir):

              gitroot_a = os.path.realpath(self.gitroot)

              gitproj_a = os.path.realpath(projdir)

-             rel_projdir = gitproj_a.replace(gitroot_a + '/', '')

-             log.debug("relative projectdir: %s", rel_projdir)

-             return rel_projdir

- 

+             cwd_a = os.path.realpath(os.getcwd())

+             self.projectdir = gitproj_a.replace(gitroot_a + '/', '')

+             self.projectsubdir = (cwd_a + "/").replace(

+                 "{}/{}/".format(gitroot_a, self.projectdir), "")

+             log.debug("relative projectdir: %s", self.projectdir)

+             log.debug("project subdir: %s", self.projectsubdir)

  

          path = os.getcwd()

          while True:
@@ -200,16 +206,16 @@

              if os.path.realpath(path) == '/':

                  raise Exception("project dir not found")

              if os.path.isdir(os.path.join(path, '.git')):

-                 self.projectdir = rel_projdir(path)

+                 rel_projdir(path)

                  return

              if glob.glob(os.path.join(path, '*.spec')):

-                 self.projectdir = rel_projdir(path)

+                 rel_projdir(path)

                  return

              path = os.path.normpath(os.path.join(path, '..'))

  

      def _run_linters(self, old_report_fd, new_report_fd):

          # pylint: disable=too-many-locals

-         lookup = get_rename_map(self.options)

+         lookup = get_rename_map(self.options, self.projectsubdir)

          if not lookup:

              return

  
@@ -269,6 +275,7 @@

                  oldcwd = os.getcwd()

                  try:

                      pd = os.path.join(self.gitroot, self.projectdir)

+                     log.debug("Switching to project directory %s", pd)

                      os.chdir(pd)

                      self._run_linters(old, new)

                  finally:

file modified
+6 -4
@@ -6,6 +6,8 @@

  %global with_python2 1

  %endif

  

+ %global min_python_copr_version 1.105.1.dev

+ 

  Name:       copr-cli

  Version:    1.89

  Release:    1%{?dist}
@@ -28,7 +30,7 @@

  BuildRequires: util-linux

  

  %if %{with python3}

- Requires:      python3-copr >= 1.89

+ Requires:      python3-copr >= %min_python_copr_version

  Requires:      python3-jinja2

  Requires:      python3-simplejson

  Requires:      python3-humanize
@@ -36,7 +38,7 @@

  

  Recommends:    python3-progress

  

- BuildRequires: python3-copr >= 1.89

+ BuildRequires: python3-copr >= %min_python_copr_version

  BuildRequires: python3-devel

  BuildRequires: python3-jinja2

  BuildRequires: python3-humanize
@@ -45,13 +47,13 @@

  BuildRequires: python3-simplejson

  BuildRequires: python3-munch

  %else

- Requires:      python-copr >= 1.89

+ Requires:      python-copr >= %min_python_copr_version

  Requires:      python-jinja2

  Requires:      python-simplejson

  Requires:      python-humanize

  

  BuildRequires: pytest

- BuildRequires: python-copr >= 1.89

+ BuildRequires: python-copr >= %min_python_copr_version

  BuildRequires: python-devel

  BuildRequires: python-jinja2

  BuildRequires: python-humanize

file modified
+106
@@ -318,6 +318,18 @@

          return self.process_build(args, self.client.build_proxy.create_from_scm, data)

  

      @requires_api_auth

+     def action_build_distgit_simple(self, args):

+         """ build-distgit method """

+         data = {

+             "packagename": args.pkgname,

+             "distgit": args.instance,

+             "namespace": args.namespace,

+             "committish": args.committish,

+         }

+         return self.process_build(

+             args, self.client.build_proxy.create_from_distgit, data)

+ 

+     @requires_api_auth

      def action_build_rubygems(self, args):

          """

          Method called when the 'buildgem' action has been selected by the user.
@@ -653,6 +665,26 @@

          print("Create or edit operation was successful.")

  

      @requires_api_auth

+     def action_add_or_edit_package_distgit(self, args):

+         """ process 'add/edit-package-distgit' requests """

+         data = {

+             # package name (args.name) is not needed in data

+             "distgit": args.instance,

+             "namespace": args.namespace,

+             "committish": args.committish,

+             "max_builds": args.max_builds,

+             "webhook_rebuild": ON_OFF_MAP[args.webhook_rebuild],

+         }

+         ownername, projectname = self.parse_name(args.copr)

+         if args.create:

+             self.client.package_proxy.add(ownername, projectname, args.name,

+                                           "distgit", data)

+         else:

+             self.client.package_proxy.edit(ownername, projectname, args.name,

+                                            "distgit", data)

+         print("Create or edit operation was successful.")

+ 

+     @requires_api_auth

      def action_add_or_edit_package_rubygems(self, args):

          ownername, projectname = self.parse_name(args.copr)

          data = {
@@ -1008,6 +1040,27 @@

                                          choices=["rpkg", "tito", "tito_test", "make_srpm"],

                                          help="Srpm build method. Default is 'rpkg'.")

  

+     parser_distgit_simple_parent = argparse.ArgumentParser(add_help=False)

+     parser_distgit_simple_parent.add_argument(

+         "--commit", dest="committish", default=None,

+         help="Branch name, tag name, or git hash to built the package from")

+     parser_distgit_simple_parent.add_argument(

+         "--namespace", dest="namespace", default=None,

+         help=(

+             "Some DistGit instances (e.g. the Fedora Copr dist-git) use "

+             "a namespaced clone/lookaside URLs.  Typically it meas that "

+             "one package may be hosted in the same DistGit instance "

+             "multiple times, in multiple namespaces.  Specify the NAMESPACE "

+             "here (e.g. @copr/copr for @copr/copr/copr-cli package)."),

+     )

+     parser_distgit_simple_parent.add_argument(

+         "--distgit", dest="instance", default=None,

+         help=(

+             "Dist-git instance to build the package from, for example "

+             "'fedora'."

+         ),

+     )

+ 

      parser_rubygems_args_parent = argparse.ArgumentParser(add_help=False)

      parser_rubygems_args_parent.add_argument("--gem", metavar="GEM", dest="gem_name",

                                               help="Specify gem name")
@@ -1090,6 +1143,22 @@

                                                help="Builds package from Git/DistGit/SVN repository.")

      parser_build_scm.set_defaults(func="action_build_scm")

  

+     # create the parser for the "build-distgit" command

+     parser_build_distgit_simple = subparsers.add_parser(

+         "build-distgit",

+         parents=[parser_distgit_simple_parent, parser_build_parent],

+         help="Builds a package from a DistGit repository",

+         description=(

+             "Build a package from a DistGit repository. "

+             "For more info about DistGit build method see the description "

+             "'add-package-distgit' command."),

+     )

+     parser_build_distgit_simple.add_argument(

+         "--name", dest="pkgname", required=True,

+         help=("Package name to build from the DistGit instance"),

+     )

+     parser_build_distgit_simple.set_defaults(func="action_build_distgit_simple")

+ 

      # create the parser for the "status" command

      parser_status = subparsers.add_parser("status", help="Get build status of build specified by its ID")

      parser_status.add_argument("build_id", help="Build ID", type=int)
@@ -1191,6 +1260,43 @@

                                                          parents=[parser_scm_args_parent, parser_add_or_edit_package_parent])

      parser_edit_package_scm.set_defaults(func="action_add_or_edit_package_scm", create=False)

  

+     # DistGit edit/create package

+     parser_add_package_distgit = subparsers.add_parser(

+         "add-package-distgit",

+         help="Creates a new DistGit package",

+         description=(

+             "DistGit (Distribution Git) is Git with additional data "

+             "storage (so called \"lookaside cache\"). It is designed to hold "

+             "content of source RPMs. For more info, see the "

+             "https://github.com/release-engineering/dist-git documentation."

+             "\n\n"

+             "To build a package for a particular distribution, you need "

+             "clone the correct git repository, and download corresponding "

+             "sources files from the lookaside cache.  Each distribution though "

+             "uses a different hostname for DistGit server and may store the "

+             "git repositories and source files on a little bit different "

+             "URIs (or even in NAMESPACEs).  That's why Copr has this "

+             "build method pre-configured as \"DistGit instances\" (one "

+             "instance per one distribution)."

+         ),

+         parents=[parser_distgit_simple_parent,

+                  parser_add_or_edit_package_parent])

+     parser_add_package_distgit.set_defaults(

+         func="action_add_or_edit_package_distgit",

+         create=True)

+     parser_edit_package_distgit = subparsers.add_parser(

+         "edit-package-distgit",

+         help="Edits an existing DistGit package",

+         description=(

+             "Edit an existing DistGit package.  For more info about DistGit "

+             "build method see the description of 'add-package-distgit' "

+             "command."),

+         parents=[parser_distgit_simple_parent,

+                  parser_add_or_edit_package_parent])

+     parser_edit_package_distgit.set_defaults(

+         func="action_add_or_edit_package_distgit",

+         create=False)

+ 

      # Rubygems edit/create

      parser_add_package_rubygems = subparsers.add_parser("add-package-rubygems",

                                                          help="Creates a new RubyGems package",

@@ -0,0 +1,156 @@

+ """

+ Unit tests for the building/defining packages with DistGit method

+ """

+ 

+ import copy

+ 

+ from munch import Munch

+ import pytest

+ 

+ import copr

+ from copr_cli import main

+ from cli_tests_lib import mock

+ 

+ # pylint: disable=unused-import

+ from cli_tests_lib import f_test_config

+ 

+ def _main(args, capsys):

+     main.main(args)

+     return capsys.readouterr()

+ 

+ def _assert_output(args, exp_stdout, exp_stderr, capsys):

+     stdout, stderr = _main(args, capsys)

+     assert exp_stdout == stdout

+     assert exp_stderr == stderr

+ 

+ # pylint: disable=redefined-outer-name,unused-argument,missing-function-docstring

+ 

+ class TestDistGitMethodBuild(object):

+     'Build was added to project:...uild/1\nCreated builds: 1\n'

+     build_1 = (

+         "Build was added to project:\n"

+         "  http://copr/coprs/build/1\n"

+         "Created builds: 1\n"

+     )

+     default_build_call = {

+         'ownername': None,

+         'projectname': 'project',

+         'project_dirname': 'project',

+         'buildopts': {

+             'memory': None,

+             'timeout': None,

+             'chroots': None,

+             'background': False,

+             'progress_callback': None,

+         },

+         'packagename': 'test',

+         'distgit': None,

+         'namespace': None,

+         'committish': None

+     }

+ 

+     @staticmethod

+     @pytest.yield_fixture

+     def f_patch_create_from_distgit(f_test_config, capsys):

+         with mock.patch("copr.v3.proxies.build.BuildProxy.create_from_distgit") as patch:

+             patch.return_value = [Munch({

+                 "id": "1",

+                 "projectname": "project",

+             })]

+             yield patch

+ 

+     def test_normal_distgit_build(self, f_patch_create_from_distgit, capsys):

+         _assert_output(

+             ['build-distgit', '--name', 'test', 'project', '--nowait'],

+             self.build_1, "",

+             capsys)

+         assert len(f_patch_create_from_distgit.call_args_list) == 1

+         call = f_patch_create_from_distgit.call_args_list[0]

+         assert call[1] == self.default_build_call

+ 

+     def test_full_featured_distgit_build(self, f_patch_create_from_distgit,

+                                          capsys):

+         _assert_output(

+             ['build-distgit', '--name', 'test', '@group/project', '--nowait',

+              '--timeout', "3600", '--chroot', 'fedora-rawhide-x86_64',

+              '--distgit', 'centos', '--commit', 'f19', '--namespace',

+              'rpms', "--background"],

+             self.build_1, "",

+             capsys)

+         assert len(f_patch_create_from_distgit.call_args_list) == 1

+         call = f_patch_create_from_distgit.call_args_list[0]

+         result = copy.deepcopy(self.default_build_call)

+         result.update({

+             "ownername": "@group",

+             "committish": "f19",

+             "distgit": "centos",

+             "namespace": "rpms",

+             "buildopts": {

+                 "memory": None,

+                 "timeout": "3600",

+                 "chroots": ['fedora-rawhide-x86_64'],

+                 "background": True,

+                 "progress_callback": None,

+             },

+         })

+         assert call[1] == result

+ 

+ 

+ class TestDistGitMethodPackage(object):

+     success_stdout = "Create or edit operation was successful.\n"

+ 

+     @staticmethod

+     @pytest.yield_fixture

+     def f_patch_package_distgit(f_test_config, capsys):

+         with mock.patch("copr.v3.proxies.package.PackageProxy.add") as p1:

+             with mock.patch("copr.v3.proxies.package.PackageProxy.edit") as p2:

+                 yield p1, p2

+ 

+     def test_add_package_normal(self, f_patch_package_distgit, capsys, ):

+         _assert_output(['add-package-distgit', '--name', 'package',

+                         'project'], self.success_stdout, "", capsys)

+         assert len(f_patch_package_distgit[0].call_args_list) == 1

+         assert len(f_patch_package_distgit[1].call_args_list) == 0

+ 

+         call = f_patch_package_distgit[0].call_args_list[0]

+         assert call == mock.call(

+             call[0][0], None, "project", "package", "distgit",

+             {'distgit': None,

+              'namespace': None,

+              'committish': None,

+              'max_builds': None,

+              'webhook_rebuild': None})

+ 

+     def test_edit_package_full(self, f_patch_package_distgit, capsys):

+         _assert_output(['edit-package-distgit', '--name', 'package', '@owner/project',

+                         '--commit', 'master', '--namespace', 'ns', '--distgit',

+                         'centos', '--webhook-rebuild', "on", "--max-builds",

+                         "1"],

+                        self.success_stdout, "", capsys)

+         assert len(f_patch_package_distgit[1].call_args_list) == 1

+         assert len(f_patch_package_distgit[0].call_args_list) == 0

+ 

+         call = f_patch_package_distgit[1].call_args_list[0]

+         assert call == mock.call(

+             call[0][0], "@owner", "project", "package", "distgit",

+             {'distgit': "centos",

+              'namespace': "ns",

+              'committish': "master",

+              'max_builds': "1",

+              'webhook_rebuild': True})

+ 

+     @staticmethod

+     def test_edit_package_fail(f_test_config, capsys):

+         with mock.patch("copr.v3.proxies.package.PackageProxy.add") as p1:

+             p1.side_effect = copr.v3.CoprRequestException("test")

+             with pytest.raises(SystemExit) as exc:

+                 main.main(['edit-package-distgit', '--name', 'package',

+                            '@owner/project/blah'])

+             assert exc.value.code == 1

+ 

+         out, err = capsys.readouterr()

+         assert out == ""

+         assert err == (

+             "\nSomething went wrong:\n"

+             "Error: Unable to connect to http://copr/api_3/.\n"

+         )

file modified
+11 -3
@@ -97,10 +97,17 @@

      }

  

  

+ def _filtered_status_enum(keys):

+     new_values = {}

+     for key, value in StatusEnum.vals.items():

+         if key in keys:

+             new_values[key] = value

+     return new_values

+ 

+ 

  class ModuleStatusEnum(StatusEnum):

-     vals = {k: v for k, v in StatusEnum.vals.items()

-             if k in ["canceled", "running", "starting", "pending",

-                       "failed", "succeeded", "waiting", "unknown"]}

+     vals = _filtered_status_enum(["canceled", "running", "starting", "pending",

+                                   "failed", "succeeded", "waiting", "unknown"])

  

  

  class BuildSourceEnum(with_metaclass(EnumType, object)):
@@ -111,6 +118,7 @@

              "rubygems": 6, # gem_name

              "scm": 8, # type, clone_url, committish, subdirectory, spec, srpm_build_method

              "custom": 9, # user-provided script to build sources

+             "distgit": 10, # distgit_instance, package_name, committish

             }

  

  

@@ -14,7 +14,7 @@

  %endif

  

  Name:       python-copr-common

- Version:    0.8

+ Version:    0.8.1.dev

  Release:    1%{?dist}

  Summary:    Python code used by Copr

  

@@ -216,6 +216,7 @@

          "pypi": "Build from PyPI",

          "rubygems": "Build from RubyGems",

          "custom": "Custom build method",

+         "distgit": "Build from DistGit",

      }

  

      return description_map.get(state, "")

@@ -1,3 +1,4 @@

+ import os

  import re

  from six.moves.urllib.parse import urlparse

  
@@ -39,6 +40,7 @@

      BasePackageForm child

          based on source_type_text input

      """

+     # pylint: disable=too-many-return-statements

      if source_type_text == 'scm':

          return PackageFormScm

      elif source_type_text == 'pypi':
@@ -51,6 +53,8 @@

          return PackageFormMock # deprecated

      elif source_type_text == "custom":

          return PackageFormCustom

+     elif source_type_text == "distgit":

+         return PackageFormDistGitSimple

      else:

          raise exceptions.UnknownSourceTypeException("Invalid source type")

  
@@ -585,11 +589,17 @@

  

  

  class BasePackageForm(FlaskForm):

+     package_name_regex = r"^[-+_.a-zA-Z0-9]+$"

+ 

      package_name = wtforms.StringField(

          "Package name",

-         validators=[wtforms.validators.Regexp(

-                         re.compile(r"^[-+_.a-zA-Z0-9]+$"),

-                         message="Please enter a valid package name.")])

+         validators=[

+             wtforms.validators.Regexp(

+                 re.compile(package_name_regex),

+                 message="Please enter a valid package name in " \

+                        + package_name_regex)]

+     )

+ 

      webhook_rebuild = wtforms.BooleanField(default=False, false_values=FALSE_VALUES)

      chroot_blacklist = wtforms.StringField(

          "Chroot blacklist",
@@ -884,6 +894,123 @@

          })

  

  

+ class DistGitValidator(object):

+     def __call__(self, form, field):

+         if field.data not in field.distgit_choices:

+             message = "DistGit ID must be one of: {}".format(

+                 ", ".join(field.distgit_choices))

+             raise wtforms.ValidationError(message)

+ 

+ 

+ class NoneFilter():

+     def __init__(self, default):

+         self.default = default

+ 

+     def __call__(self, value):

+         if value in [None, 'None']:

+             return self.default

+         return value

+ 

+ 

+ class DistGitSelectField(wtforms.SelectField):

+     """ Select-box for picking (default) dist git instance """

+ 

+     # pylint: disable=too-few-public-methods

+     def __init__(self, validators=None, filters=None, **kwargs):

+         if not validators:

+             validators = []

+         if not filters:

+             filters = []

+ 

+         self.distgit_choices = [x.name for x in DistGitLogic.ordered().all()]

+         self.distgit_default = self.distgit_choices[0]

+ 

+         validators.append(DistGitValidator())

+         filters.append(NoneFilter(self.distgit_default))

+ 

+         super().__init__(

+             label="DistGit instance",

+             validators=validators,

+             filters=filters,

+             choices=[(x, x) for x in self.distgit_choices],

+             **kwargs,

+         )

+ 

+ 

+ class PackageFormDistGitSimple(BasePackageForm):

+     """

+     This represents basically a variant of the SCM method, but with a very

+     trivial user interface.

+     """

+     distgit = DistGitSelectField()

+ 

+     committish = wtforms.StringField(

+         "Committish",

+         validators=[wtforms.validators.Optional()],

+         render_kw={

+             "placeholder": "Optional - Specific branch, tag, or commit that "

+                            "you want to build from"},

+     )

+ 

+     namespace = wtforms.StringField(

+         "DistGit namespace",

+         validators=[wtforms.validators.Optional()],

+         default=None,

+         filters=[lambda x: None if not x else os.path.normpath(x)],

+         description=(

+             "Some dist-git instances have the git repositories "

+             "namespaced - e.g. you need to specify '@copr/copr' for "

+             "the <a href='https://copr-dist-git.fedorainfracloud.org/"

+             "cgit/@copr/copr/copr-cli.git/tree/copr-cli.spec'>"

+             "@copr/copr/copr-cli</a> Fedora Copr package"),

+         render_kw={

+             "placeholder": "Optional - string, e.g. '@copr/copr'"},

+     )

+ 

+     build_requires_package_name = True

+ 

+     @property

+     def source_json(self):

+         """ Source json stored in DB in Package.source_json """

+         data = {

+             "clone_url": self.clone_url(),

+         }

+ 

+         if self.distgit.data:

+             data["distgit"] = self.distgit.data

+ 

+         for field_name in ["distgit", "namespace", "committish"]:

+             field = getattr(self, field_name)

+             if field.data:

+                 data[field_name] = field.data

+ 

+         return json.dumps(data)

+ 

+     def clone_url(self):

+         """ One-time generate the clone_url from the form data """

+         return DistGitLogic.get_clone_url(self.distgit.data,

+                                           self.package_name.data,

+                                           self.namespace.data)

+ 

+     def validate(self):

+         """

+         Try to check that we can generate clone_url from distgit, namespace and

+         package.  This can not be done by single-field-context validator.

+         """

+         if not super().validate():

+             return False

+ 

+         try:

+             self.clone_url()

+         except Exception as e:  # pylint: disable=broad-except

+             self.distgit.errors.append(

+                 "Can not validate DistGit input: {}".format(str(e))

+             )

+             return False

+ 

+         return True

+ 

+ 

  class RebuildAllPackagesFormFactory(object):

      def __new__(cls, active_chroots, package_names):

          form_cls = BaseBuildFormFactory(active_chroots, FlaskForm)
@@ -928,8 +1055,10 @@

          F.background = wtforms.BooleanField(default=False, false_values=FALSE_VALUES)

          F.project_dirname = wtforms.StringField(default=None)

  

-         # overrides BasePackageForm.package_name and is unused for building

-         F.package_name = wtforms.StringField()

+         # Overrides BasePackageForm.package_name, it is usually unused for

+         # building

+         if not getattr(F, "build_requires_package_name", None):

+             F.package_name = wtforms.StringField()

  

          # fill chroots based on project settings

          F.chroots_list = [x.name for x in active_chroots]
@@ -1000,6 +1129,14 @@

          return BaseBuildFormFactory(active_chroots, PackageFormCustom, package)

  

  

+ class BuildFormDistGitSimpleFactory:

+     """

+     Transform DistGitSimple package form into build form

+     """

+     def __new__(cls, active_chroots, package=None):

+         return BaseBuildFormFactory(active_chroots, PackageFormDistGitSimple,

+                                     package)

+ 

  class BuildFormUrlFactory(object):

      def __new__(cls, active_chroots):

          form = BaseBuildFormFactory(active_chroots, FlaskForm)
@@ -1023,39 +1160,13 @@

      build = wtforms.BooleanField("build", default=True, false_values=FALSE_VALUES)

  

  

- class DistGitValidator(object):

-     def __call__(self, form, field):

-         if field.data not in form.distgit_choices:

-             message = "DistGit ID must be one of: {}".format(

-                 ", ".join(form.distgit_choices))

-             raise wtforms.ValidationError(message)

- 

- 

- class NoneFilter():

-     def __init__(self, default):

-         self.default = default

- 

-     def __call__(self, value):

-         if value in [None, 'None']:

-             return self.default

-         return value

- 

- 

  def get_module_build_form(*args, **kwargs):

      class ModuleBuildForm(FlaskForm):

-         distgit_choices = [x.name for x in DistGitLogic.ordered().all()]

-         distgit_default = distgit_choices[0]

- 

          modulemd = FileField("modulemd")

          scmurl = wtforms.StringField()

          branch = wtforms.StringField()

  

-         distgit = wtforms.SelectField(

-             'Build against DistGit instance',

-             choices=[(x, x) for x in distgit_choices],

-             validators=[DistGitValidator()],

-             filters=[NoneFilter(distgit_default)],

-         )

+         distgit = DistGitSelectField()

  

      return ModuleBuildForm(*args, **kwargs)

  

@@ -1,23 +1,26 @@

  import math

  import random

  import string

+ import json

+ from os.path import normpath

+ import posixpath

+ import re

+ 

  import html5_parser

  

- from os.path import normpath

  from six import with_metaclass

  from six.moves.urllib.parse import urlparse, parse_qs, urlunparse, urlencode

- import re

  

  import flask

- import posixpath

  from flask import url_for

- from dateutil import parser as dt_parser

- from netaddr import IPAddress, IPNetwork

  from redis import StrictRedis

  from sqlalchemy.types import TypeDecorator, VARCHAR

- import json

+ from sqlalchemy.engine.default import DefaultDialect

+ from sqlalchemy.sql.sqltypes import String, DateTime, NullType

  

  from copr_common.enums import EnumType

+ # TODO: don't import BuildSourceEnum from helpers, use copr_common.enum instead

+ from copr_common.enums import BuildSourceEnum # pylint: disable=unused-import

  from copr_common.rpm import splitFilename

  from coprs import app

  
@@ -104,17 +107,6 @@

          return [(n, k) for k, n in cls.vals.items() if n != without]

  

  

- class BuildSourceEnum(with_metaclass(EnumType, object)):

-     vals = {"unset": 0,

-             "link": 1,  # url

-             "upload": 2,  # pkg, tmp, url

-             "pypi": 5, # package_name, version, python_versions

-             "rubygems": 6, # gem_name

-             "scm": 8, # type, clone_url, committish, subdirectory, spec, srpm_build_method

-             "custom": 9, # user-provided script to build sources

-            }

- 

- 

  class JSONEncodedDict(TypeDecorator):

      """Represents an immutable structure as a json-encoded string.

  
@@ -421,9 +413,6 @@

      return copr_url("coprs_ns.copr_builds", copr)

  

  

- from sqlalchemy.engine.default import DefaultDialect

- from sqlalchemy.sql.sqltypes import String, DateTime, NullType

- 

  # python2/3 compatible.

  PY3 = str is not bytes

  text = str if PY3 else unicode

@@ -35,6 +35,7 @@

  from coprs.logic import coprs_logic

  from coprs.logic import users_logic

  from coprs.logic.actions_logic import ActionsLogic

+ from coprs.logic.dist_git_logic import DistGitLogic

  from coprs.models import BuildChroot

  from .coprs_logic import MockChrootsLogic

  from coprs.logic.packages_logic import PackagesLogic
@@ -62,7 +63,7 @@

  

      @classmethod

      def get_srpm_build_tasks(cls, status, background=None):

-         """ Returns srpm build tasks with given status. If background is

+         """ Returns source build tasks with given status. If background is

              specified then returns normal jobs (false) or background jobs (true)

          """

          result = models.Build.query\
@@ -497,6 +498,24 @@

                                chroot_names, copr_dirname=copr_dirname, **kwargs)

  

      @classmethod

+     def create_new_from_distgit(cls, user, copr, package_name,

+                                 distgit_name=None, distgit_namespace=None,

+                                 committish=None, chroot_names=None,

+                                 copr_dirname=None, **build_options):

+         """ Request build of package from DistGit repository """

+         source_type = helpers.BuildSourceEnum("distgit")

+         source_dict = {

+             "clone_url": DistGitLogic.get_clone_url(distgit_name, package_name,

+                                                     distgit_namespace),

+         }

+         if committish:

+             source_dict["committish"] = committish

+ 

+         return cls.create_new(

+             user, copr, source_type, json.dumps(source_dict), chroot_names,

+             copr_dirname=copr_dirname, **build_options)

+ 

+     @classmethod

      def create_new_from_upload(cls, user, copr, f_uploader, orig_filename,

                                 chroot_names=None, copr_dirname=None, **build_options):

          """

@@ -15,3 +15,12 @@

              return cls.ordered().limit(1).one()

          query = models.DistGitInstance.query.filter_by(name=distgit_name)

          return query.one()

+ 

+     @classmethod

+     def get_clone_url(cls, distgit_name, package_name, namespace=None):

+         """

+         Using the DistGit instance name, package name and namespace, generate

+         and return the appropriate git clone URL

+         """

+         distgit = cls.get_with_default(distgit_name)

+         return distgit.package_clone_url(package_name, namespace)

@@ -900,7 +900,7 @@

      # the three below represent time of important events for this build

      # as returned by int(time.time())

      submitted_on = db.Column(db.Integer, nullable=False)

-     # directory name on backend with the srpm build results

+     # directory name on backend with the source build results

      result_dir = db.Column(db.Text, default='', server_default='', nullable=False)

      # memory requirements for backend builder

      memory_reqs = db.Column(db.Integer, default=app.config["DEFAULT_BUILD_MEMORY"])
@@ -1910,15 +1910,28 @@

      # e.g. 'https://src.fedoraproject.org'

      clone_url = db.Column(db.String(100), nullable=False)

  

-     # e.g. 'rpms/{pkgname}', needs to contain {pkgname} to be expanded later

+     # e.g. 'rpms/{pkgname}', needs to contain {pkgname} to be expanded later,

+     # may contain '{namespace}'.

      clone_package_uri = db.Column(db.String(100), nullable=False)

  

      # for UI form ordering, higher number means higher priority

      priority = db.Column(db.Integer, default=100, nullable=False)

  

-     def package_clone_url(self, pkgname):

+     def package_clone_url(self, pkgname, namespace=None):

+         """

+         Get the right git clone url for the package hosted in this dist git

+         instance.

+         """

          url = '/'.join([self.clone_url, self.clone_package_uri])

-         return url.format(pkgname=pkgname)

+         try:

+             if namespace:

+                 return url.format(pkgname=pkgname, namespace=namespace)

+ 

+             return url.format(pkgname=pkgname)

+         except KeyError as k:

+             raise KeyError("DistGit '{}' requires {} specified".format(

+                 self.name, k

+             ))

  

  

  class CancelRequest(db.Model):

@@ -603,7 +603,7 @@

      <div class="panel-body">

        <div class="list-group" style="margin-bottom: 0px">

          {% set srpm_build_methods = [

-         ('rpkg', 'rpkg', 'Uses <a href="https://pagure.io/rpkg-client">rpkg</a> utility to build an srpm. Supports unpacked (plain sources + .spec) as well as packed (tarballs + patches + .spec) content. Supports also building directly from DistGit.'),

+         ('rpkg', 'rpkg', 'Uses <a href="https://pagure.io/rpkg-client">rpkg</a> utility to build an srpm. Supports unpacked (plain sources + .spec) as well as packed (tarballs + patches + .spec) content.'),

          ('tito', 'tito', 'This method can be used for projects that are managed with <a href="https://github.com/dgoodwin/tito">Tito</a> packaging tool.'),

          ('tito test', 'tito_test', 'Same as previous Tito method, but passes <code>--test</code> parameter to use current branch HEAD instead of the last package tag. Also extends Release of the built package with Git versioning information.'),

          ('make srpm', 'make_srpm', 'This method allows unlimited customization. You need to provide .copr/Makefile with srpm target in your project and this target will be invoked inside a mock chroot to build the srpm. You can read more <a href="https://docs.pagure.org/copr.copr/user_documentation.html#make-srpm">here</a>.'),
@@ -715,7 +715,7 @@

                  {% else %}

                    <td data-order="-">-</td>

                  {% endif %}

-                 <td data-order="srpm build">srpm build</td>

+                 <td data-order="source build">Source build</td>

                {% else %}

                  <td data-order="{{ task.build.package.name }}">{{ task.build.package.name }} </td>

                  <td data-order="{{ task.build.pkg_version }}">{{ task.build.pkg_version }} </td>

@@ -133,6 +133,22 @@

  {% endmacro %}

  

  

+ {% macro copr_build_form_distgit(form, view, copr) %}

+   {{ copr_build_form_begin(form, view, copr) }}

+ 

+   {{ source_description('Build package from a '

+                         '<a href="https://github.com/release-engineering/dist-git">DistGit</a>'

+                         ' repository' )}}

+ 

+   {{ render_field(form.package_name) }}

+   {{ render_field(form.distgit) }}

+   {{ render_field(form.committish) }}

+   {{ render_field(form.namespace) }}

+ 

+   {{ copr_build_form_end(form, view, copr) }}

+ {% endmacro %}

+ 

+ 

  {% macro copr_build_form_pypi(form, view, copr) %}

    {{ copr_build_form_begin(form, view, copr) }}

  

@@ -66,4 +66,14 @@

    <dd>{{ source_json_dict["gem_name"] }}</dd>

    {% endif %}

  

+ {% if source_type_text == "distgit" %}

+   {% for info in ["distgit", "committish", "clone_url"] %}

+   {% if source_json_dict.get(info) %}

+   <dt>{{ info.title() }}:</dt>

+   <dd>{{ source_json_dict[info] }}</dd>

+   {% endif %}

+   {% endfor %}

+ {% endif %}

+ 

+ 

  {% endmacro %}

@@ -115,6 +115,29 @@

  {% endmacro %}

  

  

+ {% macro copr_package_form_distgit(form, view, copr, package) %}

+   {{ copr_package_form_begin(form, view, copr, package) }}

+ 

+   {{ render_field(form.distgit) }}

+   {{ render_field(form.committish) }}

+   {{ render_field(form.namespace) }}

+ 

+   {{ render_generic_pkg_form(form) }}

+   <div class="form-group">

+     <label class="col-sm-2 control-label" for="textInput-markup">

+       Auto-rebuild

+     </label>

+     <div class="col-sm-10">

+       <input type="checkbox" name="webhook_rebuild" {% if form.webhook_rebuild.data == True %}checked="checked"{% endif %}/>

+       Auto-rebuild the package on commits/PRs (currently supported only for

+       fedora distgit packages) | See also

+       <a href="{{ copr_url('coprs_ns.copr_integrations', copr) }}">Pagure Integration</a>

+     </div>

+   </div>

+   {{ copr_package_form_end(form, package, 'disgit') }}

+ {% endmacro %}

+ 

+ 

  {% macro copr_package_form_scm(form, view, copr, package) %}

    {{ copr_package_form_begin(form, view, copr, package) }}

  

@@ -4,6 +4,7 @@

     copr_package_form_pypi,

     copr_package_form_rubygems,

     copr_package_form_custom,

+    copr_package_form_distgit,

  with context %}

  

  
@@ -19,6 +20,7 @@

    {# kwargs: pass package_name='...' when editing the package #}

    <ul class="nav nav-tabs nav-tabs-pf">

      {{ nav_element("scm", "SCM", copr_url(view, copr, source_type_text="scm", **kwargs)) }}

+     {{ nav_element("distgit", "DistGit", copr_url(view, copr, source_type_text="distgit", **kwargs)) }}

      {{ nav_element("pypi", "PyPI", copr_url(view, copr, source_type_text="pypi", **kwargs)) }}

      {{ nav_element("rubygems", "RubyGems", copr_url(view, copr, source_type_text="rubygems", **kwargs)) }}

      {{ nav_element("custom", "Custom", copr_url(view, copr, source_type_text="custom", **kwargs)) }}
@@ -29,6 +31,9 @@

    {% if source_type_text == "scm" %}

      {{ copr_package_form_scm(form_scm, view, copr, package) }}

  

+   {% elif source_type_text == "distgit" %}

+     {{ copr_package_form_distgit(form_distgit, view, copr, package) }}

+ 

    {% elif source_type_text == "pypi" %}

      {{ copr_package_form_pypi(form_pypi, view, copr, package) }}

  

@@ -36,6 +36,7 @@

        {{ nav_element("url", "From URLs", copr_url('coprs_ns.copr_add_build', copr)) }}

        {{ nav_element("upload", "Upload", copr_url('coprs_ns.copr_add_build_upload', copr)) }}

        {{ nav_element("scm", "SCM", copr_url('coprs_ns.copr_add_build_scm', copr)) }}

+       {{ nav_element("distgit", "DistGit", copr_url('coprs_ns.copr_add_build_distgit', copr)) }}

        {{ nav_element("pypi", "PyPI", copr_url('coprs_ns.copr_add_build_pypi', copr)) }}

        {{ nav_element("rubygems", "RubyGems", copr_url('coprs_ns.copr_add_build_rubygems', copr)) }}

        {{ nav_element("custom", "Custom", copr_url('coprs_ns.copr_add_build_custom', copr)) }}

@@ -0,0 +1,11 @@

+ {% extends "coprs/detail/add_build.html" %}

+ 

+ {% from "coprs/detail/_builds_forms.html" import copr_build_form_distgit with context %}

+ 

+ {% set add_build_tab = "distgit" %}

+ 

+ {% block build_form %}

+ 

+ {{ copr_build_form_distgit(form, view, copr) }}

+ 

+ {% endblock %}

@@ -186,6 +186,30 @@

          )

      return process_creating_new_build(copr, form, create_new_build)

  

+ @apiv3_ns.route("/build/create/distgit", methods=POST)

+ @api_login_required

+ def create_from_distgit():

+     """

+     route for v3.proxies.create_from_distgit() call

+     """

+     copr = get_copr()

+     data = rename_fields(get_form_compatible_data())

+     # pylint: disable=not-callable

+     form = forms.BuildFormDistGitSimpleFactory(copr.active_chroots)(data, meta={'csrf': False})

+ 

+     def create_new_build():

+         return BuildsLogic.create_new_from_distgit(

+             flask.g.user,

+             copr,

+             package_name=form.package_name.data,

+             distgit_name=form.distgit.data,

+             distgit_namespace=form.namespace.data,

+             committish=form.committish.data,

+             chroot_names=form.selected_chroots,

+             copr_dirname=form.project_dirname.data,

+             background=form.background.data,

+         )

+     return process_creating_new_build(copr, form, create_new_build)

  

  @apiv3_ns.route("/build/create/pypi", methods=POST)

  @api_login_required

@@ -1,7 +1,10 @@

  import flask

- from . import query_params, pagination, get_copr, ListPaginator, GET, POST, PUT, DELETE

- from .json2form import get_form_compatible_data, get_input, without_empty_fields

- from coprs.exceptions import (ObjectNotFound, BadRequest)

+ 

+ from coprs.exceptions import (

+         BadRequest,

+         ObjectNotFound,

+         NoPackageSourceException

+ )

  from coprs.views.misc import api_login_required

  from coprs import db, models, forms

  from coprs.views.apiv3_ns import apiv3_ns
@@ -13,6 +16,8 @@

  # @TODO Don't import things from APIv1

  from coprs.views.api_ns.api_general import process_package_add_or_edit

  

+ from . import query_params, pagination, get_copr, ListPaginator, GET, POST, PUT, DELETE

+ from .json2form import get_form_compatible_data

  

  def to_dict(package, with_latest_build=False, with_latest_succeeded_build=False):

      source_dict = package.source_json_dict
@@ -161,8 +166,12 @@

                               .format(name=form.package_name.data, copr=copr.name))

      if form.validate_on_submit():

          buildopts = {k: v for k, v in form.data.items() if k in data}

-         build = PackagesLogic.build_package(flask.g.user, copr, package, form.selected_chroots,

-                                             copr_dirname=form.project_dirname.data, **buildopts)

+         try:

+             build = PackagesLogic.build_package(

+                 flask.g.user, copr, package, form.selected_chroots,

+                 copr_dirname=form.project_dirname.data, **buildopts)

+         except NoPackageSourceException as e:

+             raise BadRequest(str(e))

          db.session.commit()

      else:

          raise BadRequest(form.errors)

@@ -3,7 +3,6 @@

  

  from copr_common.enums import StatusEnum

  from coprs import db, app

- from coprs import helpers

  from coprs import models

  from coprs.logic import actions_logic

  from coprs.logic.builds_logic import BuildsLogic
@@ -16,7 +15,6 @@

  from coprs.views.backend_ns import backend_ns

  from sqlalchemy.sql import false, true

  

- import json

  import logging

  

  log = logging.getLogger(__name__)
@@ -105,12 +103,8 @@

              "memory_reqs": task.build.memory_reqs,

              "timeout": task.build.timeout,

              "enable_net": task.build.enable_net,

-             "git_repo": task.build.package.dist_git_repo,

+             "git_repo": task.build.package.dist_git_clone_url,

              "git_hash": task.git_hash,

-             "source_type": helpers.BuildSourceEnum("scm"),

-             "source_json": json.dumps(

-                 {'clone_url': task.build.package.dist_git_clone_url, 'committish': task.git_hash}),

-             "fetch_sources_only": True,

              "package_name": task.build.package.name,

              "package_version": task.build.pkg_version,

              "uses_devel_repo": task.build.copr.devel_mode,
@@ -354,7 +348,7 @@

  

      if task_id == build.task_id:

          if build.source_status in run_statuses:

-             log.info("rescheduling srpm build {}".format(build.id))

+             log.info("rescheduling source build %s", build.id)

              BuildsLogic.update_state_from_dict(build, {

                  "task_id": task_id,

                  "status": StatusEnum("pending")

@@ -11,8 +11,7 @@

  from coprs.logic.builds_logic import BuildsLogic

  from coprs.logic.complex_logic import ComplexLogic

  

- from coprs.views.misc import (login_required, req_with_copr,

-         req_with_copr, send_build_icon)

+ from coprs.views.misc import (login_required, req_with_copr, send_build_icon)

  from coprs.views.coprs_ns import coprs_ns

  

  from coprs.exceptions import (
@@ -182,6 +181,51 @@

      form = forms.BuildFormScmFactory(copr.active_chroots)()

      return process_new_build(copr, form, factory, render_add_build_scm, add_view, url_on_success)

  

+ ################################ DistGit builds ################################

+ 

+ @coprs_ns.route("/<username>/<coprname>/add_build_distgit/")

+ @coprs_ns.route("/g/<group_name>/<coprname>/add_build_distgit/")

+ @login_required

+ @req_with_copr

+ def copr_add_build_distgit(copr, form=None):

+     """ GET request for distgit build """

+     return render_add_build_distgit(

+         copr, form, view='coprs_ns.copr_new_build_distgit')

+ 

+ @coprs_ns.route("/<username>/<coprname>/new_build_distgit/", methods=["POST"])

+ @coprs_ns.route("/g/<group_name>/<coprname>/new_build_distgit/", methods=["POST"])

+ @login_required

+ @req_with_copr

+ def copr_new_build_distgit(copr):

+     """ POST request for distgit build """

+     view = 'coprs_ns.copr_new_build_distgit'

+     url_on_success = helpers.copr_url("coprs_ns.copr_builds", copr)

+     return process_new_build_distgit(copr, view, url_on_success)

+ 

+ def render_add_build_distgit(copr, form, view, package=None):

+     """ Render the distgit build form """

+     if not form:

+         # pylint: disable=not-callable

+         form = forms.BuildFormDistGitSimpleFactory(copr.active_chroots)()

+     return flask.render_template("coprs/detail/add_build/distgit.html",

+                                  copr=copr, form=form, view=view, package=package)

+ 

+ def process_new_build_distgit(copr, add_view, url_on_success):

+     """ Handle the POST data from distgit build form """

+     def factory(**build_options):

+         BuildsLogic.create_new_from_distgit(

+             flask.g.user,

+             copr,

+             package_name=form.package_name.data,

+             distgit_name=form.distgit.data,

+             distgit_namespace=form.namespace.data,

+             committish=form.committish.data,

+             chroot_names=form.selected_chroots,

+             **build_options

+         )

+     # pylint: disable=not-callable

+     form = forms.BuildFormDistGitSimpleFactory(copr.active_chroots)()

+     return process_new_build(copr, form, factory, render_add_build_distgit, add_view, url_on_success)

  

  ################################ PyPI builds ################################

  

@@ -7,8 +7,13 @@

  from coprs import forms

  from coprs import helpers

  from coprs.views.coprs_ns import coprs_ns

- from coprs.views.coprs_ns.coprs_builds import render_add_build_scm, render_add_build_pypi, render_add_build_custom

- from coprs.views.misc import login_required, req_with_copr, req_with_copr, send_build_icon

+ from coprs.views.coprs_ns.coprs_builds import (

+     render_add_build_scm,

+     render_add_build_pypi,

+     render_add_build_custom,

+     render_add_build_distgit,

+ )

+ from coprs.views.misc import login_required, req_with_copr, send_build_icon

  from coprs.logic.complex_logic import ComplexLogic

  from coprs.logic.packages_logic import PackagesLogic

  from coprs.logic.users_logic import UsersLogic
@@ -106,9 +111,17 @@

          form = forms.BuildFormCustomFactory

          f = render_add_build_custom

          view_suffix = "_custom"

+     elif package.source_type_text == "distgit":

+         form = forms.BuildFormDistGitSimpleFactory

+         f = render_add_build_distgit

+         view_suffix = "_distgit"

+         data["package_name"] = package_name

      else:

-         flask.flash("Package {} has not the default source which is required for rebuild. Please configure some source"

-                     .format(package_name, copr.full_name))

+         flask.flash(

+             # TODO: sync this with the API error NoPackageSourceException

+             "Package {} doesn't have the default source method set, but it is "

+             "required for the rebuild request.  Please configure some source "

+             "method first".format(package_name))

          return flask.redirect(helpers.copr_url("coprs_ns.copr_edit_package", copr, package_name=package_name))

  

      form = form(copr.active_chroots, package)(data=data)
@@ -127,6 +140,7 @@

          "pypi": forms.PackageFormPyPI(),

          "rubygems": forms.PackageFormRubyGems(),

          "custom": forms.PackageFormCustom(),

+         "distgit": forms.PackageFormDistGitSimple(),

      }

  

      if "form" in kwargs:
@@ -136,6 +150,7 @@

                                   source_type_text=source_type_text, view="coprs_ns.copr_new_package",

                                   form_scm=form["scm"], form_pypi=form["pypi"],

                                   form_rubygems=form["rubygems"],

+                                  form_distgit=form['distgit'],

                                   form_custom=form['custom'])

  

  
@@ -171,6 +186,7 @@

          "pypi": forms.PackageFormPyPI,

          "rubygems": forms.PackageFormRubyGems,

          "custom": forms.PackageFormCustom,

+         "distgit": forms.PackageFormDistGitSimple,

      }

      form = {k: v(formdata=None) for k, v in form_classes.items()}

  
@@ -183,6 +199,7 @@

                                   source_type_text=source_type_text, view="coprs_ns.copr_edit_package",

                                   form_scm=form["scm"], form_pypi=form["pypi"],

                                   form_rubygems=form["rubygems"],

+                                  form_distgit=form["distgit"],

                                   form_custom=form['custom'])

  

  

@@ -23,7 +23,10 @@

  

  from urllib.parse import urlparse

  

- SCM_SOURCE_TYPE = helpers.BuildSourceEnum("scm")

+ SUPPORTED_SOURCE_TYPES = [

+     helpers.BuildSourceEnum("scm"),

+     helpers.BuildSourceEnum("distgit"),

+ ]

  

  log = logging.getLogger(__name__)

  if os.getenv('PAGURE_EVENTS_TESTONLY'):
@@ -79,7 +82,7 @@

      def get_candidates_for_rebuild(cls, clone_url):

          rows = models.Package.query \

              .join(models.CoprDir) \

-             .filter(models.Package.source_type == SCM_SOURCE_TYPE) \

+             .filter(models.Package.source_type.in_(SUPPORTED_SOURCE_TYPES)) \

              .filter(models.Package.webhook_rebuild) \

              .filter(models.CoprDir.main) \

              .filter(models.Package.source_json.ilike('%' + clone_url + '%'))

@@ -24,6 +24,9 @@

  

  class CoprsTestCase(object):

  

+     # made available by TransactionDecorator

+     test_client = None

+ 

      original_config = coprs.app.config.copy()

  

      @classmethod
@@ -706,7 +709,13 @@

              clone_package_uri='some/other/uri/{pkgname}/git',

              priority='120',

          )

-         self.db.session.add_all([self.dg1, self.dg2])

+         self.dg3 = models.DistGitInstance(

+             name='namespaced',

+             clone_url='https://namespaced.org',

+             clone_package_uri='some/other/uri/{namespace}/{pkgname}/git',

+             priority='30',

+         )

+         self.db.session.add_all([self.dg1, self.dg2, self.dg3])

  

      @pytest.fixture

      def f_copr_chroots_assigned(self, f_users, f_coprs, f_mock_chroots,

@@ -1,5 +1,160 @@

+ """

+ Test all kind of build request via v3 API

+ """

+ 

+ import copy

+ import json

+ 

+ import pytest

+ 

+ from copr_common.enums import BuildSourceEnum

+ 

  from tests.coprs_test_case import CoprsTestCase

  

  

- class TestBuilds(CoprsTestCase):

-     pass

+ # Items are

+ # =========

+ # 1. source type text

+ # 2. form data to be sent for build

+ # 3. expected "source_json" data to be set for the build

+ # 4. additionl "source_json" data expected to be set per-package

+ 

+ CASES = [(

+     "distgit",

+     {

+         "ownername": "user2",

+         "projectname": "foocopr",

+         "package_name": "mock",

+     },

+     {

+         "clone_url": "git://prio.com/some/other/uri/mock/git",

+     },

+     {

+         "distgit": "prioritized",

+     },

+ ), (

+     "distgit",

+     {

+         "ownername": "user2",

+         "projectname": "foocopr",

+         "package_name": "cpio",

+         "distgit": "fedora",

+     },

+     {

+         # no need to store "distgit" to source_json

+         "clone_url": "https://src.fedoraproject.org/rpms/cpio"

+     },

+     {

+         "distgit": "fedora",

+     },

+ ), (

+     "distgit",

+     {

+         "ownername": "user2",

+         "projectname": "foocopr",

+         "package_name": "tar",

+         "committish": "f15",

+     },

+     {

+         "committish": "f15",

+         "clone_url": "git://prio.com/some/other/uri/tar/git",

+     },

+     {

+         "distgit": "prioritized",

+ 

+     },

+ ), (

+     "distgit",

+     {

+         "ownername": "user2",

+         "projectname": "foocopr",

+         "package_name": "blah",

+         "namespace": "@copr/copr",

+         "distgit": "namespaced",

+     },

+     {

+         "clone_url": "https://namespaced.org/some/other/uri/@copr/copr/blah/git",

+     },

+     {

+         "distgit": "namespaced",

+         "namespace": "@copr/copr",

+     },

+ )]

+ 

+ BUILDOPTS = [{}, {

+     "chroots": ["fedora-18-x86_64"],

+ }, {

+     "chroots": ["fedora-17-x86_64"],

+ }]

+ 

+ 

+ # 1. description

+ # 2. form input

+ # 3. expected error contents

+ CASES_BAD = [(

+     "no namespace specified",

+     {

+         "ownername": "user2",

+         "projectname": "foocopr",

+         "package_name": "blah",

+         "distgit": "namespaced",

+     },

+     ["Can not validate DistGit input", "specified"],

+ ), (

+     "non-existent distgit",

+     {

+         "ownername": "user2",

+         "projectname": "foocopr",

+         "package_name": "blah",

+         "distgit": "nonexistent",

+     },

+     ["DistGit ID must be one of: prioritized, "],

+ )]

+ 

+ 

+ class TestAPIv3Builds(CoprsTestCase):

+ 

+     @pytest.mark.usefixtures("f_users", "f_users_api", "f_coprs",

+                              "f_mock_chroots", "f_other_distgit", "f_db")

+     @pytest.mark.parametrize("case", CASES)

+     @pytest.mark.parametrize("buildopts", BUILDOPTS)

+     def test_v3_builds(self, buildopts, case):

+         source_type_text, data, source_json, _ = case

+         form_data = copy.deepcopy(data)

+         form_data.update(buildopts)

+ 

+         endpoint = "/api_3/build/create/distgit"

+         user = self.models.User.query.filter_by(username='user2').first()

+         r = self.post_api3_with_auth(endpoint, form_data, user)

+         assert r.status_code == 200

+         build = self.models.Build.query.first()

+ 

+         enabled_chroots = set(['fedora-17-x86_64', 'fedora-17-i386'])

+         if not form_data.get("chroots"):

+             assert build.chroots == []

+         elif set(form_data['chroots']).issubset(enabled_chroots):

+             real = {mch.name for mch in build.chroots}

+             assert real == set(form_data["chroots"])

+         else:

+             assert build.chroots == []

+ 

+         assert build.source_type == BuildSourceEnum(source_type_text)

+         assert json.loads(build.source_json) == source_json

+ 

+         timeout = buildopts.get("timeout")

+         if timeout:

+             assert build.timeout == timeout

+ 

+     @pytest.mark.usefixtures("f_users", "f_users_api", "f_coprs",

+                              "f_mock_chroots", "f_other_distgit", "f_db")

+     @pytest.mark.parametrize("case", CASES_BAD)

+     def test_v3_build_failure(self, case):

+         endpoint = "/api_3/build/create/distgit"

+         user = self.models.User.query.filter_by(username='user2').first()

+         # missing namespace

+         _, form_data, errors = case

+         response = self.post_api3_with_auth(endpoint, form_data, user)

+         assert response.status_code == 400

+         error_message = response.json['error']

+         for error in errors:

+             assert error in error_message

@@ -0,0 +1,75 @@

+ """

+ Test all kind of build request via v3 API

+ """

+ 

+ import copy

+ import json

+ 

+ import pytest

+ 

+ from copr_common.enums import BuildSourceEnum

+ 

+ from tests.coprs_test_case import CoprsTestCase

+ from tests.test_apiv3.test_builds import CASES

+ 

+ 

+ CHROOTS = [[], ["fedora-18-x86_64"], ["fedora-17-i386"]]

+ 

+ 

+ class TestAPIv3Packages(CoprsTestCase):

+ 

+     @pytest.mark.usefixtures("f_users", "f_users_api", "f_coprs",

+                              "f_mock_chroots", "f_other_distgit", "f_db")

+     @pytest.mark.parametrize("case", CASES)

+     @pytest.mark.parametrize("chroots", CHROOTS)

+     def test_v3_packages(self, chroots, case):

+         source_type_text, data, source_json, additional_data = case

+         form_data = copy.deepcopy(data)

+         expected_source_dict = copy.deepcopy(source_json)

+         expected_source_dict.update(additional_data)

+         pkg_name = form_data["package_name"]

+         endpoint = "/api_3/package/add/{0}/{1}/{2}/{3}".format(

+             "user2", "foocopr", pkg_name, source_type_text)

+ 

+         user = self.models.User.query.filter_by(username='user2').first()

+         r = self.post_api3_with_auth(endpoint, form_data, user)

+         assert r.status_code == 200

+         package = self.models.Package.query.first()

+         assert package.name == pkg_name

+         assert package.webhook_rebuild is False

+         assert json.loads(package.source_json) == expected_source_dict

+ 

+         # Try to edit the package.

+         endpoint = "/api_3/package/edit/{0}/{1}/{2}/{3}".format(

+             "user2", "foocopr", pkg_name, source_type_text)

+ 

+         form_data["webhook_rebuild"] = True

+         r = self.post_api3_with_auth(endpoint, form_data, user)

+         assert r.status_code == 200

+ 

+         package = self.models.Package.query.first()

+         assert package.name == pkg_name

+         assert json.loads(package.source_json) == expected_source_dict

+         assert package.webhook_rebuild is True

+ 

+         # Try to build the package.

+         endpoint = "/api_3/package/build"

+         rebuild_data = {

+             "ownername": "user2",

+             "projectname": form_data["projectname"],

+             "package_name": form_data["package_name"],

+         }

+         self.post_api3_with_auth(endpoint, rebuild_data, user)

+         build = self.models.Build.query.get(1)

+         assert json.loads(build.source_json) == expected_source_dict

+         assert build.source_type == BuildSourceEnum(source_type_text)

+         assert build.chroots == []

+ 

+         rebuild_data["chroots"] = chroots

+         self.post_api3_with_auth(endpoint, rebuild_data, user)

+         build = self.models.Build.query.get(2)

+         assert json.loads(build.source_json) == expected_source_dict

+         if "fedora-18-x86_64" in chroots or chroots == []:

+             assert build.chroots == []

+         else:

+             assert [mch.name for mch in build.chroots] == ["fedora-17-i386"]

@@ -0,0 +1,75 @@

+ """

+ Test routes related to DistGit method

+ """

+ import json

+ import pytest

+ 

+ from copr_common.enums import BuildSourceEnum

+ 

+ from tests.coprs_test_case import CoprsTestCase, TransactionDecorator

+ 

+ class TestDistGitMethod(CoprsTestCase):

+     """ add/edit package, add build """

+ 

+     @TransactionDecorator("u1")

+     @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_db")

+     def test_copr_user_can_add_distgit_build(self):

+         """ add build using web-UI """

+         self.db.session.add_all([self.u1, self.c1])

+         data = {

+             "package_name": "mock",

+             "committish": "master",

+             "fedora-18-x86_64": True,

+         }

+         endpoint = "/coprs/{0}/{1}/new_build_distgit/".format(self.u1.name,

+                                                               self.c1.name)

+         self.test_client.post(endpoint, data=data, follow_redirects=True)

+         build = self.models.Build.query.first()

+         assert build.source_type == BuildSourceEnum.distgit

+         assert build.source_json == json.dumps({

+             "clone_url": "https://src.fedoraproject.org/rpms/mock",

+             "committish": "master"})

+ 

+         assert len(build.chroots) == 1

+         assert build.chroots[0].name == "fedora-18-x86_64"

+ 

+     @TransactionDecorator("u1")

+     @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_db")

+     def test_copr_user_can_add_distgit_package(self):

+         """ add package using web-UI """

+         self.db.session.add_all([self.u1, self.c1])

+         data = {

+             "package_name": "mock",

+             "distgit": "fedora",

+             "committish": "master",

+         }

+         endpoint = "/coprs/{0}/{1}/package/new/distgit".format(self.u1.name,

+                                                                self.c1.name)

+         self.test_client.post(endpoint, data=data, follow_redirects=True)

+ 

+         package = self.models.Package.query.first()

+         assert package.name == "mock"

+         assert package.source_type == 10

+         assert json.loads(package.source_json) == {

+             "distgit": "fedora",  # prefilled as default

+             "committish": "master",

+             "clone_url": "https://src.fedoraproject.org/rpms/mock"

+         }

+ 

+         self.db.session.close()

+         self.db.session.add_all([self.u1, self.c1])

+         endpoint = "/coprs/{0}/{1}/package/mock/edit/distgit".format(self.u1.name,

+                                                                      self.c1.name)

+         data["committish"] = "f15"

+         data["namespace"] = "@copr/copr"

+         self.test_client.post(endpoint, data=data, follow_redirects=True)

+ 

+         package = self.models.Package.query.first()

+         assert package.name == "mock"

+         assert package.source_type == 10

+         assert json.loads(package.source_json) == {

+             "distgit": "fedora",  # prefilled as default

+             "committish": "f15",

+             "namespace": "@copr/copr",

+             "clone_url": "https://src.fedoraproject.org/rpms/mock"

+         }

file modified
+2 -1
@@ -46,7 +46,8 @@

  

  [VARIABLES]

  # A regular expression matching names used for dummy variables (i.e. not used).

- dummy-variables-rgx=_|dummy

+ # - "step_impl" is used for all the behave steps

+ dummy-variables-rgx=_|dummy|step_impl

  

  

  [BASIC]

@@ -1,8 +1,10 @@

  import mock

  from requests import Response

- from copr.v3 import BuildProxy

+ from copr.v3 import Client, BuildProxy

  from copr.v3.requests import Request

  

+ from copr.test.test_client import config_location

+ 

  

  @mock.patch.object(Request, "send")

  class TestBuildProxy(object):
@@ -17,3 +19,20 @@

          build = build_proxy.get(1)

          assert build.id == 1

          assert build.foo == "bar"

+ 

+ 

+ @mock.patch("copr.v3.proxies.build.Request")

+ def test_build_distgit(request):

+     mock_client = Client.create_from_config_file(config_location)

+     mock_client.build_proxy.create_from_distgit(

+         "praiskup", "ping", "mock", committish="master",

+     )

+     assert len(request.call_args_list) == 1

+     call = request.call_args_list[0]

+     args = call[1]

+     assert args['method'] == 'POST'

+     assert args['endpoint'] == '/build/create/distgit'

+     assert args['data'] == {

+         'ownername': 'praiskup', 'projectname': 'ping',

+         'distgit': None, 'namespace': None, 'package_name': 'mock',

+         'committish': 'master', 'project_dirname': None}

@@ -0,0 +1,30 @@

+ """

+ Test for adding packages

+ """

+ 

+ import mock

+ import pytest

+ 

+ from copr.v3 import Client

+ from copr.test.test_client import config_location

+ 

+ 

+ @pytest.mark.parametrize('method_name', ['add', 'edit'])

+ @mock.patch("copr.v3.proxies.package.Request")

+ def test_package_distgit(request, method_name):

+     mock_client = Client.create_from_config_file(config_location)

+     method = getattr(mock_client.package_proxy, method_name)

+     method("fooUser", "test", "mock", "distgit",

+            {"committish": "master", "distgit": "fedora"})

+     assert len(request.call_args_list) == 1

+     call = request.call_args_list[0]

+     endpoint = call[0][0]

+     args = call[1]

+     assert args['method'] == 'POST'

+     base_url = "/package/{0}".format(method_name)

+     assert endpoint == base_url + "/{ownername}/{projectname}/{package_name}/{source_type_text}"

+     params = args['params']

+     assert params == {'ownername': 'fooUser', 'projectname': 'test',

+                       'package_name': 'mock', 'source_type_text': 'distgit'}

+     assert args['data'] == {'committish': 'master', 'distgit': 'fedora',

+                             'package_name': 'mock'}

@@ -173,6 +173,38 @@

          }

          return self._create(endpoint, data, buildopts=buildopts)

  

+     def create_from_distgit(self, ownername, projectname, packagename,

+                             committish=None, namespace=None, distgit=None,

+                             buildopts=None, project_dirname=None):

+         """

+         Create a build from a DistGit repository

+ 

+         :param str ownername:

+         :param str projectname:

+         :param str packagename: the DistGit package name to build

+         :param str committish: name of a branch, tag, or a git hash

+         :param str namespace: DistGit namespace, e.g. '@copr/copr' for

+             the '@copr/copr/copr-cli' package

+         :param str distgit: the DistGit instance name we build against,

+             for example 'fedora'.  Optional, and the default is deployment

+             specific.

+         :param buildopts: http://python-copr.readthedocs.io/en/latest/client_v3/build_options.html

+         :param str project_dirname:

+         :return: Munch

+         """

+         endpoint = "/build/create/distgit"

+         data = {

+             "ownername": ownername,

+             "projectname": projectname,

+             "distgit": distgit,

+             "namespace": namespace,

+             "package_name": packagename,

+             "committish": committish,

+             "project_dirname": project_dirname,

+         }

+         return self._create(endpoint, data, buildopts=buildopts)

+ 

+ 

      def create_from_pypi(self, ownername, projectname, pypi_package_name, pypi_package_version=None,

                           spec_template='', python_versions=None, buildopts=None, project_dirname=None):

          """

file modified
+1 -1
@@ -15,7 +15,7 @@

  %endif

  

  Name:       python-copr

- Version:    1.105

+ Version:    1.105.1.dev

  Release:    1%{?dist}

  Summary:    Python interface for Copr

  

file modified
+5 -1
@@ -5,4 +5,8 @@

  absdir="$(dirname "$(readlink -f "$0")")"

  export PYTHONPATH="$absdir"

  

- python3 -B -m pytest --cov-report term-missing copr/test "$@"

+ coverage=(

+     --cov-report term-missing

+     --cov copr/v3

+ )

+ python3 -B -m pytest "${coverage[@]}" copr/test "$@"

@@ -0,0 +1,10 @@

+ #! /usr/bin/python

+ 

+ """

+ From within a git checkout, try to download files from dist-git lookaside cache.

+ """

+ 

+ from copr_distgit_client import main

+ 

+ if __name__ == "__main__":

+     main()

@@ -0,0 +1,22 @@

+ #! /bin/sh

+ 

+ # Run copr-disgit-client script directly from git, TESTING ONLY SCRIPT!

+ # Copyright (C) 2020 Red Hat, Inc.

+ #

+ # 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.

+ 

+ absdir="$(dirname "$(readlink -f "$0")")"

+ export PYTHONPATH="$absdir${PYTHONPATH+:$PYTHONPATH}"

+ python3 "$absdir/bin/copr-distgit-client" "$@"

file modified
+58 -7
@@ -3,11 +3,13 @@

  %global python          python3

  %global python_pfx      python3

  %global rpm_python      python3-rpm

+ %global sitelib         %python3_sitelib

  %else

  %global __python        %__python2

  %global python          python2

  %global python_pfx      python

  %global rpm_python      rpm-python

+ %global sitelib         %python_sitelib

  %endif

  

  # do not build debuginfo sub-packages
@@ -18,7 +20,7 @@

  %{expand: %%global latest_requires_packages %1 %%{?latest_requires_packages}}

  

  Name:    copr-rpmbuild

- Version: 0.40

+ Version: 0.40.1.dev

  Summary: Run COPR build tasks

  Release: 1%{?dist}

  URL: https://pagure.io/copr/copr
@@ -29,6 +31,7 @@

  # tito build --tgz --tag %%name-%%version-%%release

  Source0:    %name-%version.tar.gz

  

+ BuildRequires: %{python}-copr-common

  BuildRequires: %{python}-devel

  BuildRequires: %{python}-distro

  %if 0%{?rhel} == 0 || 0%{?rhel} != 6
@@ -36,12 +39,16 @@

  %endif

  BuildRequires: %{rpm_python}

  BuildRequires: asciidoc

+ BuildRequires: git

  BuildRequires: %{python}-setuptools

  BuildRequires: %{python}-pytest

  BuildRequires: %{python_pfx}-munch

  BuildRequires: %{python}-requests

  BuildRequires: %{python_pfx}-jinja2

  

+ %if 0%{?fedora} || 0%{?rhel} > 7

+ BuildRequires: argparse-manpage

+ %endif

  BuildRequires: python-rpm-macros

  

  %if "%{?python}" == "python2"
@@ -51,6 +58,7 @@

  %endif

  

  Requires: %python

+ Requires: %{python}-copr-common

  Requires: %{python_pfx}-jinja2

  Requires: %{python_pfx}-munch

  Requires: %{python}-requests
@@ -80,6 +88,19 @@

  %package -n copr-builder

  Summary: copr-rpmbuild with all weak dependencies

  Requires: %{name} = %{version}-%{release}

+ Requires: copr-distgit-client = %{version}-%{release}

+ 

+ 

+ %package -n copr-distgit-client

+ Summary: Utility to download sources from dist-git

+ 

+ %description -n copr-distgit-client

+ A simple, configurable python utility that is able to download sources from

+ various dist-git instances, and generate source RPMs.

+ 

+ The utility is able to automatically map the .git/config clone URL into

+ the corresponding dist-git instance configuration.

+ 

  

  %if 0%{?fedora}

  # replacement for yum/yum-utils, to be able to work with el* chroots
@@ -141,10 +162,15 @@

  

  %prep

  %setup -q

+ for script in bin/copr-rpmbuild* \

+               bin/copr-distgit*

+ do

+     sed -i '1 s|#.*|#! /usr/bin/%python|' "$script"

+ done

  

  

  %check

- PYTHON=%{python} ./run_tests.sh

+ PYTHON=%{python} ./run_tests.sh --no-coverage

  

  

  %build
@@ -212,10 +238,6 @@

  install -p -m 755 bin/copr-rpmbuild-cancel %buildroot%_bindir

  install -p -m 755 bin/copr-rpmbuild-log %buildroot%_bindir

  

- for script in %buildroot/%{_bindir}/copr-rpmbuild*; do

-     sed -i '1 s|#.*|#! /usr/bin/%python|' "$script"

- done

- 

  name="%{name}" version="%{version}" summary="%{summary}" %py_install

  

  install -p -m 755 copr-update-builder %buildroot%_bindir
@@ -229,12 +251,27 @@

    done

  )

  

+ install -p -m 755 bin/copr-distgit-client %buildroot%_bindir

+ %if 0%{?fedora} || 0%{?rhel} > 7

+ argparse-manpage --pyfile copr_distgit_client.py \

+     --function _get_argparser \

+     --author "Copr Team" \

+     --author-email "copr-team@redhat.com" \

+     --url %url --project-name Copr \

+ > %{buildroot}%{_mandir}/man1/copr-distgit-client.1

+ %endif

+ mkdir -p %{buildroot}%{_sysconfdir}/copr-distgit-client

+ install -p -m 644 etc/copr-distgit-client/default.ini \

+     %{buildroot}%{_sysconfdir}/copr-distgit-client

+ mkdir -p %{buildroot}%{sitelib}

+ install -p -m 644 copr_distgit_client.py %{buildroot}%{expand:%%%{python}_sitelib}

+ 

  

  %files

  %{!?_licensedir:%global license %doc}

  %license LICENSE

  

- %{expand:%%%{python}_sitelib}/*

+ %sitelib/copr_rpmbuild*

  

  %{_bindir}/copr-rpmbuild*

  %{_bindir}/copr-sources-custom
@@ -258,6 +295,20 @@

  %doc %mock_config_overrides/README

  

  

+ %files -n copr-distgit-client

+ %license LICENSE

+ %_bindir/copr-distgit-client

+ %if 0%{?fedora} || 0%{?rhel} > 7

+ %_mandir/man1/copr-distgit-client.1*

+ %endif

+ %dir %_sysconfdir/copr-distgit-client

+ %config %_sysconfdir/copr-distgit-client/default.ini

+ %sitelib/copr_distgit_client.*

+ %if "%{?python}" != "python2"

+ %sitelib/__pycache__/copr_distgit_client*

+ %endif

+ 

+ 

  %changelog

  * Mon Aug 10 2020 Pavel Raiskup <praiskup@redhat.com> 0.40-1

  - provide the "dynamic" %%buildtag

@@ -0,0 +1,335 @@

+ """

+ copr-distgit-client code, moved to module to simplify unit-testing

+ """

+ 

+ import argparse

+ import configparser

+ import errno

+ import glob

+ import logging

+ import pipes

+ import os

+ import subprocess

+ from six.moves.urllib.parse import urlparse

+ 

+ 

+ def log_cmd(command, comment="Running command"):

+     """ Dump the command to stderr so it can be c&p to shell """

+     command = ' '.join([pipes.quote(x) for x in command])

+     logging.info("%s: %s", comment, command)

+ 

+ 

+ def check_output(cmd, comment="Reading stdout from command"):

+     """ el6 compatible subprocess.check_output() """

+     log_cmd(cmd, comment)

+     process = subprocess.Popen(

+         cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

+     (stdout, _) = process.communicate()

+     if process.returncode:

+         raise RuntimeError("Exit non-zero: {0}".format(process.returncode))

+     return stdout

+ 

+ def call(cmd, comment="Calling"):

+     """ wrap sp.call() with logging info """

+     log_cmd(cmd, comment)

+     return subprocess.call(cmd)

+ 

+ def check_call(cmd, comment="Checked call"):

+     """ wrap sp.check_call() with logging info """

+     log_cmd(cmd, comment)

+     subprocess.check_call(cmd)

+ 

+ def _load_configfile(filename):

+     config = configparser.ConfigParser()

+     config.read(filename)

+ 

+ 

+ def _load_config(directory):

+     config = configparser.ConfigParser()

+     files = glob.glob(os.path.join(directory, "*.ini"))

+     logging.debug("Files %s in config directory %s", files, directory)

+     config.read(files)

+ 

+     config_dict = {

+         "instances": {},

+         "clone_host_map": {},

+     }

+ 

+     instances = config_dict["instances"]

+     for section_name in config.sections():

+         section = config[section_name]

+         instance = instances[section_name] = {}

+         for key in section.keys():

+             # array-like config options

+             if key in ["clone_hostnames"]:

+                 hostnames = section[key].split()

+                 instance[key] = [h.strip() for h in hostnames]

+             else:

+                 instance[key] = section[key]

+ 

+         for key in ["sources", "specs"]:

+             if key in instance:

+                 continue

+             instance[key] = "."

+ 

+         if "sources_file" not in instance:

+             instance["sources_file"] = "sources"

+ 

+         if "default_sum" not in instance:

+             instance["default_sum"] = "md5"

+ 

+         for host in instance["clone_hostnames"]:

+             config_dict["clone_host_map"][host] = instance

+ 

+     return config_dict

+ 

+ 

+ def download(url, filename):

+     """ Download URL as FILENAME using curl command """

+     command = [

+         "curl",

+         "-H", "Pragma:",

+         "-o", filename,

+         "--location",

+         "--remote-time",

+         "--show-error",

+         "--fail",

+         url,

+     ]

+ 

+     if call(command):

+         raise RuntimeError("Can't download file {0}".format(filename))

+ 

+ 

+ def mkdir_p(path):

+     """ mimic 'mkdir -p <path>' command """

+     try:

+         os.makedirs(path)

+     except OSError as err:

+         if err.errno != errno.EEXIST:

+             raise

+ 

+ 

+ def download_file_and_check(url, params, distgit_config):

+     """ Download given URL (if not yet downloaded), and try the checksum """

+     filename = params["filename"]

+     sum_binary = params["hashtype"] + "sum"

+ 

+     mkdir_p(distgit_config["sources"])

+ 

+     if not os.path.exists(filename):

+         logging.info("Downloading %s", filename)

+         download(url, filename)

+     else:

+         logging.info("File %s already exists", filename)

+ 

+     sum_command = [sum_binary, filename]

+     output = check_output(sum_command).decode("utf-8")

+     checksum, _ = output.strip().split()

+     if checksum != params["hash"]:

+         raise RuntimeError("Check-sum {0} is wrong, expected: {1}".format(

+             checksum,

+             params["hash"],

+         ))

+ 

+ 

+ def get_distgit_config(config):

+     """

+     Given the '.git/config' file from current directory, return the

+     appropriate part of dist-git configuration.

+     Returns tuple: (urlparse(clone_url), distgit_config)

+     """

+     git_config = ".git/config"

+     if not os.path.exists(git_config):

+         msg = "{0} not found, $PWD is not a git repository".format(git_config)

+         raise RuntimeError(msg)

+ 

+     git_conf_reader = configparser.ConfigParser()

+     git_conf_reader.read(git_config)

+     url = git_conf_reader['remote "origin"']["url"]

+     parsed_url = urlparse(url)

+     if parsed_url.hostname is None:

+         hostname = "localhost"

+     else:

+         hostname = parsed_url.hostname

+     return parsed_url, config["clone_host_map"][hostname]

+ 

+ 

+ def get_spec(distgit_config):

+     """

+     Find the specfile name inside distgit_config["specs"] directory

+     """

+     specfiles = glob.glob(os.path.join(distgit_config["specs"], '*.spec'))

+     if len(specfiles) != 1:

+         raise RuntimeError("Exactly one spec file expected")

+     specfile = os.path.basename(specfiles[0])

+     return specfile

+ 

+ 

+ def sources(args, config):

+     """

+     Locate the sources, and download them from the appropriate dist-git

+     lookaside cache.

+     """

+     parsed_url, distgit_config = get_distgit_config(config)

+     namespace = parsed_url.path.lstrip('/').split('/')

+     # drop the last {name}.git part

+     namespace.pop()

+     namespace = list(reversed(namespace))

+ 

+     output = check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])

+     output = output.decode("utf-8").strip()

+     if output == "HEAD":

+         output = check_output(["git", "rev-parse", "HEAD"])

+         output = output.decode("utf-8").strip()

+     refspec = output

+     specfile = get_spec(distgit_config)

+     name = specfile[:-5]

+     sources_file = distgit_config["sources_file"].format(name=name)

+     if not os.path.exists(sources_file):

+         raise RuntimeError("{0} file not found".format(sources_file))

+ 

+     logging.info("Reading sources specification file: %s", sources_file)

+     with open(sources_file, 'r') as sfd:

+         while True:

+             line = sfd.readline()

+             if not line:

+                 break

+ 

+             kwargs = {

+                 "name": name,

+                 "refspec": refspec,

+                 "namespace": namespace,

+             }

+ 

+             source_spec = line.split()

+             if len(source_spec) == 2:

+                 # old md5/sha1 format: 0ced6f20b9fa1bea588005b5ad4b52c1  tar-1.26.tar.xz

+                 kwargs["hashtype"] = distgit_config["default_sum"].lower()

+                 kwargs["hash"] = source_spec[0]

+                 kwargs["filename"] = source_spec[1]

+             elif len(source_spec) == 4:

+                 # SHA512 (tar-1.30.tar.xz.sig) = <HASH>

+                 kwargs["hashtype"] = source_spec[0].lower()

+                 kwargs["hash"] = source_spec[3]

+                 filename = os.path.basename(source_spec[1])

+                 kwargs["filename"] = filename.strip('()')

+             else:

+                 msg = "Weird sources line: {0}".format(line)

+                 raise RuntimeError(msg)

+ 

+             url_file = '/'.join([

+                 distgit_config["lookaside_location"],

+                 distgit_config["lookaside_uri_pattern"].format(**kwargs)

+             ])

+ 

+             download_file_and_check(url_file, kwargs, distgit_config)

+ 

+ 

+ def srpm(args, config):

+     """

+     Using the appropriate dist-git configuration, generate source RPM

+     file.  This requires running 'def sources()' first.

+     """

+     _, distgit_config = get_distgit_config(config)

+ 

+     cwd = os.getcwd()

+     sources_dir = os.path.join(cwd, distgit_config["sources"])

+     specs = os.path.join(cwd, distgit_config["specs"])

+     spec = get_spec(distgit_config)

+ 

+     mkdir_p(args.outputdir)

+ 

+     spec_abspath = os.path.join(specs, spec)

+ 

+     if args.mock_chroot:

+         command = [

+             "mock", "--buildsrpm",

+             "-r", args.mock_chroot,

+             "--spec", spec_abspath,

+             "--sources", sources_dir,

+             "--resultdir", args.outputdir,

+         ]

+     else:

+         command = [

+             "rpmbuild", "-bs", spec_abspath,

+             "--define", "dist %nil",

+             "--define", "_sourcedir {0}".format(sources_dir),

+             "--define", "_srcrpmdir {0}".format(args.outputdir),

+         ]

+ 

+     if args.dry_run or 'COPR_DISTGIT_CLIENT_DRY_RUN' in os.environ:

+         log_cmd(command, comment="Dry run")

+     else:

+         check_call(command)

+ 

+ 

+ def _get_argparser():

+     parser = argparse.ArgumentParser(prog="copr-distgit-client",

+                                      description="""\

+ A simple, configurable python utility that is able to download sources from

+ various dist-git instances, and generate source RPMs.

+ The utility is able to automatically map the .git/config clone URL into the

+ corresponding dist-git instance configuration.

+ """)

+ 

+     # main parser

+     default_confdir = os.environ.get("COPR_DISTGIT_CLIENT_CONFDIR",

+                                      "/etc/copr-distgit-client")

+     parser.add_argument(

+         "--configdir", default=default_confdir,

+         help="Where to load configuration files from")

+     parser.add_argument(

+         "--loglevel", default="info",

+         help="Python logging level, e.g. debug, info, error")

+     subparsers = parser.add_subparsers(

+         title="actions", dest="action")

+ 

+     # sources parser

+     subparsers.add_parser(

+         "sources",

+         description=(

+             "Using the 'url' .git/config, detect where the right DistGit "

+             "lookaside cache exists, and download the corresponding source "

+             "files."),

+         help="Download sources from the lookaside cache")

+ 

+     # srpm parser

+     srpm_parser = subparsers.add_parser(

+         "srpm",

+         help="Generate a source RPM",

+         description=(

+             "Generate a source RPM from the downloaded source files "

+             "by 'sources' command, please run 'sources' first."),

+     )

+     srpm_parser.add_argument(

+         "--outputdir",

+         default="/tmp",

+         help="Where to store the resulting source RPM")

+     srpm_parser.add_argument(

+         "--mock-chroot",

+         help=("Generate the SRPM in mock buildroot instead of on host.  The "

+               "argument is passed down to mock as the 'mock -r|--root' "

+               "argument."),

+     )

+     srpm_parser.add_argument(

+         "--dry-run", action="store_true",

+         help=("Don't produce the SRPM, just print the command which would be "

+               "otherwise called"),

+     )

+     return parser

+ 

+ 

+ def main():

+     """ The entrypoint for the whole logic """

+     args = _get_argparser().parse_args()

+     logging.basicConfig(

+         level=getattr(logging, args.loglevel.upper()),

+         format="%(levelname)s: %(message)s",

+     )

+     config = _load_config(args.configdir)

+ 

+     if args.action == "srpm":

+         srpm(args, config)

+     else:

+         sources(args, config)

@@ -1,3 +1,4 @@

+ import errno

  import logging

  import munch

  import subprocess
@@ -18,15 +19,6 @@

  CONF_DIRS = [os.getcwd(), "/etc/copr-rpmbuild"]

  

  

- class SourceType:

-     LINK = 1

-     UPLOAD = 2

-     PYPI = 5

-     RUBYGEMS = 6

-     SCM = 8

-     CUSTOM = 9

- 

- 

  def cmd_debug(result):

      log.debug("")

      log.debug("cmd: {0}".format(result.cmd))
@@ -58,9 +50,12 @@

          process = subprocess.Popen(

              cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, preexec_fn=preexec_fn)

          (stdout, stderr) = process.communicate()

-     except FileNotFoundError:

-         raise RuntimeError("Package with `{}` command is not installed".format(cmd[0]))

      except OSError as e:

+         if e.errno == errno.ENOENT:

+             raise RuntimeError(

+                 "Command '{0}' can not be executed.  Either the command "

+                 "itself isn't installed, or it's interpreter (shebang) is "

+                 "missing on the system".format(cmd[0]))

          raise RuntimeError(str(e))

  

      result = munch.Munch(
@@ -272,3 +267,41 @@

      def done(self):

          for timer in self.timers:

              timer.cancel()

+ 

+ def git_clone_url_basepath(clone_url):

+     """

+     Given the clone URL, get the last part of the URL, without the git suffix.

+     """

+     last_part = clone_url.rstrip("/").split("/")[-1]

+     if last_part.endswith(".git"):

+         return last_part[:-4]

+     return last_part

+ 

+ def git_clone_and_checkout(url, committish, repo_path, scm_type="git"):

+     """

+     Clone given URL (SCM_TYPE=svn/git) into REPO_PATH, and checkout the

+     COMMITTISH reference.

+     """

+     if scm_type == 'git':

+         clone_cmd = ['git', 'clone', url,

+                      repo_path, '--depth', '500',

+                      '--no-single-branch']

+     else:

+         clone_cmd = ['git', 'svn', 'clone', url,

+                      repo_path]

+ 

+     try:

+         run_cmd(clone_cmd)

+     except RuntimeError as e:

+         log.error(str(e))

+         if scm_type == 'git':

+             # re-try with deep-full clone

+             run_cmd(['git', 'clone', url, repo_path])

+         else:

+             raise e

+ 

+     if not committish:

+         committish = "master"

+ 

+     checkout_cmd = ['git', 'checkout', committish]

+     run_cmd(checkout_cmd, cwd=repo_path)

@@ -1,9 +1,11 @@

- from ..helpers import SourceType

+ from copr_common.enums import BuildSourceEnum

+ 

  from .rubygems import RubyGemsProvider

  from .pypi import PyPIProvider

  from .spec import UrlProvider

  from .scm import ScmProvider

  from .custom import CustomProvider

+ from .distgit import DistGitProvider

  

  

  __all__ = [RubyGemsProvider, PyPIProvider,
@@ -13,12 +15,13 @@

  def factory(source_type):

      try:

          return {

-             SourceType.LINK: UrlProvider,

-             SourceType.UPLOAD: UrlProvider,

-             SourceType.RUBYGEMS: RubyGemsProvider,

-             SourceType.PYPI: PyPIProvider,

-             SourceType.SCM: ScmProvider,

-             SourceType.CUSTOM: CustomProvider,

+             BuildSourceEnum.link: UrlProvider,

+             BuildSourceEnum.upload: UrlProvider,

+             BuildSourceEnum.rubygems: RubyGemsProvider,

+             BuildSourceEnum.pypi: PyPIProvider,

+             BuildSourceEnum.scm: ScmProvider,

+             BuildSourceEnum.custom: CustomProvider,

+             BuildSourceEnum.distgit: DistGitProvider,

          }[source_type]

      except KeyError:

          raise RuntimeError("No provider associated with this source type")

@@ -8,7 +8,7 @@

  

  

  class Provider(object):

-     def __init__(self, source_json, outdir, config):

+     def __init__(self, source_dict, outdir, config):

          self.outdir = outdir

          self.config = config

  
@@ -30,3 +30,10 @@

              enabled_protocols = string2list(self.config.get("main", "enabled_source_protocols"))

              rpmmacros.write("%__urlhelper_localopts --proto -all,{0}\n"

                              .format(','.join(["+"+protocol for protocol in enabled_protocols])))

+ 

+     def produce_srpm(self):

+         """

+         Using the TASK dict and the CONFIG, generate a source RPM in the

+         RESULTDIR.  Each method needs to override this one.

+         """

+         raise NotImplementedError

@@ -0,0 +1,39 @@

+ """

+ The "Build package from DistGit" method

+ """

+ 

+ import os

+ 

+ from copr_rpmbuild import helpers

+ from copr_rpmbuild.providers.base import Provider

+ 

+ 

+ class DistGitProvider(Provider):

+     """

+     DistGit provider, wrapper around copr-distgit-client script

+     """

+     def __init__(self, source_dict, outdir, config):

+         super(DistGitProvider, self).__init__(source_dict, outdir, config)

+         self.clone_url = source_dict["clone_url"]

+         self.committish = source_dict.get("committish")

+         self.clone_to = os.path.join(

+             self.workdir,

+             helpers.git_clone_url_basepath(self.clone_url))

+ 

+     def produce_sources(self):

+         """

+         Clone and download sources from DistGit lookaside cache.

+ 

+         We define this helper function on top of Provider() API because we use

+         the DistGit method on two places; first as a normal "source method", and

+         second for getting sources from our own "proxy" DistGit instance.

+         """

+         helpers.git_clone_and_checkout(self.clone_url, self.committish,

+                                        self.clone_to)

+         helpers.run_cmd(["copr-distgit-client", "sources"], cwd=self.clone_to)

+ 

+     def produce_srpm(self):

+         self.produce_sources()

+         helpers.run_cmd([

+             "copr-distgit-client", "srpm", "--outputdir", self.outdir

+         ], cwd=self.clone_to)

@@ -16,14 +16,14 @@

  

  

  class ScmProvider(Provider):

-     def __init__(self, source_json, outdir, config):

-         super(ScmProvider, self).__init__(source_json, outdir, config)

-         self.scm_type = source_json.get('type') or 'git'

-         self.clone_url = source_json.get('clone_url')

-         self.committish = source_json.get('committish') or 'master'

-         self.repo_subdir = source_json.get('subdirectory') or ''

-         self.spec_relpath = source_json.get('spec') or ''

-         self.srpm_build_method = source_json.get('srpm_build_method') or 'rpkg'

+     def __init__(self, source_dict, outdir, config):

+         super(ScmProvider, self).__init__(source_dict, outdir, config)

+         self.scm_type = source_dict.get('type') or 'git'

+         self.clone_url = source_dict.get('clone_url')

+         self.committish = source_dict.get('committish') or 'master'

+         self.repo_subdir = source_dict.get('subdirectory') or ''

+         self.spec_relpath = source_dict.get('spec') or ''

+         self.srpm_build_method = source_dict.get('srpm_build_method') or 'rpkg'

          self.repo_dirname = os.path.splitext(os.path.basename(

              self.clone_url.rstrip('/')))[0]

          self.repo_path = helpers.path_join(self.workdir, self.repo_dirname)
@@ -111,7 +111,11 @@

                  mock_bind_mount_cmd_part, '--chroot', make_srpm_cmd_part]

  

      def produce_srpm(self):

-         self.clone_and_checkout()

+         helpers.git_clone_and_checkout(

+             self.clone_url,

+             self.committish,

+             self.repo_path,

+             self.scm_type)

          cmd = {

              'rpkg': self.get_rpkg_command,

              'tito': self.get_tito_command,
@@ -122,34 +126,3 @@

              raise RuntimeError("The user-defined SCM subdirectory `{}' doesn't exist within this repository {}"

                                 .format(self.repo_subdir, self.clone_url))

          return run_cmd(cmd, cwd=self.repo_subpath)

- 

-     def produce_sources(self):

-         self.clone_and_checkout()

- 

-         copy_cmd = ['cp', '-r', '.', self.outdir]

-         run_cmd(copy_cmd, cwd=self.repo_subpath)

- 

-         cmd = ['rpkg', '-C', self.generate_rpkg_config(),

-                'sources', '--outdir', self.outdir]

-         return run_cmd(cmd, cwd=self.repo_subpath)

- 

-     def clone_and_checkout(self):

-         if self.scm_type == 'git':

-             clone_cmd = ['git', 'clone', self.clone_url,

-                          self.repo_path, '--depth', '500',

-                          '--no-single-branch']

-         else:

-             clone_cmd = ['git', 'svn', 'clone', self.clone_url,

-                          self.repo_path]

- 

-         try:

-             helpers.run_cmd(clone_cmd)

-         except RuntimeError as e:

-             log.error(str(e))

-             if self.scm_type == 'git':

-                 helpers.run_cmd(['git', 'clone', self.clone_url, self.repo_path])

-             else:

-                 raise e

- 

-         checkout_cmd = ['git', 'checkout', self.committish]

-         helpers.run_cmd(checkout_cmd, cwd=self.repo_path)

@@ -0,0 +1,25 @@

+ [fedora]

+ clone_hostnames =

+     pkgs.fedoraproject.org

+     src.fedoraproject.org

+ lookaside_location = https://src.fedoraproject.org

+ lookaside_uri_pattern = repo/pkgs/rpms/{name}/{filename}/{hashtype}/{hash}/{filename}

+ 

+ [centos]

+ clone_hostnames = git.centos.org

+ lookaside_location = https://git.centos.org

+ sources_file = .{name}.metadata

+ specs = SPECS

+ sources = SOURCES

+ default_sum = SHA1

+ lookaside_uri_pattern = sources/{name}/{refspec}/{hash}

+ 

+ [fedora-copr]

+ clone_hostnames = copr-dist-git.fedorainfracloud.org

+ lookaside_location = https://copr-dist-git.fedorainfracloud.org

+ lookaside_uri_pattern = repo/pkgs/{namespace[1]}/{namespace[0]}/{name}/{filename}/{hashtype}/{hash}/{filename}

+ 

+ [fedora-copr-dev]

+ clone_hostnames = copr-dist-git-dev.fedorainfracloud.org

+ lookaside_location = https://copr-dist-git-dev.fedorainfracloud.org

+ lookaside_uri_pattern = repo/pkgs/{namespace[1]}/{namespace[0]}/{name}/{filename}/{hashtype}/{hash}/{filename}

file modified
+8 -12
@@ -23,9 +23,8 @@

  

  from copr_rpmbuild import providers

  from copr_rpmbuild.builders.mock import MockBuilder

- from copr_rpmbuild.helpers import read_config, extract_srpm, locate_srpm, \

-      SourceType, parse_copr_name, dump_live_log, copr_chroot_to_task_id

- 

+ from copr_rpmbuild.helpers import read_config, \

+      parse_copr_name, dump_live_log, copr_chroot_to_task_id

  from six.moves.urllib.parse import urlparse, urljoin, urlencode

  

  log = logging.getLogger(__name__)
@@ -252,16 +251,13 @@

  

      sourcedir = tempfile.mkdtemp(prefix="copr-rpmbuild-")

      try:

-         scm_provider = providers.ScmProvider(task["source_json"], sourcedir, config)

-         if task.get("fetch_sources_only"):

-             scm_provider.produce_sources()

-         else:

-             scm_provider.produce_srpm()

-             built_srpm = locate_srpm(sourcedir)

-             extract_srpm(built_srpm, sourcedir)

- 

+         distgit = providers.DistGitProvider(

+             {"clone_url": task["git_repo"], "committish": task["git_hash"]},

+             sourcedir, config,

+         )

+         distgit.produce_sources()

          resultdir = config.get("main", "resultdir")

-         builder = MockBuilder(task, sourcedir, resultdir, config)

+         builder = MockBuilder(task, distgit.clone_to, resultdir, config)

          builder.run()

          builder.touch_success_file()

      finally:

file modified
+16 -2
@@ -1,5 +1,19 @@

- #!/bin/sh

+ #! /bin/bash

  

  set -e

  

- PYTHONPATH=".:$PYTHONPATH" "${PYTHON:-python3}" -m pytest -s tests "$@"

+ args=()

+ 

+ coverage=( --cov-report term-missing --cov bin --cov copr_rpmbuild --cov copr_distgit_client )

+ for arg; do

+     case $arg in

+     --no-coverage) coverage=() ;;

+     *) args+=( "$arg" ) ;;

+     esac

+ done

+ 

+ abspath=$(readlink -f .)

+ common_path=$(readlink -f "$abspath"/../common)

+ export PYTHONPATH="${PYTHONPATH+$PYTHONPATH:}$common_path:$abspath"

+ export PATH=$(readlink -f bin):$PATH

+ "${PYTHON:-python3}" -m pytest -s tests "${coverage[@]}" "${args[@]}"

@@ -0,0 +1,88 @@

+ """

+ Test the DistGit provider

+ """

+ 

+ import os

+ import shutil

+ import tempfile

+ 

+ import configparser

+ 

+ from copr_distgit_client import check_output

+ from copr_rpmbuild.providers.distgit import DistGitProvider

+ 

+ try:

+     from unittest import mock

+ except ImportError:

+     # Python 2 version depends on mock

+     import mock

+ 

+ from tests.test_distgit_client import init_git

+ from tests import TestCase

+ 

+ class TestDistGitProvider(TestCase):

+     olddir = None

+     workdir = None

+     origin = None

+     outdir = None

+     configdir = None

+     lookaside = None

+     env_patch = None

+     main_config = None

+ 

+     def _setup_configdir(self):

+         self.configdir = os.path.join(self.workdir, "configdir")

+         os.mkdir(self.configdir)

+ 

+         config = """\n

+ [lllocal]

+ clone_hostnames = localhost

+ lookaside_location = file://{workdir}

+ lookaside_uri_pattern = lookaside/{{filename}}

+ """.format(workdir=self.workdir)

+         with open(os.path.join(self.configdir, "default.ini"), "w+") as fdc:

+             fdc.write(config)

+ 

+         self.env_patch = mock.patch.dict(os.environ, {

+             "COPR_DISTGIT_CLIENT_CONFDIR": self.configdir,

+             "COPR_DISTGIT_CLIENT_DRY_RUN": "true",

+         })

+         self.env_patch.start()

+ 

+     def setup_method(self, method):

+         _unused_but_needed_for_el6 = (method)

+         self.workdir = tempfile.mkdtemp(prefix="copr-distgit-provider-test-")

+         self.origin = os.path.join(self.workdir, "origin.git")

+         os.mkdir(self.origin)

+         self.outdir = os.path.join(self.workdir, "out")

+         os.chdir(self.origin)

+         self.lookaside = os.path.join(self.workdir, "lookaside")

+         os.mkdir(self.lookaside)

+         datafile = os.path.join(self.lookaside, "datafile")

+         with open(datafile, "w") as fdd:

+             fdd.write("data content\n")

+ 

+         output = check_output(['md5sum', datafile])

+         md5 = output.decode("utf-8").strip().split(' ')[0]

+         init_git([

+             ("test.spec", "specfile_content\n"),

+             ("sources", "{md5} datafile\n".format(md5=md5)),

+         ])

+         self._setup_configdir()

+ 

+         self.main_config = configparser.ConfigParser()

+         self.main_config.add_section("main")

+         self.main_config.set("main", "enabled_source_protocols", "file")

+ 

+     def teardown_method(self, method):

+         _unused_but_needed_for_el6 = (method)

+         shutil.rmtree(self.workdir)

+         self.env_patch.stop()

+ 

+     def test_distgit_method(self):

+         os.mkdir(self.outdir)

+         source_dict = {"clone_url": self.origin}

+         dgp = DistGitProvider(source_dict, self.outdir, self.main_config)

+         dgp.produce_srpm()

+         assert os.path.exists(os.path.join(self.outdir, "obtain-sources",

+                                            "origin", "datafile"))

@@ -0,0 +1,173 @@

+ """

+ copr-distgit-client testsuite

+ """

+ 

+ import os

+ import shutil

+ import tempfile

+ try:

+     from unittest import mock

+ except ImportError:

+     import mock

+ 

+ from copr_distgit_client import sources, srpm, _load_config, check_output

+ 

+ # pylint: disable=useless-object-inheritance

+ 

+ def init_git(files=None):

+     """ Initialize .git/ directory """

+ 

+     check_output(["git", "init", "."])

+     shutil.rmtree(".git/hooks")

+     check_output(["git", "config", "user.email", "you@example.com"])

+     check_output(["git", "config", "user.name", "Your Name"])

+     check_output(["git", "config", "advice.detachedHead", "false"])

+ 

+     for filename, content in files:

+         dirname = os.path.dirname(filename)

+         try:

+             os.makedirs(dirname)

+         except OSError:

+             pass

+         with open(filename, "w") as filed:

+             filed.write(content)

+         check_output(["git", "add", filename])

+ 

+     check_output(["git", "commit", "-m", "initial"])

+ 

+ 

+ def git_origin_url(url):

+     """ setup .git/config with core.origin.url == URL """

+     with open(".git/config", "a+") as gcf:

+         gcf.write('[remote "origin"]\n')

+         gcf.write('url = {0}\n'.format(url))

+ 

+ 

+ class TestDistGitDownload(object):

+     """ Test the 'sources()' method """

+     config = None

+     args = None

+     workdir = None

+ 

+     def setup_method(self, method):

+         _unused_but_needed_for_el6 = (method)

+         testdir = os.path.dirname(__file__)

+         projdir = os.path.dirname(testdir)

+         config_dir = os.path.join(projdir, 'etc/copr-distgit-client')

+         self.config = _load_config(config_dir)

+         class _Args:

+             # pylint: disable=too-few-public-methods

+             dry_run = False

+         self.args = _Args()

+         self.workdir = tempfile.mkdtemp(prefix="copr-distgit-test-")

+         os.chdir(self.workdir)

+ 

+     def teardown_method(self, method):

+         _unused_but_needed_for_el6 = (method)

+         shutil.rmtree(self.workdir)

+ 

+ 

+     @mock.patch('copr_distgit_client.download_file_and_check')

+     def test_copr_distgit(self, download):

+         init_git([

+             ("test.spec", ""),

+             ("sources", "2102fd0602de72e58765adcbf92349d8 retrace-server-git-955.3e4742a.tar.gz\n"),

+         ])

+         git_origin_url("https://copr-dist-git.fedorainfracloud.org/git/@abrt/retrace-server-devel/retrace-server.git")

+         sources(self.args, self.config)

+         assert len(download.call_args_list) == 1

+         assert download.call_args_list[0][0][0] == (

+             "https://copr-dist-git.fedorainfracloud.org/repo/pkgs/"

+             "@abrt/retrace-server-devel/test/retrace-server-git-955.3e4742a.tar.gz/"

+             "md5/2102fd0602de72e58765adcbf92349d8/retrace-server-git-955.3e4742a.tar.gz"

+         )

+ 

+     @mock.patch('copr_distgit_client.download_file_and_check')

+     def test_fedora_old(self, download):

+         """

+         Old sources format + ssh clone

+         """

+         init_git([

+             ("tar.spec", ""),

+             ("sources", "0ced6f20b9fa1bea588005b5ad4b52c1  tar-1.26.tar.xz\n"),

+         ])

+         git_origin_url("ssh://praiskup@pkgs.fedoraproject.org/rpms/tar")

+         sources(self.args, self.config)

+         assert len(download.call_args_list) == 1

+         assert download.call_args_list[0][0][0] == (

+             "https://src.fedoraproject.org/repo/pkgs/rpms/"

+             "tar/tar-1.26.tar.xz/md5/0ced6f20b9fa1bea588005b5ad4b52c1/tar-1.26.tar.xz"

+         )

+ 

+     @mock.patch('copr_distgit_client.download_file_and_check')

+     def test_fedora_new(self, download):

+         """

+         New sources format + anonymous clone

+         """

+         sha512 = (

+             "1bd13854009b6ee08958481738e6bf661e40216a2befe461d06b4b350eb882e43"

+             "1b3a4eeea7ca1d35d37102df76194c9d933df2b18b3c5401350e9fc17017750"

+         )

+         init_git([

+             ("tar.spec", ""),

+             ("sources", "SHA512 (tar-1.32.tar.xz) = {0}\n".format(sha512)),

+         ])

+         git_origin_url("https://src.fedoraproject.org/rpms/tar.git")

+         sources(self.args, self.config)

+         assert len(download.call_args_list) == 1

+         url = (

+             "https://src.fedoraproject.org/repo/pkgs/rpms/"

+             "tar/tar-1.32.tar.xz/sha512/{sha512}/tar-1.32.tar.xz"

+         ).format(sha512=sha512)

+         assert download.call_args_list[0][0][0] == url

+ 

+     @mock.patch('copr_distgit_client.download_file_and_check')

+     def test_centos(self, download):

+         """

+         Anonymous centos clone

+         """

+         init_git([

+             ("SPECS/centpkg-minimal.spec", ""),

+             (".centpkg-minimal.metadata", "cf9ce8d900768ed352a6f19a2857e64403643545 SOURCES/centpkg-minimal.tar.gz\n"),

+         ])

+         git_origin_url("https://git.centos.org/rpms/centpkg-minimal.git")

+         sources(self.args, self.config)

+         assert len(download.call_args_list) == 1

+         assert download.call_args_list[0][0][0] == (

+             "https://git.centos.org/sources/centpkg-minimal/master/"

+             "cf9ce8d900768ed352a6f19a2857e64403643545"

+         )

+         assert download.call_args_list[0][0][2]["sources"] == "SOURCES"

+         assert download.call_args_list[0][0][1]["hashtype"] == "sha1"

+ 

+         oldref = check_output(["git", "rev-parse", "HEAD"]).decode("utf-8")

+         oldref = oldref.strip()

+ 

+         # create new commit, and checkout back (so --show-current is not set)

+         check_output(["git", "commit", "--allow-empty", "-m", "empty"])

+         check_output(["git", "checkout", "-q", oldref])

+ 

+         sources(self.args, self.config)

+         assert download.call_args_list[1][0][0] == (

+             "https://git.centos.org/sources/centpkg-minimal/{0}/"

+             "cf9ce8d900768ed352a6f19a2857e64403643545"

+         ).format(oldref)

+ 

+ 

+     @mock.patch("copr_distgit_client.subprocess.check_call")

+     def test_centos_download(self, patched_check_call):

+         init_git([

+             ("SPECS/centpkg-minimal.spec", ""),

+             (".centpkg-minimal.metadata", "cf9ce8d900768ed352a6f19a2857e64403643545 SOURCES/centpkg-minimal.tar.gz\n"),

+         ])

+         git_origin_url("https://git.centos.org/rpms/centpkg-minimal.git")

+         setattr(self.args, "outputdir", os.path.join(self.workdir, "result"))

+         setattr(self.args, "mock_chroot", None)

+         srpm(self.args, self.config)

+         assert patched_check_call.call_args_list[0][0][0] == [

+             'rpmbuild', '-bs',

+             os.path.join(self.workdir, "SPECS", "centpkg-minimal.spec"),

+             '--define', 'dist %nil',

+             '--define', '_sourcedir ' + self.workdir + '/SOURCES',

+             '--define', '_srcrpmdir ' + self.workdir + '/result',

+         ]

file modified
+4 -2
@@ -3,8 +3,10 @@

  import unittest

  import shutil

  

+ from copr_common.enums import BuildSourceEnum

+ 

  from main import produce_srpm

- from copr_rpmbuild.helpers import SourceType

+ 

  

  try:

       from unittest import mock
@@ -19,7 +21,7 @@

  

      config = {}

      resultdir = "/path/to/non/existing/directory"

-     task = {"source_type": SourceType.UPLOAD,

+     task = {"source_type": BuildSourceEnum.upload,

              "source_json": {"url": "http://foo.ex/somepackage.spec"}}

  

      @mock.patch("copr_rpmbuild.providers.spec.UrlProvider.produce_srpm")

@@ -1,19 +1,19 @@

  import unittest

  import pytest

  

+ from copr_common.enums import BuildSourceEnum

+ 

  from copr_rpmbuild.providers import (factory, RubyGemsProvider, PyPIProvider,

                                       UrlProvider)

  

- from copr_rpmbuild.helpers import SourceType

- 

  

  class TestProvidersFactory(unittest.TestCase):

      def setUp(self):

          self.not_existing_source_type = 99

  

      def test_factory(self):

-         self.assertEqual(factory(SourceType.RUBYGEMS), RubyGemsProvider)

-         self.assertEqual(factory(SourceType.PYPI), PyPIProvider)

-         self.assertEqual(factory(SourceType.LINK), UrlProvider)

+         self.assertEqual(factory(BuildSourceEnum.rubygems), RubyGemsProvider)

+         self.assertEqual(factory(BuildSourceEnum.pypi), PyPIProvider)

+         self.assertEqual(factory(BuildSourceEnum.link), UrlProvider)

          with pytest.raises(RuntimeError):

              factory(self.not_existing_source_type)

very rough WIP for now

Ready for review now.

Metadata Update from @praiskup:
- Pull-request tagged with: wip

3 years ago

1 new commit added

  • Fedora Copr dist git instances
3 years ago

1 new commit added

  • Create 'srpm'.
3 years ago

rebased onto eb59864f589a68505d16751d886c1d34704715d8

3 years ago

rebased onto 860a5c9befff91c9b135a56e9a05dac12734e54e

3 years ago

8 new commits added

  • fix add/edit-package-distgit
  • cli, frontend, python: new command build-distgit
  • fixup for previous commit
  • cli: don't dump --memory option into --help
  • common, frontend: new "simple" distgit method
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • frontend: package name validator as a separate class
3 years ago

1 new commit added

  • frontend: fix paugre-events to accept distgit
3 years ago

2 new commits added

  • form defaults need to be set at __init__() time, not python parsing time
  • Drop aliases, they don't work on RHEL <= 7
3 years ago

6 new commits added

  • pylint
  • dist-git-client tests moved to appropriate filename
  • rpmbuild: new dist-git method
  • git_clone_and_checkout fix
  • rpmbuild: better Provider object design
  • updated copr-distgit-client
3 years ago

1 new commit added

  • pylint2
3 years ago

1 new commit added

  • pylint3
3 years ago

1 new commit added

  • pylint4
3 years ago

pretty please pagure-ci rebuild

3 years ago

rebased onto 7ba0aea6ddecd4093c286f6d802ac300d7b6f1c9

3 years ago

Metadata Update from @praiskup:
- Pull-request untagged with: wip
- Pull-request tagged with: needs-work

3 years ago

Metadata Update from @praiskup:
- Pull-request untagged with: needs-work
- Pull-request tagged with: needs-tests

3 years ago

1 new commit added

  • frontend: don't duplicate BuildSourceEnum + pylint
3 years ago

1 new commit added

  • rpmbuild: drop SourceType and rely on BuildSourceEnum
3 years ago

This change is surprisingly complicated when compared to how this is done in frontend/run_tests.sh or cli/run_tests.sh. Do we really need the for loop?

rename "source_json" into "source_spec" (it's not a JSON anymore at that time).

It's not, but IMHO it is still a more descriptive name than source_spec. What about source_dict if we want to change the name?

With this, you can put the option to any place on command line. But sure I can drop this, or "compress" the code to fewer lines.

Yeah, I didn't want to put the "type" to the name, we can have type hints ... but I can change that, sure.

I would rather see this as either a class or a function def validate_package_name(form, field): calling this Regexp (similarly to how NameCharactersValidator.__call__ is implemented).

But then this line is only to dismiss pylint warnings that BuildSourceEnum is imported but not used, I guess? In that case I would go with just simply adding a #pylint: comment to it. If there is some other reason for this line, can you please note that in the comment above?

That attribute should be always defined, I think we can go just if not F.build_requires_package_name:

This is just fine, but it is duplicating a logic from DistGitProvider. I would like to see DistGitProvider.produce_srpm split into itself and DistGitProvider.produce_sources. Where produce_srpm would be just calling produce_sources and one command on top of it.

Then, DistGitProvider.produce_sources could be used here.

I made you a few notes but overall I like the PR (in particular some of the cleanups regarding python-copr-common, and then obviously the main feature it brings). I would like to see some beaker tests though. Also, a UI screenshot would be appreciated.

rebased onto 7bec67df1a410dfe94caa35e3085736778993875

3 years ago

It's not, but IMHO it is still a more descriptive name than source_spec. What about source_dict if we want to change the name?

I didn't fell in love to the source_dict name, but done.

This change is surprisingly complicated when compared to how this is done in frontend/run_tests.sh or cli/run_tests.sh. Do we really need the for loop?

I at least made the loop less talkative. Lemme know if you really want to drop
that for loop entirely.

I would rather see this as either a class or a function def validate_package_name(form, field):
calling this Regexp (similarly to how NameCharactersValidator.call is implemented).

Fixed. I originally wanted to use this on two places (once in BasePackageForm and once
in DistGitSimple form for dist-git specific name). Though I decided not to eventually, and
the abstraction was done for no real puprpose in the end.

But then this line is only to dismiss pylint warnings that BuildSourceEnum is imported but not
used, I guess? In that case I would go with just simply adding a #pylint: comment to it.

Done.

This is just fine, but it is duplicating a logic from DistGitProvider. I would like to see DistGitProvider.produce_srpm split into itself and DistGitProvider.produce_sources. Where produce_srpm would be just calling produce_sources and one command on top of it.
Then, DistGitProvider.produce_sources could be used here.

Done.

I would like to see some beaker tests though. Also, a UI screenshot would be appreciated.

Sure, I plan to add unittests as well, it is still "needs-tests" ATM. Thank you for the pre-review.

8 new commits added

  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

9 new commits added

  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

I at least made the loop less talkative. Lemme know if you really want to drop
that for loop entirely.

If it brings any value to you, feel free to keep it. I can't imagine it though, since I can pass additional parameters for all run_tests.sh scripts and never needed to care about their order. What is your use-case?

Nitpick, you have two blank lines here, pylint will likely yell at you.

Thank you for the changes, looks good.

9 new commits added

  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

9 new commits added

  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

If it brings any value to you, feel free to keep it. I can't imagine it though, since I can pass additional parameters for all run_tests.sh scripts and never needed to care about their order. What is your use-case?

It sounds convenient to use ./run_tests.sh -k test_produce_rpm --no-coverage as well as
./run_tests.sh --no-coverage -k test_produce_rpm. Even though I rarely turn coverage
off, only in limitted mock environment ...

Nitpick, you have two blank lines here, pylint will likely yell at you.

It did not, but good catch - fixed. I don't know, but I haven't seen this kind of error
for quite a long time now.

I am still working on the tests, though. I'll finish this PR on Monday.

9 new commits added

  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

rebased onto c843b048293ad012bee57655b7262500d40604f3

3 years ago

9 new commits added

  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

9 new commits added

  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

9 new commits added

  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

10 new commits added

  • rpmbuild: catch FileNotFound on el6 correctly
  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

rebased onto 6b3c8174a33b07d504b4560843dcd69a2078588d

3 years ago

13 new commits added

  • behave: first experiments with behave in Copr
  • beaker: move Install/*/runtest.sh into an image
  • beaker: minimize docker environment image
  • rpmbuild: catch FileNotFound on el6 correctly
  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

14 new commits added

  • build_aux: make linter produce diff only for project subdir
  • behave: first experiments with behave in Copr
  • beaker: move Install/*/runtest.sh into an image
  • beaker: minimize docker environment image
  • rpmbuild: catch FileNotFound on el6 correctly
  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

Metadata Update from @praiskup:
- Pull-request untagged with: needs-tests

3 years ago

rebased onto 2c518f7fcc68bef6054bcbf068085176d3714220

3 years ago

14 new commits added

  • build_aux: make linter produce diff only for project subdir
  • behave: first experiments with behave in Copr
  • beaker: move Install/*/runtest.sh into an image
  • beaker: minimize docker environment image
  • rpmbuild: catch FileNotFound on el6 correctly
  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
3 years ago

rebased onto 35fe78a3a6242a6c7a583eb10c7fde23eebcc1d6

3 years ago

Please take a look.

I experimented a bit with the BDD python3-behave platform (a rather large patch
because I needed to do add the initial file tree layout).

The first two patches are from the PR#1503, so once it is merged I'll rebase this.

Also, note that at the moment this code is deployed on staging.

14 new commits added

  • build_aux: make linter produce diff only for project subdir
  • behave: first experiments with behave in Copr
  • rpmbuild: catch FileNotFound on el6 correctly
  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
  • beaker, doc: move Install/*/runtest.sh into an image
  • beaker: minimize docker environment image
3 years ago

1 new commit added

  • frontend: use "source build" collocation, not "srpm build"
3 years ago

15 new commits added

  • frontend: use "source build" collocation, not "srpm build"
  • build_aux: make linter produce diff only for project subdir
  • behave: first experiments with behave in Copr
  • rpmbuild: catch FileNotFound on el6 correctly
  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
  • beaker, doc: move Install/*/runtest.sh into an image
  • beaker: minimize docker environment image
3 years ago

15 new commits added

  • frontend: use "source build" collocation, not "srpm build"
  • build_aux: make linter produce diff only for project subdir
  • behave: first experiments with behave in Copr
  • rpmbuild: catch FileNotFound on el6 correctly
  • rpmbuild: drop SourceType and rely on BuildSourceEnum
  • frontend: don't duplicate enums.BuildSourceEnum
  • common, cli, python, rpmbuild, frontend, backend: DistGit source method
  • common: RHEL6 fix for ModuleStatusEnum
  • frontend: nicer message in package name validator
  • frontend: nicer web-UI error message on missing default source method
  • frontend: catch NoPackageSourceException in apiv3 on rebuild
  • rpmbuild: fix Provider class design
  • rpmbuild: inform about testsuite coverage
  • beaker, doc: move Install/*/runtest.sh into an image
  • beaker: minimize docker environment image
3 years ago

1 new commit added

  • beaker: fix runtest.sh package count assertions
3 years ago

Beaker tests are passing now.

rebased onto 27de4c7a41f2e97cd10c6ec88881360aed0aaa33

3 years ago

Rebased (just dropped the commits from #1503).

rebased onto 0f162fe8d0cd79ac28348d79c517d169ab48c566

3 years ago

Please take another look.

beaker-tests/Sanity/copr-cli-basic-operations/runtest-distgit.sh

I think runtest-behave.sh would be a better option because

1) I guess they are not testing just the distgit but the stack as a whole
2) We could easily add new, distgit-non-related tests

but otherwise, +1.
Thank you for the PR, and I am really interested how what is going to be our experience with those behave tests.

I tagged those scenarios with @distgit tag ... and the wrapper uses --tags option.
So that particular wrapper should stay dist-git oriented ... as long as we want to
have those tests run in parallel, we need to split them like that even in the future (behave
is serial-only, unless we use some nonstandard third-party patches).

Alright. Let's go for it, +1
We can always make changes in the future.

rebased onto c0a15a9

3 years ago

Pull-Request has been merged by praiskup

3 years ago

thank you for the review!

Metadata
Flags
jenkins
success (100%)
Build #517 successful (commit: 1a762c07)
3 years ago
Copr build
success (100%)
#1672620
3 years ago
Copr build
success (100%)
#1672619
3 years ago
Copr build
success (100%)
#1672618
3 years ago
Copr build
success (100%)
#1672617
3 years ago
Copr build
success (100%)
#1672616
3 years ago
Copr build
success (100%)
#1672615
3 years ago
jenkins
success (100%)
Build #509 successful (commit: 950c198b)
3 years ago
Copr build
success (100%)
#1657532
3 years ago
Copr build
success (100%)
#1657531
3 years ago
Copr build
pending (50%)
#1657530
3 years ago
Copr build
success (100%)
#1657529
3 years ago
Copr build
success (100%)
#1657528
3 years ago
Copr build
success (100%)
#1657527
3 years ago
jenkins
success (100%)
Build #507 successful (commit: 93970cba)
3 years ago
Copr build
success (100%)
#1657518
3 years ago
Copr build
success (100%)
#1657517
3 years ago
Copr build
success (100%)
#1657516
3 years ago
Copr build
success (100%)
#1657515
3 years ago
Copr build
success (100%)
#1657514
3 years ago
Copr build
success (100%)
#1657513
3 years ago
Copr build
pending (50%)
#1656841
3 years ago
Copr build
success (100%)
#1656840
3 years ago
Copr build
success (100%)
#1656839
3 years ago
Copr build
success (100%)
#1656838
3 years ago
jenkins
success (100%)
Build #501 successful (commit: 9032633a)
3 years ago
Copr build
success (100%)
#1656837
3 years ago
Copr build
pending (50%)
#1656836
3 years ago
Copr build
success (100%)
#1656483
3 years ago
Copr build
success (100%)
#1656482
3 years ago
jenkins
success (100%)
Build #496 successful (commit: 528b6b5b)
3 years ago
Copr build
success (100%)
#1656481
3 years ago
Copr build
success (100%)
#1656480
3 years ago
Copr build
success (100%)
#1656479
3 years ago
Copr build
pending (50%)
#1656478
3 years ago
jenkins
failure
Build #495 failed (commit: c7207d9b)
3 years ago
Copr build
success (100%)
#1656456
3 years ago
Copr build
success (100%)
#1656455
3 years ago
Copr build
success (100%)
#1656454
3 years ago
Copr build
success (100%)
#1656453
3 years ago
Copr build
success (100%)
#1656452
3 years ago
Copr build
failure
#1656451
3 years ago
Copr build
success (100%)
#1656447
3 years ago
jenkins
success (100%)
Build #494 successful (commit: 370a2c08)
3 years ago
Copr build
success (100%)
#1656446
3 years ago
Copr build
success (100%)
#1656445
3 years ago
Copr build
success (100%)
#1656444
3 years ago
Copr build
success (100%)
#1656443
3 years ago
Copr build
success (100%)
#1656442
3 years ago
Copr build
success (100%)
#1656357
3 years ago
Copr build
success (100%)
#1656356
3 years ago
jenkins
success (100%)
Build #493 successful (commit: 6c1753a8)
3 years ago
Copr build
success (100%)
#1656355
3 years ago
Copr build
success (100%)
#1656354
3 years ago
Copr build
success (100%)
#1656353
3 years ago
Copr build
pending (50%)
#1656352
3 years ago
jenkins
success (100%)
Build #492 successful (commit: d3213d6c)
3 years ago
Copr build
success (100%)
#1656211
3 years ago
Copr build
success (100%)
#1656210
3 years ago
Copr build
success (100%)
#1656209
3 years ago
Copr build
success (100%)
#1656208
3 years ago
Copr build
success (100%)
#1656207
3 years ago
Copr build
success (100%)
#1656206
3 years ago
jenkins
success (100%)
Build #490 successful (commit: ba6a90d4)
3 years ago
Copr build
success (100%)
#1656193
3 years ago
Copr build
success (100%)
#1656192
3 years ago
Copr build
success (100%)
#1656191
3 years ago
Copr build
success (100%)
#1656190
3 years ago
Copr build
success (100%)
#1656189
3 years ago
Copr build
success (100%)
#1656188
3 years ago
jenkins
success (100%)
Build #489 successful (commit: a8a16061)
3 years ago
Copr build
success (100%)
#1656178
3 years ago
Copr build
success (100%)
#1656177
3 years ago
Copr build
success (100%)
#1656176
3 years ago
Copr build
success (100%)
#1656175
3 years ago
Copr build
success (100%)
#1656174
3 years ago
Copr build
success (100%)
#1656173
3 years ago
jenkins
success (100%)
Build #488 successful (commit: e8898b53)
3 years ago
Copr build
success (100%)
#1656172
3 years ago
Copr build
success (100%)
#1656171
3 years ago
Copr build
success (100%)
#1656170
3 years ago
Copr build
success (100%)
#1656169
3 years ago
Copr build
success (100%)
#1656168
3 years ago
Copr build
success (100%)
#1656167
3 years ago
Copr build
success (100%)
#1656150
3 years ago
jenkins
failure
Build #487 failed (commit: c2211462)
3 years ago
Copr build
success (100%)
#1656149
3 years ago
Copr build
success (100%)
#1656148
3 years ago
Copr build
success (100%)
#1656147
3 years ago
Copr build
success (100%)
#1656146
3 years ago
Copr build
success (100%)
#1656145
3 years ago
jenkins
success (100%)
Build #486 successful (commit: 2ca5fcaa)
3 years ago
Copr build
success (100%)
#1655908
3 years ago
Copr build
success (100%)
#1655907
3 years ago
Copr build
success (100%)
#1655906
3 years ago
Copr build
success (100%)
#1655905
3 years ago
Copr build
success (100%)
#1655904
3 years ago
Copr build
success (100%)
#1655903
3 years ago
jenkins
success (100%)
Build #472 successful (commit: 38781dd4)
3 years ago
Copr build
success (100%)
#1652639
3 years ago
Copr build
success (100%)
#1652638
3 years ago
Copr build
success (100%)
#1652637
3 years ago
Copr build
success (100%)
#1652636
3 years ago
Copr build
failure
#1652635
3 years ago
Copr build
success (100%)
#1652634
3 years ago
jenkins
success (100%)
Build #467 successful (commit: d5b54066)
3 years ago
Copr build
success (100%)
#1652275
3 years ago
Copr build
success (100%)
#1652274
3 years ago
Copr build
success (100%)
#1652273
3 years ago
Copr build
failure
#1652272
3 years ago
Copr build
failure
#1652271
3 years ago
Copr build
success (100%)
#1652270
3 years ago
Copr build
success (100%)
#1652268
3 years ago
Copr build
success (100%)
#1652267
3 years ago
jenkins
success (100%)
Build #466 successful (commit: d5b54066)
3 years ago
Copr build
success (100%)
#1652266
3 years ago
Copr build
failure
#1652265
3 years ago
Copr build
failure
#1652264
3 years ago
Copr build
success (100%)
#1652263
3 years ago
Copr build
success (100%)
#1652259
3 years ago
Copr build
success (100%)
#1652258
3 years ago
jenkins
success (100%)
Build #465 successful (commit: e5feb515)
3 years ago
Copr build
success (100%)
#1652257
3 years ago
Copr build
failure
#1652256
3 years ago
Copr build
failure
#1652255
3 years ago
Copr build
pending (50%)
#1652254
3 years ago
jenkins
failure
Build #464 failed (commit: db401e83)
3 years ago
Copr build
success (100%)
#1652222
3 years ago
Copr build
success (100%)
#1652221
3 years ago
Copr build
success (100%)
#1652220
3 years ago
Copr build
failure
#1652219
3 years ago
Copr build
failure
#1652218
3 years ago
Copr build
success (100%)
#1652217
3 years ago
jenkins
success (100%)
Build #463 successful (commit: 7a264b85)
3 years ago
Copr build
success (100%)
#1651577
3 years ago
Copr build
success (100%)
#1651576
3 years ago
Copr build
success (100%)
#1651575
3 years ago
Copr build
success (100%)
#1651574
3 years ago
Copr build
failure
#1651573
3 years ago
Copr build
success (100%)
#1651572
3 years ago
jenkins
success (100%)
Build #462 successful (commit: dd3fd2f9)
3 years ago
Copr build
failure
#1646846
3 years ago
Copr build
success (100%)
#1646845
3 years ago
Copr build
success (100%)
#1646844
3 years ago
Copr build
success (100%)
#1646843
3 years ago
Copr build
failure
#1646842
3 years ago
Copr build
success (100%)
#1646841
3 years ago
jenkins
success (100%)
Build #461 successful (commit: 3dc982cb)
3 years ago
Copr build
failure
#1646840
3 years ago
Copr build
success (100%)
#1646839
3 years ago
Copr build
success (100%)
#1646838
3 years ago
Copr build
success (100%)
#1646837
3 years ago
Copr build
failure
#1646836
3 years ago
Copr build
success (100%)
#1646835
3 years ago
jenkins
success (100%)
Build #459 successful (commit: 8652028c)
3 years ago
Copr build
success (100%)
#1646420
3 years ago
Copr build
success (100%)
#1646419
3 years ago
Copr build
success (100%)
#1646418
3 years ago
Copr build
pending (50%)
#1646417
3 years ago
Copr build
failure
#1646416
3 years ago
Copr build
failure
#1646415
3 years ago
jenkins
success (100%)
Build #458 successful (commit: 92561124)
3 years ago
Copr build
success (100%)
#1646404
3 years ago
Copr build
success (100%)
#1646403
3 years ago
Copr build
success (100%)
#1646402
3 years ago
Copr build
failure
#1646401
3 years ago
Copr build
failure
#1646400
3 years ago
Copr build
success (100%)
#1646399
3 years ago
jenkins
success (100%)
Build #457 successful (commit: 8c808e16)
3 years ago
Copr build
success (100%)
#1646386
3 years ago
Copr build
success (100%)
#1646385
3 years ago
Copr build
success (100%)
#1646384
3 years ago
Copr build
pending (50%)
#1646383
3 years ago
Copr build
failure
#1646382
3 years ago
Copr build
success (100%)
#1646381
3 years ago
jenkins
success (100%)
Build #451 successful (commit: 37546d00)
3 years ago
Copr build
success (100%)
#1644877
3 years ago
Copr build
success (100%)
#1644876
3 years ago
Copr build
success (100%)
#1644875
3 years ago
Copr build
failure
#1644874
3 years ago
Copr build
failure
#1644873
3 years ago
Copr build
success (100%)
#1644872
3 years ago
jenkins
success (100%)
Build #450 successful (commit: 050f9715)
3 years ago
Copr build
success (100%)
#1644861
3 years ago
Copr build
success (100%)
#1644860
3 years ago
Copr build
success (100%)
#1644859
3 years ago
Copr build
success (100%)
#1644858
3 years ago
Copr build
failure
#1644857
3 years ago
Copr build
failure
#1644856
3 years ago
jenkins
success (100%)
Build #448 successful (commit: 5a72acc0)
3 years ago
Copr build
success (100%)
#1644668
3 years ago
Copr build
success (100%)
#1644667
3 years ago
Copr build
success (100%)
#1644666
3 years ago
Copr build
success (100%)
#1644665
3 years ago
Copr build
failure
#1644664
3 years ago
Copr build
success (100%)
#1644663
3 years ago
jenkins
success (100%)
Build #445 successful (commit: 16948b7a)
3 years ago
jenkins
success (100%)
Build #441 successful (commit: 16948b7a)
3 years ago
jenkins
success (100%)
Build #440 successful (commit: 16948b7a)
3 years ago
jenkins
failure
Build #439 failed (commit: 16948b7a)
3 years ago
jenkins
failure
Build #439 failed (commit: 16948b7a)
3 years ago
jenkins
success (100%)
Build #438 successful (commit: 16948b7a)
3 years ago
jenkins
success (100%)
Build #437 successful (commit: 16948b7a)
3 years ago
Copr build
success (100%)
#1644363
3 years ago
Copr build
success (100%)
#1644362
3 years ago
Copr build
success (100%)
#1644361
3 years ago
Copr build
success (100%)
#1644360
3 years ago
Copr build
failure
#1644359
3 years ago
Copr build
success (100%)
#1644358
3 years ago
jenkins
failure
Build #436 failed (commit: a4307499)
3 years ago
Copr build
success (100%)
#1644351
3 years ago
Copr build
success (100%)
#1644350
3 years ago
Copr build
success (100%)
#1644349
3 years ago
Copr build
success (100%)
#1644348
3 years ago
Copr build
failure
#1644347
3 years ago
Copr build
success (100%)
#1644346
3 years ago
jenkins
failure
Build #435 failed (commit: fd88af3e)
3 years ago
Copr build
success (100%)
#1644343
3 years ago
Copr build
success (100%)
#1644342
3 years ago
Copr build
pending (50%)
#1644341
3 years ago
Copr build
success (100%)
#1644340
3 years ago
Copr build
failure
#1644339
3 years ago
Copr build
success (100%)
#1644338
3 years ago
jenkins
failure
Build #434 failed (commit: a21dd7cb)
3 years ago
Copr build
success (100%)
#1644335
3 years ago
Copr build
success (100%)
#1644334
3 years ago
Copr build
success (100%)
#1644333
3 years ago
Copr build
success (100%)
#1644332
3 years ago
Copr build
failure
#1644331
3 years ago
Copr build
success (100%)
#1644330
3 years ago
jenkins
failure
Build #432 failed (commit: d13bb873)
3 years ago
Copr build
success (100%)
#1644204
3 years ago
Copr build
success (100%)
#1644203
3 years ago
Copr build
success (100%)
#1644202
3 years ago
Copr build
success (100%)
#1644201
3 years ago
Copr build
failure
#1644200
3 years ago
Copr build
success (100%)
#1644199
3 years ago
jenkins
failure
Build #429 failed (commit: e22b3965)
3 years ago
Copr build
failure
#1643821
3 years ago
Copr build
success (100%)
#1643820
3 years ago
Copr build
success (100%)
#1643819
3 years ago
Copr build
success (100%)
#1643818
3 years ago
Copr build
failure
#1643817
3 years ago
Copr build
success (100%)
#1643816
3 years ago
jenkins
failure
Build #428 failed (commit: 375b50d6)
3 years ago
Copr build
failure
#1643798
3 years ago
Copr build
success (100%)
#1643797
3 years ago
Copr build
success (100%)
#1643796
3 years ago
Copr build
pending (50%)
#1643795
3 years ago
Copr build
failure
#1643794
3 years ago
Copr build
failure
#1643793
3 years ago
jenkins
success (100%)
Build #420 successful (commit: ca720a23)
3 years ago
Copr build
success (100%)
#1637120
3 years ago
Copr build
failure
#1637119
3 years ago
Copr build
failure
#1637118
3 years ago
jenkins
success (100%)
Build #419 successful (commit: 7f269cb1)
3 years ago
Copr build
success (100%)
#1637112
3 years ago
Copr build
failure
#1637111
3 years ago
Copr build
pending (50%)
#1637110
3 years ago
jenkins
failure
Build #418 failed (commit: 8a79ad2c)
3 years ago
Copr build
success (100%)
#1635812
3 years ago
jenkins
failure
Build #416 failed (commit: eaf4b627)
3 years ago
Copr build
success (100%)
#1635314
3 years ago
jenkins
failure
Build #411 failed (commit: b2ea9959)
3 years ago
Copr build
success (100%)
#1620377
3 years ago
Changes Summary 66
+4 -0
file changed
.pylintpath/pylint_copr_plugin.py
+1 -1
file changed
backend/copr_backend/background_worker_build.py
+1 -1
file changed
beaker-tests/DockerTestEnv/Dockerfile
+1 -0
file changed
beaker-tests/README
+25
file added
beaker-tests/Sanity/copr-cli-basic-operations/runtest-distgit.sh
+11 -9
file changed
beaker-tests/Sanity/copr-cli-basic-operations/runtest.sh
+1
file added
behave/.gitignore
+0
file added
behave/__init__.py
+144
file added
behave/copr_behave_lib.py
+22
file added
behave/features/build_from_distgit.feature
+52
file added
behave/features/environment.py
+53
file added
behave/features/steps/builds.py
+47
file added
behave/features/steps/package.py
+30
file added
behave/features/steps/project.py
+1 -0
file changed
build_aux/jenkins-job
+16 -9
file changed
build_aux/linter
+6 -4
file changed
cli/copr-cli.spec
+106 -0
file changed
cli/copr_cli/main.py
+156
file added
cli/tests/test_distgit.py
+11 -3
file changed
common/copr_common/enums.py
+1 -1
file changed
common/python-copr-common.spec
+1 -0
file changed
frontend/coprs_frontend/coprs/filters.py
+143 -32
file changed
frontend/coprs_frontend/coprs/forms.py
+9 -20
file changed
frontend/coprs_frontend/coprs/helpers.py
+20 -1
file changed
frontend/coprs_frontend/coprs/logic/builds_logic.py
+9 -0
file changed
frontend/coprs_frontend/coprs/logic/dist_git_logic.py
+17 -4
file changed
frontend/coprs_frontend/coprs/models.py
+2 -2
file changed
frontend/coprs_frontend/coprs/templates/_helpers.html
+16 -0
file changed
frontend/coprs_frontend/coprs/templates/coprs/detail/_builds_forms.html
+10 -0
file changed
frontend/coprs_frontend/coprs/templates/coprs/detail/_describe_source.html
+23 -0
file changed
frontend/coprs_frontend/coprs/templates/coprs/detail/_package_forms.html
+5 -0
file changed
frontend/coprs_frontend/coprs/templates/coprs/detail/_package_helpers.html
+1 -0
file changed
frontend/coprs_frontend/coprs/templates/coprs/detail/add_build.html
+11
file added
frontend/coprs_frontend/coprs/templates/coprs/detail/add_build/distgit.html
+24 -0
file changed
frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_builds.py
+14 -5
file changed
frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_packages.py
+2 -8
file changed
frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py
+46 -2
file changed
frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py
+21 -4
file changed
frontend/coprs_frontend/coprs/views/coprs_ns/coprs_packages.py
+5 -2
file changed
frontend/coprs_frontend/pagure_events.py
+10 -1
file changed
frontend/coprs_frontend/tests/coprs_test_case.py
+157 -2
file changed
frontend/coprs_frontend/tests/test_apiv3/test_builds.py
+75
file added
frontend/coprs_frontend/tests/test_apiv3/test_packages.py
+75
file added
frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_method_distgit.py
+2 -1
file changed
pylintrc
+20 -1
file changed
python/copr/test/client_v3/test_builds.py
+30
file added
python/copr/test/client_v3/test_packages.py
+32 -0
file changed
python/copr/v3/proxies/build.py
+1 -1
file changed
python/python-copr.spec
+5 -1
file changed
python/run_tests.sh
+10
file added
rpmbuild/bin/copr-distgit-client
+22
file added
rpmbuild/copr-distgit-client
+58 -7
file changed
rpmbuild/copr-rpmbuild.spec
+335
file added
rpmbuild/copr_distgit_client.py
+44 -11
file changed
rpmbuild/copr_rpmbuild/helpers.py
+10 -7
file changed
rpmbuild/copr_rpmbuild/providers/__init__.py
+8 -1
file changed
rpmbuild/copr_rpmbuild/providers/base.py
+39
file added
rpmbuild/copr_rpmbuild/providers/distgit.py
+13 -40
file changed
rpmbuild/copr_rpmbuild/providers/scm.py
+25
file added
rpmbuild/etc/copr-distgit-client/default.ini
+8 -12
file changed
rpmbuild/main.py
+16 -2
file changed
rpmbuild/run_tests.sh
+88
file added
rpmbuild/tests/test_distgit.py
+173
file added
rpmbuild/tests/test_distgit_client.py
+4 -2
file changed
rpmbuild/tests/test_main.py
+5 -5
file changed
rpmbuild/tests/test_providers.py