Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package osc for openSUSE:Factory checked in 
at 2023-08-10 15:34:24
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/osc (Old)
 and      /work/SRC/openSUSE:Factory/.osc.new.11712 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "osc"

Thu Aug 10 15:34:24 2023 rev:180 rq:1103223 version:1.3.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/osc/osc.changes  2023-07-15 23:15:24.723603895 
+0200
+++ /work/SRC/openSUSE:Factory/.osc.new.11712/osc.changes       2023-08-10 
15:34:37.404434297 +0200
@@ -1,0 +2,25 @@
+Wed Aug  9 11:36:47 UTC 2023 - Daniel Mach <[email protected]>
+
+- 1.3.0
+  - Command-line:
+    - Add experimental support of Git SCM to the 'build' command
+    - Add experimental support of Git SCM to the 'service' command
+    - Make 'meta' command capable of editing attributes
+    - Change '--add' option in 'meta attribute' command to skip duplicate 
values
+    - Add an interactive option to display build log in 'request list -i' 
command
+    - Add '--setopt' option for setting config options from the command-line
+    - Fix '--prefer-pkgs' option for noinstall="1" packages in kiwi builds
+    - Change 'checkout' command to print open requests only when running in an 
interactive terminal
+    - Enhance '--force' option description in the 'request' command
+  - Connection:
+    - Fix crash when HTTP_PROXY env contains no auth
+  - Library:
+    - Add 'git_scm' module for handling packages that live in git scm rather 
than usual obs scm
+    - Change pop_project_package_from_args() to use get_store() to support Git 
SCM
+    - Change osc.build module to use 'store' object instead of calling 
core.store_*() functions
+    - Use alternative project if specified in parse_repoarchdescr()
+    - Fix xml indent() on Python 3.6
+    - Fix less pager by adding '-R' to LESS env
+    - Improve print_msg() and migrate some arbitrary prints to it
+
+-------------------------------------------------------------------

Old:
----
  osc-1.2.0.tar.gz

New:
----
  osc-1.3.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ osc.spec ++++++
--- /var/tmp/diff_new_pack.nqBlCg/_old  2023-08-10 15:34:38.336440110 +0200
+++ /var/tmp/diff_new_pack.nqBlCg/_new  2023-08-10 15:34:38.340440135 +0200
@@ -49,7 +49,7 @@
 %endif
 
 Name:           osc
-Version:        1.2.0
+Version:        1.3.0
 Release:        0
 Summary:        Command-line client for the Open Build Service
 License:        GPL-2.0-or-later
@@ -75,6 +75,8 @@
 BuildRequires:  %{use_python_pkg}-setuptools
 BuildRequires:  %{use_python_pkg}-urllib3
 BuildRequires:  diffstat
+# needed for git scm tests
+BuildRequires:  git-core
 
 Requires:       %{use_python_pkg}-cryptography
 Requires:       %{use_python_pkg}-rpm
@@ -96,6 +98,10 @@
 Recommends:     powerpc32
 Recommends:     sudo
 
+# needed for building from git
+Recommends:     git-core
+Recommends:     git-lfs
+
 # needed for `osc add <URL>`
 Recommends:     obs-service-recompress
 Recommends:     obs-service-download_files

++++++ PKGBUILD ++++++
--- /var/tmp/diff_new_pack.nqBlCg/_old  2023-08-10 15:34:38.376440359 +0200
+++ /var/tmp/diff_new_pack.nqBlCg/_new  2023-08-10 15:34:38.380440384 +0200
@@ -1,5 +1,5 @@
 pkgname=osc
-pkgver=1.2.0
+pkgver=1.3.0
 pkgrel=0
 pkgdesc="Command-line client for the Open Build Service"
 arch=('x86_64')

++++++ debian.changelog ++++++
--- /var/tmp/diff_new_pack.nqBlCg/_old  2023-08-10 15:34:38.420440634 +0200
+++ /var/tmp/diff_new_pack.nqBlCg/_new  2023-08-10 15:34:38.424440659 +0200
@@ -1,4 +1,4 @@
-osc (1.2.0-0) unstable; urgency=low
+osc (1.3.0-0) unstable; urgency=low
 
   * Placeholder
 

++++++ osc-1.2.0.tar.gz -> osc-1.3.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/NEWS new/osc-1.3.0/NEWS
--- old/osc-1.2.0/NEWS  2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/NEWS  2023-08-09 13:34:16.000000000 +0200
@@ -1,3 +1,25 @@
+- 1.3.0
+  - Command-line:
+    - Add experimental support of Git SCM to the 'build' command
+    - Add experimental support of Git SCM to the 'service' command
+    - Make 'meta' command capable of editing attributes
+    - Change '--add' option in 'meta attribute' command to skip duplicate 
values
+    - Add an interactive option to display build log in 'request list -i' 
command
+    - Add '--setopt' option for setting config options from the command-line
+    - Fix '--prefer-pkgs' option for noinstall="1" packages in kiwi builds
+    - Change 'checkout' command to print open requests only when running in an 
interactive terminal
+    - Enhance '--force' option description in the 'request' command
+  - Connection:
+    - Fix crash when HTTP_PROXY env contains no auth
+  - Library:
+    - Add 'git_scm' module for handling packages that live in git scm rather 
than usual obs scm
+    - Change pop_project_package_from_args() to use get_store() to support Git 
SCM
+    - Change osc.build module to use 'store' object instead of calling 
core.store_*() functions
+    - Use alternative project if specified in parse_repoarchdescr()
+    - Fix xml indent() on Python 3.6
+    - Fix less pager by adding '-R' to LESS env
+    - Improve print_msg() and migrate some arbitrary prints to it
+
 - 1.2.0
   - Command-line:
     - Add 'repo' command and subcommands for managing repositories in project 
meta
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/__init__.py 
new/osc-1.3.0/osc/__init__.py
--- old/osc-1.2.0/osc/__init__.py       2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/osc/__init__.py       2023-08-09 13:34:16.000000000 +0200
@@ -13,7 +13,7 @@
 
 
 from .util import git_version
-__version__ = git_version.get_version('1.2.0')
+__version__ = git_version.get_version('1.3.0')
 
 
 # vim: sw=4 et
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/_private/common.py 
new/osc-1.3.0/osc/_private/common.py
--- old/osc-1.2.0/osc/_private/common.py        2023-07-14 11:08:24.000000000 
+0200
+++ new/osc-1.3.0/osc/_private/common.py        2023-08-09 13:34:16.000000000 
+0200
@@ -1,16 +1,22 @@
 import sys
 
 
-def print_msg(msg, print_to="debug"):
+def print_msg(*args, print_to="debug"):
     from .. import conf
 
     if print_to is None:
         return
     elif print_to == "debug":
+        # print a debug message to stderr if config["debug"] is set
         if conf.config["debug"]:
-            print(f"DEBUG: {msg}", file=sys.stderr)
+            print("DEBUG:", *args, file=sys.stderr)
+    elif print_to == "verbose":
+        # print a verbose message to stdout if config["verbose"] or 
config["debug"] is set
+        if conf.config["verbose"] or conf.config["debug"]:
+            print(*args)
     elif print_to == "stdout":
-        print(msg)
+        # print the message to stdout
+        print(*args)
     else:
         raise ValueError(f"Invalid value of the 'print_to' option: {print_to}")
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/_private/project.py 
new/osc-1.3.0/osc/_private/project.py
--- old/osc-1.2.0/osc/_private/project.py       2023-07-14 11:08:24.000000000 
+0200
+++ new/osc-1.3.0/osc/_private/project.py       2023-08-09 13:34:16.000000000 
+0200
@@ -10,7 +10,7 @@
         self.apiurl = apiurl
 
     def to_bytes(self):
-        ET.indent(self.root, space="  ", level=0)
+        api.xml_indent(self.root)
         return ET.tostring(self.root, encoding="utf-8")
 
     def to_string(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/babysitter.py 
new/osc-1.3.0/osc/babysitter.py
--- old/osc-1.2.0/osc/babysitter.py     2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/osc/babysitter.py     2023-08-09 13:34:16.000000000 +0200
@@ -116,9 +116,8 @@
         except AttributeError:
             body = ''
 
-        if osc_conf.config["debug"]:
-            print(e.hdrs, file=sys.stderr)
-            print(body, file=sys.stderr)
+        _private.print_msg(e.hdrs, print_to="debug")
+        _private.print_msg(body, print_to="debug")
 
         if e.code in [400, 403, 404, 500]:
             if b'<summary>' in body:
@@ -162,8 +161,7 @@
         print(e.message, file=sys.stderr)
     except oscerr.OscIOError as e:
         print(e.msg, file=sys.stderr)
-        if osc_conf.config["debug"]:
-            print(e.e, file=sys.stderr)
+        _private.print_msg(e.e, print_to="debug")
     except (oscerr.WrongOptions, oscerr.WrongArgs) as e:
         print(e, file=sys.stderr)
         return 2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/build.py new/osc-1.3.0/osc/build.py
--- old/osc-1.2.0/osc/build.py  2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/osc/build.py  2023-08-09 13:34:16.000000000 +0200
@@ -20,7 +20,7 @@
 from . import connection
 from . import core
 from . import oscerr
-from .core import get_buildinfo, store_read_project, store_read_package, 
meta_exists, quote_plus, get_buildconfig, is_package_dir, dgst
+from .core import get_buildinfo, meta_exists, quote_plus, get_buildconfig, dgst
 from .core import get_binarylist, get_binary_file, run_external, 
return_external, raw_input
 from .fetch import Fetcher, OscFileGrabber, verify_pacs
 from .meter import create_text_meter
@@ -595,12 +595,13 @@
     return kiwipath
 
 
-def calculate_prj_pac(opts, descr):
-    project = opts.alternative_project or store_read_project('.')
+def calculate_prj_pac(store, opts, descr):
+    project = opts.alternative_project or store.project
     if opts.local_package:
         package = os.path.splitext(os.path.basename(descr))[0]
     else:
-        package = store_read_package('.')
+        store.assert_is_package()
+        package = store.package
     return project, package
 
 
@@ -639,7 +640,7 @@
     return run_external(cmd[0], *cmd[1:])
 
 
-def main(apiurl, opts, argv):
+def main(apiurl, store, opts, argv):
 
     repo = argv[0]
     arch = argv[1]
@@ -768,11 +769,11 @@
         prj = opts.alternative_project
         pac = '_repository'
     else:
-        prj = store_read_project(os.curdir)
+        prj = store.project
         if opts.local_package:
             pac = '_repository'
         else:
-            pac = store_read_package(os.curdir)
+            pac = store.package
     if opts.multibuild_package:
         buildargs.append('--buildflavor=%s' % opts.multibuild_package)
         pac = pac + ":" + opts.multibuild_package
@@ -797,7 +798,7 @@
     if pacname == '_repository':
         if not opts.local_package:
             try:
-                pacname = store_read_package(os.curdir)
+                pacname = store.package
             except oscerr.NoWorkingCopy:
                 opts.local_package = True
         if opts.local_package:
@@ -834,7 +835,7 @@
     bc_file = None
     bi_filename = '_buildinfo-%s-%s.xml' % (repo, arch)
     bc_filename = '_buildconfig-%s-%s' % (repo, arch)
-    if is_package_dir('.') and os.access(core.store, os.W_OK):
+    if store.is_package and os.access(core.store, os.W_OK):
         bi_filename = os.path.join(os.getcwd(), core.store, bi_filename)
         bc_filename = os.path.join(os.getcwd(), core.store, bc_filename)
     elif not os.access('.', os.W_OK):
@@ -859,7 +860,7 @@
     if opts.noinit:
         buildargs.append('--noinit')
 
-    if not is_package_dir('.'):
+    if not store.is_package:
         opts.noservice = True
 
     # check for source services
@@ -1268,10 +1269,24 @@
                     if name == filename:
                         print("Using prefered package: " + path + "/" + 
filename)
                         os.unlink(tffn)
-                        if opts.linksources:
-                            os.link(path + "/" + filename, tffn)
-                        else:
-                            os.symlink(path + "/" + filename, tffn)
+
+        if prefer_pkgs:
+            localpkgdir = "repos/_local/"
+            os.mkdir(localpkgdir)
+            buildargs.append("--kiwi-parameter")
+            buildargs.append("--add-repo")
+            buildargs.append("--kiwi-parameter")
+            buildargs.append(f"dir://./{localpkgdir}")
+            buildargs.append("--kiwi-parameter")
+            buildargs.append("--add-repotype")
+            buildargs.append("--kiwi-parameter")
+            buildargs.append("rpm-md")
+            for name, path in prefer_pkgs.items():
+                tffn = os.path.join(localpkgdir, os.path.basename(path))
+                if opts.linksources:
+                    os.link(path, tffn)
+                else:
+                    os.symlink(path, tffn)
 
     if build_type == 'kiwi':
         # Is a obsrepositories tag used?
@@ -1481,8 +1496,8 @@
         cmd = [change_personality[bi.buildarch]] + cmd
 
     # record our settings for later builds
-    if is_package_dir(os.curdir):
-        core.store_write_last_buildroot(os.curdir, repo, arch, vm_type)
+    if store.is_package:
+        store.last_buildroot = repo, arch, vm_type
 
     try:
         rc = run_external(cmd[0], *cmd[1:])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/commandline.py 
new/osc-1.3.0/osc/commandline.py
--- old/osc-1.2.0/osc/commandline.py    2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/osc/commandline.py    2023-08-09 13:34:16.000000000 +0200
@@ -30,6 +30,7 @@
 from . import cmdln
 from . import commands as osc_commands
 from . import conf
+from . import git_scm
 from . import oscerr
 from . import store as osc_store
 from .core import *
@@ -384,6 +385,13 @@
             help="specify alternate configuration file",
         )
         self.add_argument(
+            "--setopt",
+            metavar="KEY=VALUE",
+            action="append",
+            default=[],
+            help="set a config option for the current program run",
+        )
+        self.add_argument(
             "--no-keyring",
             action="store_true",
             help="disable usage of desktop keyring system",
@@ -401,6 +409,11 @@
                 # let's leave setting the right value to conf.get_config()
                 pass
 
+        overrides = {}
+        for i in args.setopt:
+            key, value = i.split("=")
+            overrides[key] = value
+
         try:
             conf.get_config(
                 override_apiurl=args.apiurl,
@@ -412,6 +425,7 @@
                 override_post_mortem=args.post_mortem,
                 override_traceback=args.traceback,
                 override_verbose=args.verbose,
+                overrides=overrides,
             )
         except oscerr.NoConfigfile as e:
             print(e.msg, file=sys.stderr)
@@ -675,9 +689,8 @@
 
     if project == ".":
         # project name taken from the working copy
-        project_store = osc_store.Store(path)
         try:
-            project_store = osc_store.Store(path)
+            project_store = osc_store.get_store(path)
             project = project_store.project
         except oscerr.NoWorkingCopy:
             if not project_is_optional:
@@ -687,7 +700,7 @@
     if package == ".":
         # package name taken from the working copy
         try:
-            package_store = osc_store.Store(path)
+            package_store = osc_store.get_store(path)
             package_store.assert_is_package()
             package = package_store.package
         except oscerr.NoWorkingCopy:
@@ -1741,7 +1754,7 @@
     @cmdln.option('-s', '--set', metavar='ATTRIBUTE_VALUES',
                         help='set attribute values')
     @cmdln.option('--add', metavar='ATTRIBUTE_VALUES',
-                        help='add to the existing attribute values')
+                        help='add to the existing attribute values, skip 
duplicates')
     @cmdln.option('--delete', action='store_true',
                   help='delete a pattern or attribute')
     def do_meta(self, subcmd, opts, *args):
@@ -1816,7 +1829,16 @@
         if opts.add and opts.set:
             self.argparse_error("Options --add and --set are mutually 
exclusive")
 
+        if cmd == "attribute" and opts.edit and not opts.attribute:
+            self.argparse_error("Please specify --attribute")
+
         apiurl = self.get_api_url()
+        project = None
+        package = None
+        subpackage = None
+        user = None
+        group = None
+        pattern = None
 
         # Specific arguments
         #
@@ -1953,6 +1975,16 @@
                           path_args=(project, pattern),
                           apiurl=apiurl,
                           template_args=None)
+            elif cmd == 'attribute':
+                edit_meta(
+                    metatype='attribute',
+                    edit=True,
+                    path_args=(quote_plus(project), 
quote_plus(opts.attribute)),
+                    apiurl=apiurl,
+                    # PUT is not supported
+                    method="POST",
+                    template_args=None,
+                )
 
         # create attribute entry
         if (opts.create or opts.set or opts.add) and cmd == 'attribute':
@@ -1963,7 +1995,7 @@
             if len(aname) != 2:
                 raise oscerr.WrongOptions('Given attribute is not in 
"NAMESPACE:NAME" style')
 
-            values = ''
+            values = []
 
             if opts.add:
                 # read the existing values from server
@@ -1971,8 +2003,7 @@
                 nodes = _private.api.find_nodes(root, "attributes", 
"attribute", {"namespace": aname[0], "name": aname[1]}, "value")
                 for node in nodes:
                     # append the existing values
-                    value = _private.api.xml_escape(node.text)
-                    values += f"<value>{value}</value>"
+                    values.append(node.text)
 
                 # pretend we're setting values in order to append the values 
we have specified on the command-line,
                 # because OBS API doesn't support extending the value list 
directly
@@ -1980,9 +2011,20 @@
 
             if opts.set:
                 for i in opts.set.split(','):
-                    values += '<value>%s</value>' % _private.api.xml_escape(i)
+                    # append the new values
+                    # we skip duplicates during --add
+                    if opts.add and i in values:
+                        continue
+                    values.append(i)
 
-            d = '<attributes><attribute namespace=\'%s\' name=\'%s\' 
>%s</attribute></attributes>' % (aname[0], aname[1], values)
+            values_str = ""
+            for value in values:
+                value = _private.api.xml_escape(value)
+                values_str += f"<value>{value}</value>"
+
+            ns = _private.api.xml_escape(aname[0])
+            name = _private.api.xml_escape(aname[1])
+            d = f"<attributes><attribute namespace='{ns}' name='{name}' 
>{values_str}</attribute></attributes>"
             url = makeurl(apiurl, attributepath)
             for data in streamfile(url, http_POST, data=d):
                 sys.stdout.buffer.write(data)
@@ -2976,7 +3018,7 @@
     @cmdln.option('-a', '--all', action='store_true',
                         help='all states. Same as\'-s all\'')
     @cmdln.option('-f', '--force', action='store_true',
-                        help='enforce state change, can be used to ignore open 
reviews')
+                        help='enforce state change, can be used to ignore open 
reviews, devel-package dependencies and more')
     @cmdln.option('-s', '--state',
                         help='only list requests in one of the comma separated 
given states (new/review/accepted/revoked/declined) or "all" 
[default="new,review,declined"]')
     @cmdln.option('-D', '--days', metavar='DAYS',
@@ -4254,8 +4296,7 @@
                 except:
                     print('Error while checkout package:\n', package, 
file=sys.stderr)
 
-            if conf.config['verbose']:
-                print('Note: You can use "osc delete" or "osc submitpac" when 
done.\n')
+            _private.print_msg('Note: You can use "osc delete" or "osc 
submitpac" when done.\n', print_to="verbose")
 
     @cmdln.alias('branchco')
     @cmdln.alias('bco')
@@ -4406,8 +4447,7 @@
         if opts.checkout:
             checkout_package(apiurl, targetprj, package, 
server_service_files=False,
                              expand_link=True, prj_dir=Path(targetprj))
-            if conf.config['verbose']:
-                print('Note: You can use "osc delete" or "osc submitpac" when 
done.\n')
+            _private.print_msg('Note: You can use "osc delete" or "osc 
submitpac" when done.\n', print_to="verbose")
         else:
             apiopt = ''
             if conf.get_configParser().get('general', 'apiurl') != apiurl:
@@ -5257,7 +5297,8 @@
                                  
server_service_files=opts.server_side_source_service_files,
                                  progress_obj=self.download_progress, 
size_limit=opts.limit_size,
                                  meta=opts.meta, outdir=opts.output_dir)
-                print_request_list(apiurl, project, package)
+                if os.isatty(sys.stdout.fileno()):
+                    print_request_list(apiurl, project, package)
 
         elif project:
             sep = '/' if not opts.output_dir and 
conf.config['checkout_no_colon'] else conf.config['project_separator']
@@ -5318,7 +5359,8 @@
                                      
server_service_files=opts.server_side_source_service_files,
                                      progress_obj=self.download_progress, 
size_limit=opts.limit_size,
                                      meta=opts.meta)
-            print_request_list(apiurl, project)
+            if os.isatty(sys.stdout.fileno()):
+                print_request_list(apiurl, project)
 
         else:
             self.argparse_error("Incorrect number of arguments.")
@@ -6923,7 +6965,7 @@
                 if no_repo:
                     raise oscerr.WrongArgs("Repository is missing. Cannot 
guess build description without repository")
                 apiurl = self.get_api_url()
-                project = store_read_project('.')
+                project = alternative_project or store_read_project('.')
                 # some distros like Debian rename and move build to obs-build
                 if not os.path.isfile('/usr/lib/build/queryconfig') and 
os.path.isfile('/usr/lib/obs-build/queryconfig'):
                     queryconfig = '/usr/lib/obs-build/queryconfig'
@@ -7187,25 +7229,22 @@
         if len(args) > 3:
             raise oscerr.WrongArgs('Too many arguments')
 
-        project = None
-        try:
-            project = store_read_project(Path.cwd())
-            if project == opts.alternative_project:
-                opts.alternative_project = None
-        except oscerr.NoWorkingCopy:
-            # This may be a project managed entirely via git?
-            if os.path.isdir(Path.cwd().parent / '.osc') and 
os.path.isdir(Path.cwd().parent / '.git'):
-                project = store_read_project(Path.cwd())
-                opts.alternative_project = project
-            pass
+        store = osc_store.get_store(Path.cwd(), print_warnings=True)
+        store.assert_is_package()
+
+        if opts.alternative_project == store.project:
+            opts.alternative_project = None
 
-        if len(args) == 0 and is_package_dir(Path.cwd()):
+        # HACK: avoid calling some underlying store_*() functions from 
parse_repoarchdescr() method
+        # We'll fix parse_repoarchdescr() later because it requires a larger 
change
+        if not opts.alternative_project and isinstance(store, 
git_scm.GitStore):
+            opts.alternative_project = store.project
+
+        if len(args) == 0 and store.is_package and store.last_buildroot:
             # build env not specified, just read from last build attempt
-            lastbuildroot = store_read_last_buildroot(Path.cwd())
-            if lastbuildroot:
-                args = [lastbuildroot[0], lastbuildroot[1]]
-                if not opts.vm_type:
-                    opts.vm_type = lastbuildroot[2]
+            args = [store.last_buildroot[0], store.last_buildroot[1]]
+            if not opts.vm_type:
+                opts.vm_type = store.last_buildroot[2]
 
         vm_chroot = opts.vm_type or conf.config['build-type']
         if (subcmd in ('shell', 'chroot') or opts.shell or opts.wipe) and not 
vm_chroot:
@@ -7214,7 +7253,7 @@
             else:
                 args = self.parse_repoarchdescr(args, opts.noinit or 
opts.offline, opts.alternative_project, False, opts.vm_type, 
opts.multibuild_package)
                 repo, arch, build_descr = args
-                prj, pac = osc_build.calculate_prj_pac(opts, build_descr)
+                prj, pac = osc_build.calculate_prj_pac(store, opts, 
build_descr)
                 apihost = urlsplit(self.get_api_url())[1]
                 build_root = osc_build.calculate_build_root(apihost, prj, pac, 
repo,
                                                             arch)
@@ -7238,12 +7277,12 @@
 
         if not opts.local_package:
             try:
-                package = store_read_package(Path.cwd())
                 prj = Project(os.pardir, getPackageList=False, wc_check=False)
-                if prj.status(package) == 'A':
+                if prj.status(store.package) == "A":
                     # a package with state 'A' most likely does not exist on
                     # the server - hence, treat it as a local package
                     opts.local_package = True
+                    print("INFO: Building the package as a local package.", 
file=sys.stderr)
             except oscerr.NoWorkingCopy:
                 pass
 
@@ -7272,7 +7311,7 @@
 
         print('Building %s for %s/%s' % (args[2], args[0], args[1]))
         if not opts.host:
-            return osc_build.main(self.get_api_url(), opts, args)
+            return osc_build.main(self.get_api_url(), store, opts, args)
         else:
             return self._do_rbuild(subcmd, opts, *args)
 
@@ -7583,36 +7622,33 @@
         """
         # disabledrun and localrun exists as well, but are considered to be 
obsolete
 
-        args = slash_split(args)
-        project = package = singleservice = mode = None
+        args = list(args)
         apiurl = self.get_api_url()
-        remote_commands = ('remoterun', 'rr', 'merge', 'wait')
+        project = None
+        package = None
+        singleservice = None
+        mode = None
+        remote_commands = ("remoterun", "rr", "merge", "wait")
+        obsolete_commands = ("localrun", "lr", "disabledrun", "dr")
 
         if len(args) < 1:
-            raise oscerr.WrongArgs('No command given.')
-        elif len(args) < 3:
-            if args[0] in remote_commands:
-                if not is_package_dir(Path.cwd()):
-                    msg = ('Either specify the project and package or execute '
-                           'the command in a package working copy.')
-                    raise oscerr.WrongArgs(msg)
-                package = store_read_package(Path.cwd())
-                project = store_read_project(Path.cwd())
-            else:
-                # raise an appropriate exception if Path.cwd() is no package wc
-                store_read_package(Path.cwd())
-            if len(args) == 2:
-                singleservice = args[1]
-        elif len(args) == 3 and args[0] in remote_commands:
-            project = self._process_project_name(args[1])
-            package = args[2]
-        else:
-            raise oscerr.WrongArgs('Too many arguments.')
+            self.argparse_error("Please specify a command")
 
-        command = args[0]
+        command = args.pop(0)
+        if command not in ("runall", "ra", "run", "localrun", "manualrun", 
"disabledrun", "remoterun", "lr", "dr", "mr", "rr", "merge", "wait"):
+            self.argparse_error(f"Invalid command: {command}")
 
-        if command not in ('runall', 'ra', 'run', 'localrun', 'manualrun', 
'disabledrun', 'remoterun', 'lr', 'dr', 'mr', 'rr', 'merge', 'wait'):
-            raise oscerr.WrongArgs('Wrong command given.')
+        if command in obsolete_commands:
+            print(f"WARNING: Command '{command}' is obsolete", file=sys.stderr)
+
+        if len(args) == 1:
+            singleservice = args.pop(0)
+        elif len(args) in (0, 2) and command in remote_commands:
+            project, package = pop_project_package_from_args(
+                args, default_project=".", default_package=".", 
package_is_optional=False
+            )
+
+        ensure_no_remaining_args(args)
 
         if command in ('remoterun', 'rr'):
             print(runservice(apiurl, project, package))
@@ -7626,9 +7662,10 @@
             print(mergeservice(apiurl, project, package))
             return
 
+        store = osc_store.get_store(Path.cwd(), print_warnings=True)
+        store.assert_is_package()
+
         if command in ('runall', 'ra', 'run', 'localrun', 'manualrun', 
'disabledrun', 'lr', 'mr', 'dr', 'r'):
-            if not is_package_dir(Path.cwd()):
-                raise oscerr.WrongArgs('Local directory is no package')
             p = Package(".")
             if command  in ("localrun", "lr"):
                 mode = "local"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/conf.py new/osc-1.3.0/osc/conf.py
--- old/osc-1.2.0/osc/conf.py   2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/osc/conf.py   2023-08-09 13:34:16.000000000 +0200
@@ -232,11 +232,13 @@
     cp = OscConfigParser.OscConfigParser(config)
     cp.add_section("general")
 
-    typed_opts = ((_boolean_opts, cp.getboolean), (_integer_opts, cp.getint))
-    for opts, meth in typed_opts:
+    typed_opts = ((_boolean_opts, cp.getboolean, bool), (_integer_opts, 
cp.getint, int))
+    for opts, meth, typ in typed_opts:
         for opt in opts:
             if opt not in config:
                 continue
+            if isinstance(config[opt], typ):
+                continue
             try:
                 config[opt] = meth('general', opt)
             except ValueError as e:
@@ -750,7 +752,9 @@
                override_traceback=None,
                override_post_mortem=None,
                override_no_keyring=None,
-               override_verbose=None):
+               override_verbose=None,
+               overrides=None
+               ):
     """do the actual work (see module documentation)"""
     global config
 
@@ -786,6 +790,17 @@
         raise oscerr.ConfigError(msg, conffile)
 
     config = dict(cp.items('general', raw=1))
+
+    # if the overrides trigger an exception, the 'post_mortem' option
+    # must be set to the appropriate type otherwise the non-empty string gets 
evaluated as True
+    config = apply_option_types(config, conffile)
+
+    overrides = overrides or {}
+    for key, value in overrides.items():
+        if key not in config:
+            raise oscerr.ConfigError(f"Unknown config option '{key}'", 
"<command-line>")
+        config[key] = value
+
     config['conffile'] = conffile
 
     config = apply_option_types(config, conffile)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/connection.py 
new/osc-1.3.0/osc/connection.py
--- old/osc-1.2.0/osc/connection.py     2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/osc/connection.py     2023-08-09 13:34:16.000000000 +0200
@@ -21,6 +21,7 @@
 import urllib3.util
 
 from . import __version__
+from . import _private
 from . import conf
 from . import oscerr
 from . import oscssl
@@ -107,10 +108,11 @@
         user_agent=f"osc/{__version__}",
     )
 
-    proxy_basic_auth = urllib.parse.unquote(proxy_purl.auth)
-    proxy_basic_auth = proxy_basic_auth.encode("utf-8")
-    proxy_basic_auth = base64.b64encode(proxy_basic_auth).decode()
-    proxy_headers["Proxy-Authorization"] = f"Basic {proxy_basic_auth:s}"
+    if proxy_purl.auth:
+        proxy_basic_auth = urllib.parse.unquote(proxy_purl.auth)
+        proxy_basic_auth = proxy_basic_auth.encode("utf-8")
+        proxy_basic_auth = base64.b64encode(proxy_basic_auth).decode()
+        proxy_headers["Proxy-Authorization"] = f"Basic {proxy_basic_auth:s}"
 
     manager = urllib3.ProxyManager(proxy_url, proxy_headers=proxy_headers)
     return manager
@@ -685,9 +687,7 @@
             return False
 
         if not self.ssh_keygen_path:
-            if conf.config["debug"]:
-                msg = "Skipping signature auth because ssh-keygen is not 
available"
-                print(msg, file=sys.stderr)
+            _private.print_msg("Skipping signature auth because ssh-keygen is 
not available", print_to="debug")
             return False
 
         if not self.sshkey_known():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/core.py new/osc-1.3.0/osc/core.py
--- old/osc-1.2.0/osc/core.py   2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/osc/core.py   2023-08-09 13:34:16.000000000 +0200
@@ -18,6 +18,7 @@
 import fnmatch
 import glob
 import hashlib
+import io
 import locale
 import os
 import platform
@@ -49,6 +50,7 @@
 from . import conf
 from . import meter
 from . import oscerr
+from . import store as osc_store
 from .connection import http_request, http_GET, http_POST, http_PUT, 
http_DELETE
 from .store import Store
 from .util.helper import decode_list, decode_it, raw_input, _html_escape
@@ -516,8 +518,7 @@
                     raise oscerr.PackageNotInstalled("obs-service-%s" % cmd[0])
                 cmd[0] = "/usr/lib/obs/service/" + cmd[0]
                 cmd = cmd + ["--outdir", temp_dir]
-                if conf.config['verbose'] or verbose or conf.config['debug']:
-                    print("Run source service:", ' '.join(cmd))
+                _private.print_msg("Run source service:", " ".join(cmd), 
print_to="verbose")
                 r = run_external(*cmd)
 
                 if r != 0:
@@ -1239,7 +1240,8 @@
 
         self.dir = workingdir or "."
         self.absdir = os.path.abspath(self.dir)
-        self.store = Store(self.dir)
+        self.store = osc_store.get_store(self.dir)
+        self.store.assert_is_package()
         self.storedir = os.path.join(self.absdir, store)
         self.progress_obj = progress_obj
         self.size_limit = size_limit
@@ -1247,10 +1249,8 @@
         if size_limit and size_limit == 0:
             self.size_limit = None
 
-        check_store_version(self.dir)
-
-        self.prjname = store_read_project(self.dir)
-        self.name = store_read_package(self.dir)
+        self.prjname = self.store.project
+        self.name = self.store.package
         self.apiurl = self.store.apiurl
 
         self.update_datastructs()
@@ -3600,8 +3600,7 @@
     function. In case of a list not -- this is to be backwards compatible.
     """
     query = query or []
-    if conf.config['debug']:
-        print('makeurl:', baseurl, l, query)
+    _private.print_msg("makeurl:", baseurl, l, query, print_to="debug")
 
     if isinstance(query, list):
         query = '&'.join(query)
@@ -3929,7 +3928,7 @@
         def __call__(self, **kwargs):
             return self._delegate(**kwargs)
 
-    def __init__(self, url, input, change_is_required=False, file_ext='.xml'):
+    def __init__(self, url, input, change_is_required=False, file_ext='.xml', 
method=None):
         if isinstance(url, self._URLFactory):
             self._url_factory = url
         else:
@@ -3939,6 +3938,7 @@
         self.url = self._url_factory()
         self.change_is_required = change_is_required
         (fd, self.filename) = tempfile.mkstemp(prefix='osc_metafile.', 
suffix=file_ext)
+        self._method = method
 
         open_mode = 'w'
         input_as_str = None
@@ -3964,7 +3964,10 @@
         print('Sending meta data...')
         # don't do any exception handling... it's up to the caller what to do 
in case
         # of an exception
-        http_PUT(self.url, file=self.filename)
+        if self._method == "POST":
+            http_POST(self.url, file=self.filename)
+        else:
+            http_PUT(self.url, file=self.filename)
         os.unlink(self.filename)
         print('Done.')
 
@@ -4021,7 +4024,7 @@
                      'template': new_package_templ,
                      'file_ext': '.xml'
                      },
-             'attribute': {'path': 'source/%s/%s/_meta',
+             'attribute': {'path': 'source/%s/_attribute/%s',
                            'template': new_attribute_templ,
                            'file_ext': '.xml'
                            },
@@ -4117,6 +4120,7 @@
     remove_linking_repositories=False,
     change_is_required=False,
     apiurl: Optional[str] = None,
+    method: Optional[str] = None,
     msg=None,
 ):
 
@@ -4150,7 +4154,7 @@
         return make_meta_url(metatype, path_args, apiurl, force, 
remove_linking_repositories, msg)
 
     url_factory = metafile._URLFactory(delegate)
-    f = metafile(url_factory, data, change_is_required, 
metatypes[metatype]['file_ext'])
+    f = metafile(url_factory, data, change_is_required, 
metatypes[metatype]['file_ext'], method=method)
 
     if edit:
         f.edit()
@@ -4402,11 +4406,21 @@
         else:
             tmpfile.write(message)
         tmpfile.flush()
+
+        env = os.environ.copy()
+
         pager = os.getenv("PAGER", default="").strip()
         pager = pager or get_default_pager()
+
+        # LESS env is not always set and we need -R to display escape 
sequences properly
+        less_opts = os.getenv("LESS", default="")
+        if "-R" not in less_opts:
+            less_opts += " -R"
+        env["LESS"] = less_opts
+
         cmd = shlex.split(pager) + [tmpfile.name]
         try:
-            run_external(*cmd)
+            run_external(*cmd, env=env)
         finally:
             tmpfile.close()
 
@@ -4774,8 +4788,7 @@
             xpath_base = xpath_join(xpath_base, 
'action/source/@%(kind)s=\'%(val)s\'', op='or', inner=True)
         xpath = xpath_join(xpath, xpath_base % {'kind': kind, 'val': val}, 
op='and', nexpr_parentheses=True)
 
-    if conf.config['debug']:
-        print('[ %s ]' % xpath)
+    _private.print_msg(f"[ {xpath} ]", print_to="debug")
     res = search(apiurl, request=xpath)
     collection = res['request']
     requests = []
@@ -4916,8 +4929,7 @@
     if req_type:
         xpath += " and action/@type=\'%s\'" % req_type
 
-    if conf.config['debug']:
-        print('[ %s ]' % xpath)
+    _private.print_msg(f"[ {xpath} ]", print_to="debug")
 
     res = search(apiurl, request=xpath)
     collection = res['request']
@@ -5589,10 +5601,10 @@
         prj_dir = Path(str(prj_dir).replace(':', sep))
 
     root_dots = Path('.')
+    oldproj = None
     if conf.config['checkout_rooted']:
         if prj_dir.stem == '/':
-            if conf.config['verbose']:
-                print("checkout_rooted ignored for %s" % prj_dir)
+            _private.print_msg(f"checkout_rooted ignored for {prj_dir}", 
print_to="verbose")
             # ?? should we complain if not is_project_dir(prj_dir) ??
         else:
             # if we are inside a project or package dir, ascend to parent
@@ -5619,9 +5631,7 @@
                 root_dots = root_dots / ("../" * n)
 
     if str(root_dots) != '.':
-        if conf.config['verbose']:
-            print("%s is project dir of %s. Root found at %s" %
-                  (prj_dir, oldproj, os.path.abspath(root_dots)))
+        _private.print_msg(f"{prj_dir} is project dir of {oldproj}. Root found 
at {os.path.abspath(root_dots)}", print_to="verbose")
         prj_dir = root_dots / prj_dir
 
     if not pathname:
@@ -6936,13 +6946,16 @@
     strip_time=False,
     last=False,
     lastsucceeded=False,
+    output_buffer=None,
 ):
     """prints out the buildlog on stdout"""
 
+    output_buffer = output_buffer or sys.stdout.buffer
+
     def print_data(data, strip_time=False):
         if strip_time:
             data = buildlog_strip_time(data)
-        sys.stdout.buffer.write(data)
+        output_buffer.write(data)
 
     query = {'nostream': '1', 'start': '%s' % offset}
     if last:
@@ -8263,39 +8276,73 @@
             print('Try -f to force the state change', file=sys.stderr)
         return False
 
-    def safe_get_rpmlint_log(src_actions):
-        lintlogs = []
+    def get_repos(src_actions):
+        """
+        Translate src_actions to [{"proj": ..., "pkg": ..., "repo": ..., 
"arch": ...}]
+        """
+        result = []
         for action in src_actions:
-            print('Type %s:' % action.type)
             disabled = show_package_disabled_repos(apiurl, action.src_project, 
action.src_package)
             for repo in get_repos_of_project(apiurl, action.src_project):
-                if (disabled is None) or (repo.name not in [d['repo'] for d in 
disabled]):
-                    lintlog_entry = {
-                        'proj': action.src_project,
-                        'pkg': action.src_package,
-                        'repo': repo.name,
-                        'arch': repo.arch
+                if (disabled is None) or (repo.name not in [d["repo"] for d in 
disabled]):
+                    entry = {
+                        "proj": action.src_project,
+                        "pkg": action.src_package,
+                        "repo": repo.name,
+                        "arch": repo.arch
                     }
-                    lintlogs.append(lintlog_entry)
-                    print('(%i) %s/%s/%s/%s' % ((len(lintlogs) - 1), 
action.src_project, action.src_package, repo.name, repo.arch))
-        if not lintlogs:
-            print('No possible rpmlintlogs found')
-            return False
+                    result.append(entry)
+        return result
+
+    def select_repo(src_actions):
+        """
+        Prompt user to select a repo from a list.
+        """
+        repos = get_repos(src_actions)
+
+        for num, entry in enumerate(repos):
+            print(f"({num}) 
{entry['proj']}/{entry['pkg']}/{entry['repo']}/{entry['arch']}")
+
+        if not repos:
+            print('No repos')
+            return None
+
         while True:
             try:
-                lint_n = int(raw_input('Number of rpmlint log to examine (0 - 
%i): ' % (len(lintlogs) - 1)))
-                lintlogs[lint_n]
-                break
+                reply = raw_input(f"Number of repo to examine (0 - 
{len(repos)-1}): ").strip()
+                if not reply:
+                    return None
+                reply_num = int(reply)
+                return repos[reply_num]
             except (ValueError, IndexError):
-                print('Invalid rpmlintlog index. Please choose between 0 and 
%i' % (len(lintlogs) - 1))
+                print(f"Invalid index. Please choose between 0 and 
{len(repos)-1}")
+
+    def safe_get_rpmlint_log(src_actions):
+        repo = select_repo(src_actions)
+        if not repo:
+            return
         try:
-            print(decode_it(get_rpmlint_log(apiurl, **lintlogs[lint_n])))
+            run_pager(get_rpmlint_log(apiurl, **repo))
         except HTTPError as e:
             if e.code == 404:
-                print('No rpmlintlog for %s %s' % (lintlogs[lint_n]['repo'],
-                      lintlogs[lint_n]['arch']))
+                print(f"No rpmlint log for {repo['repo']}/{repo['arch']}")
             else:
-                raise e
+                raise
+
+    def get_build_log(src_actions):
+        repo = select_repo(src_actions)
+        if not repo:
+            return
+        try:
+            buffer = io.BytesIO()
+            print_buildlog(apiurl, repo["proj"], repo["pkg"], repo["repo"], 
repo["arch"], output_buffer=buffer)
+            buffer.seek(0)
+            run_pager(buffer.read())
+        except HTTPError as e:
+            if e.code == 404:
+                print(f"No build log for {repo['repo']}/{repo['arch']}")
+            else:
+                raise
 
     def print_request(request):
         print(request)
@@ -8344,10 +8391,10 @@
         # actions which have sources + buildresults
         src_actions = editable_actions + 
request.get_actions('maintenance_release')
         if editable_actions:
-            prompt = 
'd(i)ff/(a)ccept/(d)ecline/(r)evoke/(b)uildstatus/rpm(li)ntlog/c(l)one/(e)dit/co(m)ment/(s)kip/(c)ancel
 > '
+            prompt = 
'd(i)ff/(a)ccept/(d)ecline/(r)evoke/(b)uildstatus/(bl)buildlog/rpm(li)ntlog/c(l)one/(e)dit/co(m)ment/(s)kip/(c)ancel
 > '
         elif src_actions:
             # no edit for maintenance release requests
-            prompt = 
'd(i)ff/(a)ccept/(d)ecline/(r)evoke/(b)uildstatus/rpm(li)ntlog/c(l)one/co(m)ment/(s)kip/(c)ancel
 > '
+            prompt = 
'd(i)ff/(a)ccept/(d)ecline/(r)evoke/(b)uildstatus/(bl)buildlog/rpm(li)ntlog/c(l)one/co(m)ment/(s)kip/(c)ancel
 > '
         editprj = ''
         orequest = None
         if source_buildstatus and src_actions:
@@ -8406,6 +8453,8 @@
                 print_source_buildstatus(src_actions)
             elif repl == 'li' and src_actions:
                 safe_get_rpmlint_log(src_actions)
+            elif repl == 'bl' and src_actions:
+                get_build_log(src_actions)
             elif repl == 'e' and editable_actions:
                 # this is only for editable actions
                 if not editprj:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/git_scm/README.md 
new/osc-1.3.0/osc/git_scm/README.md
--- old/osc-1.2.0/osc/git_scm/README.md 1970-01-01 01:00:00.000000000 +0100
+++ new/osc-1.3.0/osc/git_scm/README.md 2023-08-09 13:34:16.000000000 +0200
@@ -0,0 +1,4 @@
+# Warning
+
+This module provides EXPERIMENTAL and UNSTABLE support for git scm such as 
https://src.opensuse.org/.
+The code may change or disappear without a prior notice!
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/git_scm/__init__.py 
new/osc-1.3.0/osc/git_scm/__init__.py
--- old/osc-1.2.0/osc/git_scm/__init__.py       1970-01-01 01:00:00.000000000 
+0100
+++ new/osc-1.3.0/osc/git_scm/__init__.py       2023-08-09 13:34:16.000000000 
+0200
@@ -0,0 +1,7 @@
+import sys
+
+from .store import GitStore
+
+
+def warn_experimental():
+    print("WARNING: Using EXPERIMENTAL support for git scm. The functionality 
may change or disappear without a prior notice!", file=sys.stderr)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/git_scm/store.py 
new/osc-1.3.0/osc/git_scm/store.py
--- old/osc-1.2.0/osc/git_scm/store.py  1970-01-01 01:00:00.000000000 +0100
+++ new/osc-1.3.0/osc/git_scm/store.py  2023-08-09 13:34:16.000000000 +0200
@@ -0,0 +1,151 @@
+import json
+import os
+import subprocess
+import urllib.parse
+from pathlib import Path
+
+from .. import conf as osc_conf
+from .. import oscerr
+
+
+class GitStore:
+
+    @classmethod
+    def is_project_dir(cls, path):
+        try:
+            store = cls(path)
+        except oscerr.NoWorkingCopy:
+            return False
+        return store.is_project
+
+    @classmethod
+    def is_package_dir(cls, path):
+        try:
+            store = cls(path)
+        except oscerr.NoWorkingCopy:
+            return False
+        return store.is_package
+
+    def __init__(self, path, check=True):
+        self.path = path
+        self.abspath = os.path.abspath(self.path)
+
+        # TODO: how to determine if the current git repo contains a project or 
a package?
+        self.is_project = False
+        self.is_package = os.path.exists(os.path.join(self.abspath, ".git"))
+
+        self._package = None
+        self._project = None
+
+        if check and not any([self.is_project, self.is_package]):
+            msg = f"Directory '{self.path}' is not a GIT working copy"
+            raise oscerr.NoWorkingCopy(msg)
+
+        # TODO: decide if we need explicit 'git lfs pull' or not
+        # self._run_git(["lfs", "pull"])
+
+    def assert_is_project(self):
+        if not self.is_project:
+            msg = f"Directory '{self.path}' is not a GIT working copy of a 
project"
+            raise oscerr.NoWorkingCopy(msg)
+
+    def assert_is_package(self):
+        if not self.is_package:
+            msg = f"Directory '{self.path}' is not a GIT working copy of a 
package"
+            raise oscerr.NoWorkingCopy(msg)
+
+    def _run_git(self, args):
+        return subprocess.check_output(["git"] + args, encoding="utf-8", 
cwd=self.abspath).strip()
+
+    @property
+    def apiurl(self):
+        # HACK: we're using the currently configured apiurl
+        return osc_conf.config["apiurl"]
+
+    @property
+    def project(self):
+        if self._project is None:
+            # get project from the branch name
+            branch = self._run_git(["branch", "--show-current"])
+
+            # HACK: replace hard-coded mapping with metadata from git or the 
build service
+            if branch == "factory":
+                self._project = "openSUSE:Factory"
+            else:
+                raise RuntimeError(f"Couldn't map git branch '{branch}' to a 
project")
+        return self._project
+
+    @project.setter
+    def project(self, value):
+        self._project = value
+
+    @property
+    def package(self):
+        if self._package is None:
+            origin = self._run_git(["remote", "get-url", "origin"])
+            self._package = Path(urllib.parse.urlsplit(origin).path).stem
+        return self._package
+
+    @package.setter
+    def package(self, value):
+        self._package = value
+
+    def _get_option(self, name):
+        try:
+            result = self._run_git(["config", "--local", "--get", 
f"osc.{name}"])
+        except subprocess.CalledProcessError:
+            result = None
+        return result
+
+    def _check_type(self, name, value, expected_type):
+        if not isinstance(value, expected_type):
+            raise TypeError(f"The option '{name}' should be 
{expected_type.__name__}, not {type(value).__name__}")
+
+    def _set_option(self, name, value):
+        self._run_git(["config", "--local", f"osc.{name}", value])
+
+    def _unset_option(self, name):
+        try:
+            self._run_git(["config", "--local", "--unset", f"osc.{name}"])
+        except subprocess.CalledProcessError:
+            pass
+
+    def _get_dict_option(self, name):
+        result = self._get_option(name)
+        if result is None:
+            return None
+        result = json.loads(result)
+        self._check_type(name, result, dict)
+        return result
+
+    def _set_dict_option(self, name, value):
+        if value is None:
+            self._unset_option(name)
+            return
+        self._check_type(name, value, dict)
+        value = json.dumps(value)
+        self._set_option(name, value)
+
+    @property
+    def last_buildroot(self):
+        self.assert_is_package()
+        result = self._get_dict_option("last-buildroot")
+        if result is not None:
+            result = (result["repo"], result["arch"], result["vm_type"])
+        return result
+
+    @last_buildroot.setter
+    def last_buildroot(self, value):
+        self.assert_is_package()
+        if len(value) != 3:
+            raise ValueError("A tuple with exactly 3 items is expected: (repo, 
arch, vm_type)")
+        value = {
+            "repo": value[0],
+            "arch": value[1],
+            "vm_type": value[2],
+        }
+        self._set_dict_option("last-buildroot", value)
+
+    @property
+    def scmurl(self):
+        return self._run_git(["remote", "get-url", "origin"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/store.py new/osc-1.3.0/osc/store.py
--- old/osc-1.2.0/osc/store.py  2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/osc/store.py  2023-08-09 13:34:16.000000000 +0200
@@ -10,7 +10,7 @@
 
 from . import oscerr
 from ._private import api
-
+from . import git_scm
 
 class Store:
     STORE_DIR = ".osc"
@@ -309,3 +309,22 @@
         else:
             root = self.read_xml_node("_meta", "project").getroot()
         return root
+
+
+def get_store(path, check=True, print_warnings=False):
+    """
+    Return a store object that wraps SCM in given `path`:
+     - Store for OBS SCM
+     - GitStore for Git SCM
+    """
+    try:
+        store = Store(path, check)
+    except oscerr.NoWorkingCopy as ex:
+        try:
+            store = git_scm.GitStore(path, check)
+            if print_warnings:
+                git_scm.warn_experimental()
+        except oscerr.NoWorkingCopy as ex_git:
+            # raise the original exception, do not inform that we've tried git 
working copy
+            raise ex from None
+    return store
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/osc/util/git_version.py 
new/osc-1.3.0/osc/util/git_version.py
--- old/osc-1.2.0/osc/util/git_version.py       2023-07-14 11:08:24.000000000 
+0200
+++ new/osc-1.3.0/osc/util/git_version.py       2023-08-09 13:34:16.000000000 
+0200
@@ -9,7 +9,7 @@
     """
     # the `version` variable contents get substituted during `git archive`
     # it requires adding this to .gitattributes: <path to this file> 
export-subst
-    version = "1.2.0"
+    version = "1.3.0"
     if version.startswith(("$", "%")):
         # "$": version hasn't been substituted during `git archive`
         # "%": "Format:" and "$" characters get removed from the version 
string (a GitHub bug?)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/setup.cfg new/osc-1.3.0/setup.cfg
--- old/osc-1.2.0/setup.cfg     2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/setup.cfg     2023-08-09 13:34:16.000000000 +0200
@@ -35,6 +35,7 @@
     osc
     osc._private
     osc.commands
+    osc.git_scm
     osc.output
     osc.util
 install_requires =
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/tests/common.py 
new/osc-1.3.0/tests/common.py
--- old/osc-1.2.0/tests/common.py       2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/tests/common.py       2023-08-09 13:34:16.000000000 +0200
@@ -220,6 +220,7 @@
             shutil.rmtree(self.tmpdir)
         except:
             pass
+        os.environ.pop("OSC_CONFIG", "")
         self.assertTrue(len(EXPECTED_REQUESTS) == 0)
 
     def _get_fixtures_dir(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/tests/test_conf.py 
new/osc-1.3.0/tests/test_conf.py
--- old/osc-1.2.0/tests/test_conf.py    2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/tests/test_conf.py    2023-08-09 13:34:16.000000000 +0200
@@ -1,9 +1,13 @@
 import importlib
+import os
 import unittest
 
 import osc.conf
 
 
+FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "conf_fixtures")
+
+
 class TestConf(unittest.TestCase):
     def setUp(self):
         # reset the global `config` in preparation for running the tests
@@ -13,6 +17,9 @@
         # reset the global `config` to avoid impacting tests from other classes
         importlib.reload(osc.conf)
 
+    def _get_fixtures_dir(self):
+        return FIXTURES_DIR
+
     def test_bool_opts_defaults(self):
         config = osc.conf.config
         for opt in osc.conf._boolean_opts:
@@ -28,7 +35,8 @@
             self.assertIsInstance(config[opt], int, msg=f"option: '{opt}'")
 
     def test_bool_opts(self):
-        osc.conf.get_config()
+        oscrc = os.path.join(self._get_fixtures_dir(), "oscrc")
+        osc.conf.get_config(override_conffile=oscrc, override_no_keyring=True)
         config = osc.conf.config
         for opt in osc.conf._boolean_opts:
             if opt not in config:
@@ -36,7 +44,8 @@
             self.assertIsInstance(config[opt], bool, msg=f"option: '{opt}'")
 
     def test_int_opts(self):
-        osc.conf.get_config()
+        oscrc = os.path.join(self._get_fixtures_dir(), "oscrc")
+        osc.conf.get_config(override_conffile=oscrc, override_no_keyring=True)
         config = osc.conf.config
         for opt in osc.conf._integer_opts:
             if opt not in config:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/tests/test_git_scm_store.py 
new/osc-1.3.0/tests/test_git_scm_store.py
--- old/osc-1.2.0/tests/test_git_scm_store.py   1970-01-01 01:00:00.000000000 
+0100
+++ new/osc-1.3.0/tests/test_git_scm_store.py   2023-08-09 13:34:16.000000000 
+0200
@@ -0,0 +1,49 @@
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+from osc.git_scm.store import GitStore
+
+
+class TestGitStore(unittest.TestCase):
+    def setUp(self):
+        self.tmpdir = tempfile.mkdtemp(prefix="osc_test")
+        os.chdir(self.tmpdir)
+        subprocess.check_output(["git", "init", "-b", "factory"])
+        subprocess.check_output(["git", "remote", "add", "origin", 
"https://example.com/packages/my-package.git";])
+
+    def tearDown(self):
+        try:
+            shutil.rmtree(self.tmpdir)
+        except OSError:
+            pass
+
+    def test_package(self):
+        store = GitStore(self.tmpdir)
+        self.assertEqual(store.package, "my-package")
+
+    def test_project(self):
+        store = GitStore(self.tmpdir)
+        self.assertEqual(store.project, "openSUSE:Factory")
+
+    def test_last_buildroot(self):
+        store = GitStore(self.tmpdir)
+        self.assertEqual(store.last_buildroot, None)
+        store.last_buildroot = ("repo", "arch", "vm_type")
+
+        store = GitStore(self.tmpdir)
+        self.assertEqual(store.last_buildroot, ("repo", "arch", "vm_type"))
+
+    def test_scmurl(self):
+        store = GitStore(self.tmpdir)
+        self.assertEqual(store.scmurl, 
"https://example.com/packages/my-package.git";)
+
+
+if not shutil.which("git"):
+    TestGitStore = unittest.skip("The 'git' executable is not 
available")(TestGitStore)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/tests/test_grabber.py 
new/osc-1.3.0/tests/test_grabber.py
--- old/osc-1.2.0/tests/test_grabber.py 2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/tests/test_grabber.py 2023-08-09 13:34:16.000000000 +0200
@@ -7,12 +7,16 @@
 import osc.grabber as osc_grabber
 
 
+FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "conf_fixtures")
+
+
 class TestMirrorGroup(unittest.TestCase):
     def setUp(self):
         self.tmpdir = tempfile.mkdtemp(prefix='osc_test')
         # reset the global `config` in preparation for running the tests
         importlib.reload(osc.conf)
-        osc.conf.get_config()
+        oscrc = os.path.join(self._get_fixtures_dir(), "oscrc")
+        osc.conf.get_config(override_conffile=oscrc, override_no_keyring=True)
 
     def tearDown(self):
         # reset the global `config` to avoid impacting tests from other classes
@@ -22,6 +26,9 @@
         except:
             pass
 
+    def _get_fixtures_dir(self):
+        return FIXTURES_DIR
+
     def test_invalid_scheme(self):
         gr = osc_grabber.OscFileGrabber()
         mg = osc_grabber.OscMirrorGroup(gr, ["container://example.com"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.2.0/tests/test_output.py 
new/osc-1.3.0/tests/test_output.py
--- old/osc-1.2.0/tests/test_output.py  2023-07-14 11:08:24.000000000 +0200
+++ new/osc-1.3.0/tests/test_output.py  2023-08-09 13:34:16.000000000 +0200
@@ -1,5 +1,10 @@
+import contextlib
+import importlib
+import io
 import unittest
 
+import osc.conf
+from osc._private import print_msg
 from osc.output import KeyValueTable
 
 
@@ -67,5 +72,74 @@
         self.assertEqual(str(t), expected)
 
 
+class TestPrintMsg(unittest.TestCase):
+    def setUp(self):
+        # reset the global `config` in preparation for running the tests
+        importlib.reload(osc.conf)
+
+    def tearDown(self):
+        # reset the global `config` to avoid impacting tests from other classes
+        importlib.reload(osc.conf)
+
+    def test_debug(self):
+        osc.conf.config["debug"] = 0
+        stdout = io.StringIO()
+        stderr = io.StringIO()
+        with contextlib.redirect_stdout(stdout), 
contextlib.redirect_stderr(stderr):
+            print_msg("foo", "bar", print_to="debug")
+        self.assertEqual("", stdout.getvalue())
+        self.assertEqual("", stderr.getvalue())
+
+        osc.conf.config["debug"] = 1
+        stdout = io.StringIO()
+        stderr = io.StringIO()
+        with contextlib.redirect_stdout(stdout), 
contextlib.redirect_stderr(stderr):
+            print_msg("foo", "bar", print_to="debug")
+        self.assertEqual("", stdout.getvalue())
+        self.assertEqual("DEBUG: foo bar\n", stderr.getvalue())
+
+    def test_verbose(self):
+        osc.conf.config["verbose"] = 0
+        stdout = io.StringIO()
+        stderr = io.StringIO()
+        with contextlib.redirect_stdout(stdout), 
contextlib.redirect_stderr(stderr):
+            print_msg("foo", "bar", print_to="verbose")
+        self.assertEqual("", stdout.getvalue())
+        self.assertEqual("", stderr.getvalue())
+
+        osc.conf.config["verbose"] = 1
+        stdout = io.StringIO()
+        stderr = io.StringIO()
+        with contextlib.redirect_stdout(stdout), 
contextlib.redirect_stderr(stderr):
+            print_msg("foo", "bar", print_to="verbose")
+        self.assertEqual("foo bar\n", stdout.getvalue())
+        self.assertEqual("", stderr.getvalue())
+
+        osc.conf.config["verbose"] = 0
+        osc.conf.config["debug"] = 1
+        stdout = io.StringIO()
+        stderr = io.StringIO()
+        with contextlib.redirect_stdout(stdout), 
contextlib.redirect_stderr(stderr):
+            print_msg("foo", "bar", print_to="verbose")
+        self.assertEqual("foo bar\n", stdout.getvalue())
+        self.assertEqual("", stderr.getvalue())
+
+    def test_none(self):
+        stdout = io.StringIO()
+        stderr = io.StringIO()
+        with contextlib.redirect_stdout(stdout), 
contextlib.redirect_stderr(stderr):
+            print_msg("foo", "bar", print_to=None)
+        self.assertEqual("", stdout.getvalue())
+        self.assertEqual("", stderr.getvalue())
+
+    def test_stdout(self):
+        stdout = io.StringIO()
+        stderr = io.StringIO()
+        with contextlib.redirect_stdout(stdout), 
contextlib.redirect_stderr(stderr):
+            print_msg("foo", "bar", print_to="stdout")
+        self.assertEqual("foo bar\n", stdout.getvalue())
+        self.assertEqual("", stderr.getvalue())
+
+
 if __name__ == "__main__":
     unittest.main()

++++++ osc.dsc ++++++
--- /var/tmp/diff_new_pack.nqBlCg/_old  2023-08-10 15:34:38.804443032 +0200
+++ /var/tmp/diff_new_pack.nqBlCg/_new  2023-08-10 15:34:38.812443081 +0200
@@ -1,6 +1,6 @@
 Format: 1.0
 Source: osc
-Version: 1.2.0-0
+Version: 1.3.0-0
 Binary: osc
 Maintainer: Adrian Schroeter <[email protected]>
 Architecture: any

Reply via email to