Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package osc for openSUSE:Factory checked in 
at 2024-02-27 22:48:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/osc (Old)
 and      /work/SRC/openSUSE:Factory/.osc.new.1770 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "osc"

Tue Feb 27 22:48:33 2024 rev:192 rq:1152040 version:1.6.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/osc/osc.changes  2024-01-25 18:42:11.060752950 
+0100
+++ /work/SRC/openSUSE:Factory/.osc.new.1770/osc.changes        2024-02-27 
22:48:45.815419743 +0100
@@ -1,0 +2,21 @@
+Fri Feb 23 08:57:23 UTC 2024 - Daniel Mach <daniel.m...@suse.com>
+
+- 1.6.1
+  - Command-line:
+    - Use busybox compatible commands for completion
+    - Change 'wipe' command to use the new get_user_input() function
+    - Fix error 500 in running 'meta attribute <prj>'
+  - Configuration:
+    - Fix resolving config symlink to the actual config file
+    - Honor XDG_CONFIG_HOME and XDG_CACHE_HOME env vars
+    - Warn about ignoring XDG_CONFIG_HOME and ~/.config/osc/oscrc if ~/.oscrc 
exists
+  - Library:
+    - Error out when branching a scmsync package
+    - New get_user_input() function for consistent handling of user input
+    - Move xml_indent, xml_quote and xml_unquote to osc.util.xml module
+    - Refactor makeurl(), deprecate query taking string or list arguments, 
drop osc_urlencode()
+    - Remove all path quoting, rely on makeurl()
+    - Always use dict query in makeurl()
+    - Fix core.slash_split() to strip both leading and trailing slashes
+
+-------------------------------------------------------------------

Old:
----
  osc-1.6.0.tar.gz

New:
----
  osc-1.6.1.tar.gz

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

Other differences:
------------------
++++++ osc.spec ++++++
--- /var/tmp/diff_new_pack.kd3hFl/_old  2024-02-27 22:48:46.511444976 +0100
+++ /var/tmp/diff_new_pack.kd3hFl/_new  2024-02-27 22:48:46.515445121 +0100
@@ -62,7 +62,7 @@
 %endif
 
 Name:           osc
-Version:        1.6.0
+Version:        1.6.1
 Release:        0
 Summary:        Command-line client for the Open Build Service
 License:        GPL-2.0-or-later

++++++ PKGBUILD ++++++
--- /var/tmp/diff_new_pack.kd3hFl/_old  2024-02-27 22:48:46.539445990 +0100
+++ /var/tmp/diff_new_pack.kd3hFl/_new  2024-02-27 22:48:46.543446136 +0100
@@ -1,5 +1,5 @@
 pkgname=osc
-pkgver=1.6.0
+pkgver=1.6.1
 pkgrel=0
 pkgdesc="Command-line client for the Open Build Service"
 arch=('x86_64')

++++++ debian.changelog ++++++
--- /var/tmp/diff_new_pack.kd3hFl/_old  2024-02-27 22:48:46.579447441 +0100
+++ /var/tmp/diff_new_pack.kd3hFl/_new  2024-02-27 22:48:46.583447586 +0100
@@ -1,4 +1,4 @@
-osc (1.6.0-0) unstable; urgency=low
+osc (1.6.1-0) unstable; urgency=low
 
   * Placeholder
 

++++++ osc-1.6.0.tar.gz -> osc-1.6.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/NEWS new/osc-1.6.1/NEWS
--- old/osc-1.6.0/NEWS  2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/NEWS  2024-02-23 09:45:57.000000000 +0100
@@ -1,3 +1,21 @@
+- 1.6.1
+  - Command-line:
+    - Use busybox compatible commands for completion
+    - Change 'wipe' command to use the new get_user_input() function
+    - Fix error 500 in running 'meta attribute <prj>'
+  - Configuration:
+    - Fix resolving config symlink to the actual config file
+    - Honor XDG_CONFIG_HOME and XDG_CACHE_HOME env vars
+    - Warn about ignoring XDG_CONFIG_HOME and ~/.config/osc/oscrc if ~/.oscrc 
exists
+  - Library:
+    - Error out when branching a scmsync package
+    - New get_user_input() function for consistent handling of user input
+    - Move xml_indent, xml_quote and xml_unquote to osc.util.xml module
+    - Refactor makeurl(), deprecate query taking string or list arguments, 
drop osc_urlencode()
+    - Remove all path quoting, rely on makeurl()
+    - Always use dict query in makeurl()
+    - Fix core.slash_split() to strip both leading and trailing slashes
+
 - 1.6.0
   - Command-line:
     - The 'token --trigger' command no longer sets '--operation=runservice' by 
default.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/contrib/osc.complete 
new/osc-1.6.1/contrib/osc.complete
--- old/osc-1.6.0/contrib/osc.complete  2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/contrib/osc.complete  2024-02-23 09:45:57.000000000 +0100
@@ -152,12 +152,12 @@
 update_projects_list ()
 {
     if test -s "${projects}" ; then
-        typeset -i ctime=$(command date -d "$(command stat -c '%z' 
${projects})" +'%s')
-        typeset -i   now=$(command date -d now +'%s')
+        typeset -i ctime=$(command stat -c '%Z' ${projects})
+        typeset -i   now=$(command date +'%s')
         if ((now - ctime > 86400)) ; then
             if tmp=$(mktemp ${projects}.XXXXXX) ; then
                 command ${command} ls / >| $tmp
-               mv -uf $tmp ${projects}
+               mv -f $tmp ${projects}
            fi
         fi
     else
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/doc/requirements.txt 
new/osc-1.6.1/doc/requirements.txt
--- old/osc-1.6.0/doc/requirements.txt  2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/doc/requirements.txt  2024-02-23 09:45:57.000000000 +0100
@@ -1,2 +1,3 @@
 cryptography
+sphinx-rtd-theme
 urllib3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/__init__.py 
new/osc-1.6.1/osc/__init__.py
--- old/osc-1.6.0/osc/__init__.py       2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/osc/__init__.py       2024-02-23 09:45:57.000000000 +0100
@@ -13,7 +13,7 @@
 
 
 from .util import git_version
-__version__ = git_version.get_version('1.6.0')
+__version__ = git_version.get_version('1.6.1')
 
 
 # vim: sw=4 et
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/_private/api.py 
new/osc-1.6.1/osc/_private/api.py
--- old/osc-1.6.0/osc/_private/api.py   2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/osc/_private/api.py   2024-02-23 09:45:57.000000000 +0100
@@ -7,6 +7,10 @@
 import xml.sax.saxutils
 from xml.etree import ElementTree as ET
 
+from ..util.xml import xml_escape
+from ..util.xml import xml_indent
+from ..util.xml import xml_unescape
+
 
 def get(apiurl, path, query=None):
     """
@@ -205,41 +209,3 @@
     if indent:
         xml_indent(node)
     ET.ElementTree(node).write(path)
-
-
-def xml_escape(string):
-    """
-    Escape the string so it's safe to use in XML and xpath.
-    """
-    entities = {
-        "\"": "&quot;",
-        "'": "&apos;",
-    }
-    if isinstance(string, bytes):
-        return xml.sax.saxutils.escape(string.decode("utf-8"), 
entities=entities).encode("utf-8")
-    return xml.sax.saxutils.escape(string, entities=entities)
-
-
-def xml_unescape(string):
-    """
-    Decode XML entities in the string.
-    """
-    entities = {
-        "&quot;": "\"",
-        "&apos;": "'",
-    }
-    if isinstance(string, bytes):
-        return xml.sax.saxutils.unescape(string.decode("utf-8"), 
entities=entities).encode("utf-8")
-    return xml.sax.saxutils.unescape(string, entities=entities)
-
-
-def xml_indent(root):
-    """
-    Indent XML so it looks pretty after printing or saving to file.
-    """
-    if hasattr(ET, "indent"):
-        # ElementTree supports indent() in Python 3.9 and newer
-        ET.indent(root)
-    else:
-        from .. import core as osc_core
-        osc_core.xmlindent(root)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/babysitter.py 
new/osc-1.6.1/osc/babysitter.py
--- old/osc-1.6.0/osc/babysitter.py     2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/osc/babysitter.py     2024-02-23 09:45:57.000000000 +0100
@@ -172,7 +172,7 @@
         print(e.msg, file=sys.stderr)
         traceback.print_exc(file=sys.stderr)
     except oscerr.PackageError as e:
-        print(e.msg, file=sys.stderr)
+        print(str(e), file=sys.stderr)
     except PackageError as e:
         print(f'{e.fname}:', e.msg, file=sys.stderr)
     except RPMError as e:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/build.py new/osc-1.6.1/osc/build.py
--- old/osc-1.6.0/osc/build.py  2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/osc/build.py  2024-02-23 09:45:57.000000000 +0100
@@ -21,7 +21,7 @@
 from . import connection
 from . import core
 from . import oscerr
-from .core import get_buildinfo, meta_exists, quote_plus, get_buildconfig, dgst
+from .core import get_buildinfo, meta_exists, 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
@@ -1021,13 +1021,13 @@
     except HTTPError as e:
         if e.code == 404:
             # check what caused the 404
-            if meta_exists(metatype='prj', path_args=(quote_plus(prj), ),
+            if meta_exists(metatype='prj', path_args=(prj, ),
                            template_args=None, create_new=False, 
apiurl=apiurl):
                 pkg_meta_e = None
                 try:
                     # take care, not to run into double trouble.
-                    pkg_meta_e = meta_exists(metatype='pkg', 
path_args=(quote_plus(prj),
-                                                                        
quote_plus(pac)), template_args=None, create_new=False,
+                    pkg_meta_e = meta_exists(metatype='pkg', path_args=(prj, 
pac),
+                                             template_args=None, 
create_new=False,
                                              apiurl=apiurl)
                 except:
                     pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/commandline.py 
new/osc-1.6.1/osc/commandline.py
--- old/osc-1.6.0/osc/commandline.py    2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/osc/commandline.py    2024-02-23 09:45:57.000000000 +0100
@@ -36,6 +36,7 @@
 from .core import *
 from .grabber import OscFileGrabber
 from .meter import create_text_meter
+from .output import get_user_input
 from .util import cpio, rpmquery, safewriter
 from .util.helper import _html_escape, format_table
 
@@ -1528,9 +1529,9 @@
 
         if opts.force or not filelist or '_patchinfo' not in filelist:
             print("Creating new patchinfo...")
-            query = 'cmd=createpatchinfo&name=' + patchinfo
+            query = {"cmd": "createpatchinfo", "name": patchinfo}
             if opts.force:
-                query += "&force=1"
+                query["force"] = 1
             url = makeurl(apiurl, ['source', project], query=query)
             f = http_POST(url)
             for p in meta_get_packagelist(apiurl, project):
@@ -1538,7 +1539,7 @@
                     patchinfo = p
         else:
             print("Update existing _patchinfo file...")
-            query = 'cmd=updatepatchinfo'
+            query = {"cmd": "updatepatchinfo"}
             url = makeurl(apiurl, ['source', project, patchinfo], query=query)
             f = http_POST(url)
 
@@ -1743,9 +1744,9 @@
 
     @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE',
                         help='affect only a given attribute')
-    @cmdln.option('--attribute-defaults', action='store_true',
+    @cmdln.option('--attribute-defaults', action='store_true', default=None,
                   help='include defined attribute defaults')
-    @cmdln.option('--attribute-project', action='store_true',
+    @cmdln.option('--attribute-project', action='store_true', default=None,
                   help='include project values, if missing in packages ')
     @cmdln.option('--blame', action='store_true',
                   help='show author and time of each line')
@@ -1949,7 +1950,7 @@
                           edit=True,
                           force=opts.force,
                           
remove_linking_repositories=opts.remove_linking_repositories,
-                          path_args=quote_plus(project),
+                          path_args=(project, ),
                           apiurl=apiurl,
                           msg=opts.message,
                           template_args=({
@@ -1958,7 +1959,7 @@
             elif cmd == 'pkg':
                 edit_meta(metatype='pkg',
                           edit=True,
-                          path_args=(quote_plus(project), quote_plus(package)),
+                          path_args=(project, package),
                           apiurl=apiurl,
                           template_args=({
                               'name': package,
@@ -1966,20 +1967,20 @@
             elif cmd == 'prjconf':
                 edit_meta(metatype='prjconf',
                           edit=True,
-                          path_args=quote_plus(project),
+                          path_args=(project, ),
                           apiurl=apiurl,
                           msg=opts.message,
                           template_args=None)
             elif cmd == 'user':
                 edit_meta(metatype='user',
                           edit=True,
-                          path_args=(quote_plus(user)),
+                          path_args=(user, ),
                           apiurl=apiurl,
                           template_args=({'user': user}))
             elif cmd == 'group':
                 edit_meta(metatype='group',
                           edit=True,
-                          path_args=(quote_plus(group)),
+                          path_args=(group, ),
                           apiurl=apiurl,
                           template_args=({'group': group}))
             elif cmd == 'pattern':
@@ -1992,7 +1993,7 @@
                 edit_meta(
                     metatype='attribute',
                     edit=True,
-                    path_args=(quote_plus(project), 
quote_plus(opts.attribute)),
+                    path_args=(project, opts.attribute),
                     apiurl=apiurl,
                     # PUT is not supported
                     method="POST",
@@ -2061,32 +2062,32 @@
                           
remove_linking_repositories=opts.remove_linking_repositories,
                           apiurl=apiurl,
                           msg=opts.message,
-                          path_args=quote_plus(project))
+                          path_args=(project, ))
             elif cmd == 'pkg':
                 edit_meta(metatype='pkg',
                           data=f,
                           edit=opts.edit,
                           apiurl=apiurl,
-                          path_args=(quote_plus(project), quote_plus(package)))
+                          path_args=(project, package))
             elif cmd == 'prjconf':
                 edit_meta(metatype='prjconf',
                           data=f,
                           edit=opts.edit,
                           apiurl=apiurl,
                           msg=opts.message,
-                          path_args=quote_plus(project))
+                          path_args=(project, ))
             elif cmd == 'user':
                 edit_meta(metatype='user',
                           data=f,
                           edit=opts.edit,
                           apiurl=apiurl,
-                          path_args=(quote_plus(user)))
+                          path_args=(user, ))
             elif cmd == 'group':
                 edit_meta(metatype='group',
                           data=f,
                           edit=opts.edit,
                           apiurl=apiurl,
-                          path_args=(quote_plus(group)))
+                          path_args=(group, ))
             elif cmd == 'pattern':
                 edit_meta(metatype='pattern',
                           data=f,
@@ -4445,7 +4446,7 @@
         if not exists and (srcprj != self._process_project_name(args[0]) or 
srcpkg != args[1]):
             try:
                 root = ET.fromstring(b''.join(show_attribute_meta(apiurl, 
args[0], None, None,
-                                                                  
conf.config['maintained_update_project_attribute'], False, False)))
+                                                                  
conf.config['maintained_update_project_attribute'], None, None)))
                 # this might raise an AttributeError
                 uproject = root.find('attribute').find('value').text
                 print('\nNote: The branch has been created from the configured 
update project: %s'
@@ -6025,7 +6026,7 @@
                     raise e
 
     @cmdln.alias('r')
-    @cmdln.option('-l', '--last-build', action='store_true',
+    @cmdln.option('-l', '--last-build', action='store_true', default=None,
                         help='show last build results 
(succeeded/failed/unknown)')
     @cmdln.option('-r', '--repo', action='append', default=[],
                         help='Show results only for specified repo(s)')
@@ -6276,7 +6277,7 @@
                 query['last'] = 1
             if opts.lastsucceeded:
                 query['lastsucceeded'] = 1
-            u = makeurl(self.get_api_url(), ['build', quote_plus(project), 
quote_plus(repository), quote_plus(arch), quote_plus(package), '_log'], 
query=query)
+            u = makeurl(self.get_api_url(), ['build', project, repository, 
arch, package, '_log'], query=query)
             f = http_GET(u)
             root = ET.parse(f).getroot()
             offset = int(root.find('entry').get('size'))
@@ -6289,7 +6290,7 @@
         elif opts.offset:
             offset = int(opts.offset)
         strip_time = opts.strip_time or conf.config['buildlog_strip_time']
-        print_buildlog(apiurl, quote_plus(project), quote_plus(package), 
quote_plus(repository), quote_plus(arch), offset, strip_time, opts.last, 
opts.lastsucceeded)
+        print_buildlog(apiurl, project, package, repository, arch, offset, 
strip_time, opts.last, opts.lastsucceeded)
 
     def print_repos(self, repos_only=False, exc_class=oscerr.WrongArgs, 
exc_msg='Missing arguments', project=None):
         wd = Path.cwd()
@@ -6370,7 +6371,7 @@
                 query['last'] = 1
             if opts.lastsucceeded:
                 query['lastsucceeded'] = 1
-            u = makeurl(self.get_api_url(), ['build', quote_plus(project), 
quote_plus(repository), quote_plus(arch), quote_plus(package), '_log'], 
query=query)
+            u = makeurl(self.get_api_url(), ['build', project, repository, 
arch, package, '_log'], query=query)
             f = http_GET(u)
             root = ET.parse(f).getroot()
             offset = int(root.find('entry').get('size'))
@@ -6383,7 +6384,7 @@
         elif opts.offset:
             offset = int(opts.offset)
         strip_time = opts.strip_time or conf.config['buildlog_strip_time']
-        print_buildlog(apiurl, quote_plus(project), quote_plus(package), 
quote_plus(repository), quote_plus(arch), offset, strip_time, opts.last, 
opts.lastsucceeded)
+        print_buildlog(apiurl, project, package, repository, arch, offset, 
strip_time, opts.last, opts.lastsucceeded)
 
     def _find_last_repo_arch(self, repo=None, fatal=True):
         files = glob.glob(os.path.join(Path.cwd(), store, "_buildinfo-*"))
@@ -7307,11 +7308,13 @@
                 build_root = osc_build.calculate_build_root(apihost, prj, pac, 
repo, arch, user)
             if opts.wipe and not opts.force:
                 # Confirm delete
-                print(f"Really wipe '{build_root}'? [y/N]: ", end="")
-                choice = raw_input().lower()
-                if choice != 'y':
-                    print('Aborting')
-                    sys.exit(0)
+                reply = get_user_input(
+                    f"Really wipe '{build_root}'?",
+                    answers={"y": "yes", "n": "no"},
+                    default_answer="n",
+                )
+                if reply != "y":
+                    raise oscerr.UserAbort()
             build_args = ['--root=' + build_root, '--noinit', '--shell']
             if opts.wipe:
                 build_args.append('--wipe')
@@ -8709,7 +8712,7 @@
                 apiurl = osc_store.Store(project_dir).apiurl
                 user = conf.get_apiurl_usr(apiurl)
                 data = meta_exists(metatype='pkg',
-                                   path_args=(quote_plus(project), 
quote_plus(pac)),
+                                   path_args=(project, pac),
                                    template_args=({
                                        'name': pac,
                                        'user': user}), apiurl=apiurl)
@@ -8723,7 +8726,7 @@
                     print('error - cannot get meta data', file=sys.stderr)
                     sys.exit(1)
                 edit_meta(metatype='pkg',
-                          path_args=(quote_plus(project), quote_plus(pac)),
+                          path_args=(project, pac),
                           data=data, apiurl=apiurl)
                 Package.init_package(apiurl, project, pac, 
os.path.join(project_dir, pac))
             else:
@@ -9254,7 +9257,7 @@
         o = open(destfile, 'wb')
         if md5 != '':
             query = {'rev': dir['srcmd5']}
-            u = makeurl(dir['apiurl'], ['source', dir['project'], 
dir['package'], pathname2url(name)], query=query)
+            u = makeurl(dir['apiurl'], ['source', dir['project'], 
dir['package'], name], query=query)
             for buf in streamfile(u, http_GET, BUFSIZE):
                 o.write(buf)
         o.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/conf.py new/osc-1.6.1/osc/conf.py
--- old/osc-1.6.0/osc/conf.py   2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/osc/conf.py   2024-02-23 09:45:57.000000000 +0100
@@ -65,6 +65,7 @@
 from . import credentials
 from . import OscConfigParser
 from . import oscerr
+from .output import tty
 from .util import xdg
 from .util.helper import raw_input
 from .util.models import *
@@ -1605,14 +1606,14 @@
 
 def write_config(fname, cp):
     """write new configfile in a safe way"""
-    if os.path.exists(fname) and not os.path.isfile(fname):
-        # only write to a regular file
-        return
-
     # config file is behind a symlink
     # resolve the symlink and continue writing the config as usual
     if os.path.islink(fname):
-        fname = os.readlink(fname)
+        fname = os.path.realpath(fname)
+
+    if os.path.exists(fname) and not os.path.isfile(fname):
+        # only write to a regular file
+        return
 
     # create directories to the config file (if they don't exist already)
     fdir = os.path.dirname(fname)
@@ -2055,13 +2056,17 @@
     # needed for compat reasons(users may have their oscrc still in ~
     if 'OSC_CONFIG' in os.environ:
         return os.environ.get('OSC_CONFIG')
-    if os.path.exists(os.path.expanduser('~/.oscrc')):
-        return '~/.oscrc'
 
-    if os.environ.get('XDG_CONFIG_HOME', '') != '':
-        conffile = f"{os.environ.get('XDG_CONFIG_HOME')}/osc/oscrc"
-    else:
-        conffile = '~/.config/osc/oscrc'
+    conffile = os.path.join(xdg.XDG_CONFIG_HOME, "osc", "oscrc")
+
+    if os.path.exists(os.path.expanduser("~/.oscrc")) or 
os.path.islink(os.path.expanduser("~/.oscrc")):
+        if "XDG_CONFIG_HOME" in os.environ:
+            print(f"{tty.colorize('WARNING', 'yellow,bold')}: Ignoring 
XDG_CONFIG_HOME env, loading an existing config from '~/.oscrc' instead", 
file=sys.stderr)
+            print("         To fix this, move the existing '~/.oscrc' to XDG 
location such as '~/.config/osc/oscrc'", file=sys.stderr)
+        elif os.path.exists(os.path.expanduser(conffile)):
+            print(f"{tty.colorize('WARNING', 'yellow,bold')}: Ignoring config 
'{conffile}' in XDG location, loading an existing config from ~/.oscrc 
instead", file=sys.stderr)
+            print("         To fix this, remove '~/.oscrc'", file=sys.stderr)
+        return '~/.oscrc'
 
     return conffile
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/core.py new/osc-1.6.1/osc/core.py
--- old/osc-1.6.0/osc/core.py   2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/osc/core.py   2024-02-23 09:45:57.000000000 +0100
@@ -30,14 +30,14 @@
 import tempfile
 import textwrap
 import time
+import warnings
 from functools import cmp_to_key, total_ordering
 from http.client import IncompleteRead
 from io import StringIO
 from pathlib import Path
 from typing import Optional, Dict, Union, List, Iterable
-from urllib.parse import urlsplit, urlunsplit, urlparse, quote_plus, 
urlencode, unquote
+from urllib.parse import urlsplit, urlunsplit, urlparse, quote, urlencode, 
unquote
 from urllib.error import HTTPError
-from urllib.request import pathname2url
 from xml.etree import ElementTree as ET
 
 try:
@@ -53,7 +53,9 @@
 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 import xdg
 from .util.helper import decode_list, decode_it, raw_input, _html_escape
+from .util.xml import xml_indent_compat as xmlindent
 
 
 ET_ENCODING = "unicode"
@@ -364,7 +366,7 @@
     def getProjectGlobalServices(self, apiurl: str, project: str, package: 
str):
         self.apiurl = apiurl
         # get all project wide services in one file, we don't store it yet
-        u = makeurl(apiurl, ['source', project, package], 
query='cmd=getprojectservices')
+        u = makeurl(apiurl, ["source", project, package], query={"cmd": 
"getprojectservices"})
         try:
             f = http_POST(u)
             root = ET.parse(f).getroot()
@@ -636,26 +638,6 @@
     def haserror(self):
         return self.error is not None
 
-# http://effbot.org/zone/element-lib.htm#prettyprint
-
-
-def xmlindent(elem, level=0):
-    i = "\n" + level * "  "
-    if isinstance(elem, ET.ElementTree):
-        elem = elem.getroot()
-    if len(elem):
-        if not elem.text or not elem.text.strip():
-            elem.text = i + "  "
-        for e in elem:
-            xmlindent(e, level + 1)
-            if not e.tail or not e.tail.strip():
-                e.tail = i + "  "
-        if not e.tail or not e.tail.strip():
-            e.tail = i
-    else:
-        if level and (not elem.tail or not elem.tail.strip()):
-            elem.tail = i
-
 
 class Project:
     """
@@ -1115,7 +1097,7 @@
         else:
             user = conf.get_apiurl_usr(self.apiurl)
             edit_meta(metatype='pkg',
-                      path_args=(quote_plus(self.name), quote_plus(pac)),
+                      path_args=(self.name, pac),
                       template_args=({
                           'name': pac,
                           'user': user}),
@@ -1170,11 +1152,11 @@
         package = store_read_package(pac_path)
         apiurl = store.apiurl
         if not meta_exists(metatype='pkg',
-                           path_args=(quote_plus(project), 
quote_plus(package)),
+                           path_args=(project, package),
                            template_args=None, create_new=False, 
apiurl=apiurl):
             user = conf.get_apiurl_usr(self.apiurl)
             edit_meta(metatype='pkg',
-                      path_args=(quote_plus(project), quote_plus(package)),
+                      path_args=(project, package),
                       template_args=({'name': pac, 'user': user}), 
apiurl=apiurl)
         p = Package(pac_path)
         p.todo = files
@@ -1540,18 +1522,18 @@
 
     def delete_remote_source_file(self, n):
         """delete a remote source file (e.g. from the server)"""
-        query = 'rev=upload'
-        u = makeurl(self.apiurl, ['source', self.prjname, self.name, 
pathname2url(n)], query=query)
+        query = {"rev": "upload"}
+        u = makeurl(self.apiurl, ['source', self.prjname, self.name, n], 
query=query)
         http_DELETE(u)
 
     def put_source_file(self, n, tdir, copy_only=False):
-        query = 'rev=repository'
+        query = {"rev": "repository"}
         tfilename = os.path.join(tdir, n)
         shutil.copyfile(os.path.join(self.dir, n), tfilename)
         # escaping '+' in the URL path (note: not in the URL query string) is
         # only a workaround for ruby on rails, which swallows it otherwise
         if not copy_only:
-            u = makeurl(self.apiurl, ['source', self.prjname, self.name, 
pathname2url(n)], query=query)
+            u = makeurl(self.apiurl, ['source', self.prjname, self.name, n], 
query=query)
             http_PUT(u, file=tfilename)
         if n in self.to_be_added:
             self.to_be_added.remove(n)
@@ -3452,18 +3434,18 @@
     return (m.group('apiurl'), m.group('project'), m.group('package'), 
m.group('repository'), m.group('arch'))
 
 
-def slash_split(l):
+def slash_split(args):
     """Split command line arguments like 'foo/bar' into 'foo' 'bar'.
     This is handy to allow copy/paste a project/package combination in this 
form.
 
-    Trailing slashes are removed before the split, because the split would
-    otherwise give an additional empty string.
+    Leading and trailing slashes are removed before the split, because the 
split
+    could otherwise give additional empty strings.
     """
-    r = []
-    for i in l:
-        i = i.rstrip('/')
-        r += i.split('/')
-    return r
+    result = []
+    for arg in args:
+        arg = arg.strip("/")
+        result += arg.split("/")
+    return result
 
 
 def expand_proj_pack(args, idx=0, howmany=0):
@@ -3606,39 +3588,71 @@
     return path
 
 
-def osc_urlencode(data):
+class UrlQueryArray(list):
     """
-    An urlencode wrapper that encodes dictionaries in OBS compatible way:
-    {"file": ["foo", "bar"]} -> &file[]=foo&file[]=bar
+    Passing values wrapped in this object causes ``makeurl()`` to encode the 
list
+    in Ruby on Rails compatible way (adding square brackets to the parameter 
names):
+    {"file": UrlQueryArray(["foo", "bar"])} -> &file[]=foo&file[]=bar
     """
-    data = copy.deepcopy(data)
-    if isinstance(data, dict):
-        for key, value in list(data.items()):
-            if isinstance(value, list):
-                del data[key]
-                data[f"{key}[]"] = value
+    pass
 
-    return urlencode(data, doseq=True)
 
+def makeurl(apiurl: str, path: List[str], query: Optional[dict] = None):
+    """
+    Construct an URL based on the given arguments.
 
-def makeurl(baseurl: str, l, query=None):
-    """Given a list of path compoments, construct a complete URL.
-
-    Optional parameters for a query string can be given as a list, as a
-    dictionary, or as an already assembled string.
-    In case of a dictionary, the parameters will be urlencoded by this
-    function. In case of a list not -- this is to be backwards compatible.
+    :param apiurl: URL to the API server.
+    :param path: List of URL path components.
+    :param query: Optional dictionary with URL query data.
+                  Values can be: ``str``, ``int``, ``bool``, ``[str]``, 
``[int]``.
+                  Items with value equal to ``None`` will be skipped.
     """
-    query = query or []
-    _private.print_msg("makeurl:", baseurl, l, query, print_to="debug")
+    apiurl_scheme, apiurl_netloc, apiurl_path = urlsplit(apiurl)[0:3]
+
+    path = apiurl_path.split("/") + [i.strip("/") for i in path]
+    path = [quote(i, safe="/:") for i in path]
+    path_str = "/".join(path)
+
+    # DEPRECATED
+    if isinstance(query, (list, tuple)):
+        warnings.warn(
+            "makeurl() query taking a list or a tuple is deprecated. Use dict 
instead.",
+            DeprecationWarning
+        )
+        query_str = "&".join(query)
+        return urlunsplit((apiurl_scheme, apiurl_netloc, path_str, query_str, 
""))
+
+    # DEPRECATED
+    if isinstance(query, str):
+        warnings.warn(
+            "makeurl() query taking a string is deprecated. Use dict instead.",
+            DeprecationWarning
+        )
+        query_str = query
+        return urlunsplit((apiurl_scheme, apiurl_netloc, path_str, query_str, 
""))
 
-    if isinstance(query, list):
-        query = '&'.join(query)
-    elif isinstance(query, dict):
-        query = osc_urlencode(query)
+    if query is None:
+        query = {}
+    query = copy.deepcopy(query)
 
-    scheme, netloc, path = urlsplit(baseurl)[0:3]
-    return urlunsplit((scheme, netloc, '/'.join([path] + list(l)), query, ''))
+    for key in list(query):
+        value = query[key]
+
+        if value in (None, [], ()):
+            # remove items with value equal to None or [] or ()
+            del query[key]
+        elif isinstance(value, bool):
+            # convert boolean values to "0" or "1"
+            query[key] = str(int(value))
+        elif isinstance(value, UrlQueryArray):
+            # encode lists in Ruby on Rails compatible way:
+            # {"file": ["foo", "bar"]} -> &file[]=foo&file[]=bar
+            del query[key]
+            query[f"{key}[]"] = value
+
+    query_str = urlencode(query, doseq=True)
+
+    return urlunsplit((apiurl_scheme, apiurl_netloc, path_str, query_str, ""))
 
 
 def check_store_version(dir):
@@ -3818,11 +3832,9 @@
     path.append('_attribute')
     if attribute:
         path.append(attribute)
-    query = []
-    if with_defaults:
-        query.append("with_default=1")
-    if with_project:
-        query.append("with_project=1")
+    query = {}
+    query["with_default"] = with_defaults
+    query["with_project"] = with_project
     url = makeurl(apiurl, path, query)
     try:
         f = http_GET(url)
@@ -4269,11 +4281,10 @@
 
 
 def show_project_sourceinfo(apiurl: str, project: str, nofilename: bool, 
*packages):
-    query = ['view=info']
-    if packages:
-        query.extend([f'package={quote_plus(p)}' for p in packages])
-    if nofilename:
-        query.append('nofilename=1')
+    query = {}
+    query["view"] = "info"
+    query["package"] = packages
+    query["nofilename"] = nofilename
     f = http_GET(makeurl(apiurl, ['source', project], query=query))
     return f.read()
 
@@ -4502,7 +4513,7 @@
     if os.stat(filename).st_mtime != orig_mtime:
         # file has changed
 
-        cache_dir = os.path.expanduser("~/.cache/osc/edited-messages")
+        cache_dir = os.path.expanduser(os.path.join(xdg.XDG_CACHE_HOME, "osc", 
"edited-messages"))
         try:
             os.makedirs(cache_dir, mode=0o700)
         except FileExistsError:
@@ -4670,7 +4681,7 @@
        options_block,
        _html_escape(message))
 
-    u = makeurl(apiurl, ['request'], query='cmd=create')
+    u = makeurl(apiurl, ["request"], query={"cmd": "create"})
     r = None
     try:
         f = http_POST(u, data=xml)
@@ -5149,7 +5160,7 @@
 
 
 def get_group_meta(apiurl: str, group: str):
-    u = makeurl(apiurl, ['group', quote_plus(group)])
+    u = makeurl(apiurl, ['group', group])
     try:
         f = http_GET(u)
         return b''.join(f.readlines())
@@ -5159,7 +5170,7 @@
 
 
 def get_user_meta(apiurl: str, user: str):
-    u = makeurl(apiurl, ['person', quote_plus(user)])
+    u = makeurl(apiurl, ['person', user])
     try:
         f = http_GET(u)
         return b''.join(f.readlines())
@@ -5240,7 +5251,7 @@
         query['rev'] = revision
     u = makeurl(
         apiurl,
-        ["source", prj, package, 
pathname2url(filename.encode(locale.getpreferredencoding(), "replace"))],
+        ["source", prj, package, filename],
         query=query,
     )
     download(u, targetfilename, progress_obj, mtime)
@@ -5426,7 +5437,7 @@
         query['view'] = 'xml'
         query['unified'] = 0
     if files:
-        query["file"] = files
+        query["file"] = UrlQueryArray(files)
 
     u = makeurl(apiurl, ['source', new_project, new_package], query=query)
     f = http_POST(u, retry_on_400=False)
@@ -5671,7 +5682,7 @@
 
     # before we create directories and stuff, check if the package actually
     # exists
-    meta_data = b''.join(show_package_meta(apiurl, quote_plus(project), 
quote_plus(package)))
+    meta_data = b''.join(show_package_meta(apiurl, project, package))
     root = ET.fromstring(meta_data)
     scmsync_element = root.find("scmsync")
     if scmsync_element is not None and scmsync_element.text is not None:
@@ -5756,7 +5767,7 @@
     """
 
     if '_link' in meta_get_filelist(apiurl, project, package):
-        u = makeurl(apiurl, ['source', project, package], 'cmd=linktobranch')
+        u = makeurl(apiurl, ["source", project, package], {"cmd": 
"linktobranch"})
         http_POST(u)
     else:
         raise oscerr.OscIOError(None, f'no _link file inside project 
\'{project}\' package \'{package}\'')
@@ -5791,7 +5802,7 @@
     apiurl = conf.config['apiurl']
     try:
         dst_meta = meta_exists(metatype='pkg',
-                               path_args=(quote_plus(dst_project), 
quote_plus(dst_package)),
+                               path_args=(dst_project, dst_package),
                                template_args=None,
                                create_new=False, apiurl=apiurl)
         root = ET.fromstring(parse_meta_to_string(dst_meta))
@@ -5921,7 +5932,7 @@
 
     try:
         dst_meta = meta_exists(metatype='pkg',
-                               path_args=(quote_plus(dst_project), 
quote_plus(dst_package_meta)),
+                               path_args=(dst_project, dst_package_meta),
                                template_args=None,
                                create_new=False, apiurl=apiurl)
         root = ET.fromstring(parse_meta_to_string(dst_meta))
@@ -6104,6 +6115,34 @@
     """
     Branch a package (via API call)
     """
+
+    # BEGIN: Error out on branching scmsync packages; this should be properly 
handled in the API
+
+    # read src_package meta
+    m = b"".join(show_package_meta(apiurl, src_project, src_package))
+    root = ET.fromstring(m)
+
+    devel_project = None
+    devel_package = None
+    if not nodevelproject:
+        devel_node = root.find("devel")
+        if devel_node is not None:
+            devel_project = devel_node.get("project")
+            devel_package = devel_node.get("package", src_package)
+        if devel_project:
+            # replace src_package meta with devel_package meta because we're 
about branch from devel
+            m = b"".join(show_package_meta(apiurl, devel_project, 
devel_package))
+            root = ET.fromstring(m)
+
+    # error out if we're branching a scmsync package (we'd end up with garbage 
anyway)
+    if root.find("scmsync") is not None:
+        msg = "Cannot branch a package with <scmsync> set."
+        if devel_project:
+            raise oscerr.PackageError(devel_project, devel_package, msg)
+        raise oscerr.PackageError(src_project, src_package, msg)
+
+    # END: Error out on branching scmsync packages; this should be properly 
handled in the API
+
     query = {'cmd': 'branch'}
     if nodevelproject:
         query['ignoredevel'] = '1'
@@ -6215,7 +6254,7 @@
         src_meta = replace_pkg_meta(src_meta, dst_package, dst_project, 
keep_maintainers,
                                     dst_userid, keep_develproject)
 
-        url = make_meta_url('pkg', (quote_plus(dst_project),) + 
(quote_plus(dst_package),), dst_apiurl)
+        url = make_meta_url('pkg', (dst_project, dst_package), dst_apiurl)
         found = None
         try:
             found = http_GET(url).readlines()
@@ -6264,7 +6303,7 @@
             with tempfile.NamedTemporaryFile(prefix='osc-copypac') as f:
                 get_source_file(src_apiurl, src_project, src_package, filename,
                                 targetfilename=f.name, revision=revision)
-                path = ['source', dst_project, dst_package, 
pathname2url(filename)]
+                path = ['source', dst_project, dst_package, filename]
                 u = makeurl(dst_apiurl, path, query={'rev': 'repository'})
                 http_PUT(u, file=f.name)
         tfilelist = Package.commit_filelist(dst_apiurl, dst_project, 
dst_package,
@@ -6495,29 +6534,21 @@
     repository: Optional[List[str]] = None,
     arch: Optional[List[str]] = None,
     oldstate: Optional[str] = None,
-    multibuild=False,
-    locallink=False,
+    multibuild: Optional[bool] = None,
+    locallink: Optional[bool] = None,
     code: Optional[str] = None,
 ):
     repository = repository or []
     arch = arch or []
-    query = []
-    if package:
-        query.append(f'package={quote_plus(package)}')
-    if oldstate:
-        query.append(f'oldstate={quote_plus(oldstate)}')
-    if lastbuild:
-        query.append('lastbuild=1')
-    if multibuild:
-        query.append('multibuild=1')
-    if locallink:
-        query.append('locallink=1')
-    if code:
-        query.append(f'code={quote_plus(code)}')
-    for repo in repository:
-        query.append(f'repository={quote_plus(repo)}')
-    for a in arch:
-        query.append(f'arch={quote_plus(a)}')
+    query = {}
+    query["package"] = package
+    query["oldstate"] = oldstate
+    query["lastbuild"] = lastbuild
+    query["multibuild"] = multibuild
+    query["locallink"] = locallink
+    query["code"] = code
+    query["repository"] = repository
+    query["arch"] = arch
     u = makeurl(apiurl, ['build', prj, '_result'], query=query)
     f = http_GET(u)
     return f.readlines()
@@ -7017,15 +7048,13 @@
 
 
 def get_dependson(apiurl: str, project: str, repository: str, arch: str, 
packages=None, reverse=None):
-    query = []
-    if packages:
-        for i in packages:
-            query.append(f'package={quote_plus(i)}')
+    query = {}
+    query["package"] = packages
 
     if reverse:
-        query.append('view=revpkgnames')
+        query["view"] = "revpkgnames"
     else:
-        query.append('view=pkgnames')
+        query["view"] = "pkgnames"
 
     u = makeurl(apiurl, ['build', project, repository, arch, '_builddepinfo'], 
query=query)
     f = http_GET(u)
@@ -7035,12 +7064,9 @@
 def get_buildinfo(
     apiurl: str, prj: str, package: str, repository: str, arch: str, 
specfile=None, addlist=None, debug=None
 ):
-    query = []
-    if addlist:
-        for i in addlist:
-            query.append(f'add={quote_plus(i)}')
-    if debug:
-        query.append('debug=1')
+    query = {}
+    query["add"] = addlist
+    query["debug"] = debug
 
     u = makeurl(apiurl, ['build', prj, repository, arch, package, 
'_buildinfo'], query=query)
 
@@ -7052,10 +7078,8 @@
 
 
 def get_buildconfig(apiurl: str, prj: str, repository: str, path=None):
-    query = []
-    if path:
-        for prp in path:
-            query.append(f'path={quote_plus(prp)}')
+    query = {}
+    query["path"] = path
     u = makeurl(apiurl, ['build', prj, repository, '_buildconfig'], 
query=query)
     f = http_GET(u)
     return f.read()
@@ -7859,10 +7883,10 @@
 
 def addPerson(apiurl: str, prj: str, pac: str, user: str, role="maintainer"):
     """ add a new person to a package or project """
-    path = quote_plus(prj),
+    path = (prj, )
     kind = 'prj'
     if pac:
-        path = path + (quote_plus(pac),)
+        path = path + (pac ,)
         kind = 'pkg'
     data = meta_exists(metatype=kind,
                        path_args=path,
@@ -7895,10 +7919,10 @@
 
 def delPerson(apiurl: str, prj: str, pac: str, user: str, role="maintainer"):
     """ delete a person from a package or project """
-    path = quote_plus(prj),
+    path = (prj, )
     kind = 'prj'
     if pac:
-        path = path + (quote_plus(pac), )
+        path = path + (pac, )
         kind = 'pkg'
     data = meta_exists(metatype=kind,
                        path_args=path,
@@ -7924,10 +7948,10 @@
 
 def setBugowner(apiurl: str, prj: str, pac: str, user=None, group=None):
     """ delete all bugowners (user and group entries) and set one new one in a 
package or project """
-    path = quote_plus(prj),
+    path = (prj, )
     kind = 'prj'
     if pac:
-        path = path + (quote_plus(pac), )
+        path = path + (pac, )
         kind = 'pkg'
     data = meta_exists(metatype=kind,
                        path_args=path,
@@ -7957,7 +7981,7 @@
 
 def setDevelProject(apiurl, prj, pac, dprj, dpkg=None):
     """ set the <devel project="..."> element to package metadata"""
-    path = (quote_plus(prj),) + (quote_plus(pac),)
+    path = (prj, pac)
     data = meta_exists(metatype='pkg',
                        path_args=path,
                        template_args=None,
@@ -8821,7 +8845,7 @@
 
 
 def get_comments(apiurl: str, kind, *args):
-    url = makeurl(apiurl, ('comments', kind) + args)
+    url = makeurl(apiurl, ["comments", kind] + list(args))
     f = http_GET(url)
     return ET.parse(f).getroot()
 
@@ -8844,9 +8868,8 @@
 
 def create_comment(apiurl: str, kind, comment, *args, **kwargs) -> 
Optional[str]:
     query = {}
-    if kwargs.get('parent') is not None:
-        query = {'parent_id': kwargs['parent']}
-    u = makeurl(apiurl, ('comments', kind) + args, query=query)
+    query["parent_id"] = kwargs.get("parent", None)
+    u = makeurl(apiurl, ["comments", kind] + list(args), query=query)
     f = http_POST(u, data=comment)
     ret = ET.fromstring(f.read()).find('summary')
     if ret is None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/fetch.py new/osc-1.6.1/osc/fetch.py
--- old/osc-1.6.0/osc/fetch.py  2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/osc/fetch.py  2024-02-23 09:45:57.000000000 +0100
@@ -10,7 +10,6 @@
 import subprocess
 import sys
 import tempfile
-from urllib.parse import quote_plus
 from urllib.request import HTTPError
 
 from . import checker as osc_checker
@@ -51,10 +50,10 @@
     def __download_cpio_archive(self, apiurl, project, repo, arch, package, 
**pkgs):
         if not pkgs:
             return
-        query = [f'binary={quote_plus(i)}' for i in pkgs]
-        query.append('view=cpio')
-        for module in self.modules:
-            query.append(f"module={module}")
+        query = {}
+        query["binary"] = pkgs
+        query["view"] = "cpio"
+        query["module"] = self.modules
         try:
             url = makeurl(apiurl, ['build', project, repo, arch, package], 
query=query)
             sys.stdout.write("preparing download ...\r")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/oscssl.py new/osc-1.6.1/osc/oscssl.py
--- old/osc-1.6.0/osc/oscssl.py 2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/osc/oscssl.py 2024-02-23 09:45:57.000000000 +0100
@@ -13,6 +13,7 @@
 from urllib3.util.ssl_ import create_urllib3_context
 
 from . import oscerr
+from .util import xdg
 
 
 # based on openssl's include/openssl/x509_vfy.h.in
@@ -55,7 +56,7 @@
         if not self.host:
             raise ValueError("Empty `host`")
 
-        self.dir_path = os.path.expanduser("~/.config/osc/trusted-certs")
+        self.dir_path = os.path.expanduser(os.path.join(xdg.XDG_CONFIG_HOME, 
"osc", "trusted-certs"))
         if not os.path.isdir(self.dir_path):
             try:
                 os.makedirs(self.dir_path, mode=0o700)
@@ -103,7 +104,7 @@
         Temporarily trust the certificate.
         """
         self.cert = cert
-        tmp_dir = os.path.expanduser("~/.config/osc")
+        tmp_dir = os.path.expanduser(os.path.join(xdg.XDG_CONFIG_HOME, "osc"))
         data = self.cert.public_bytes(serialization.Encoding.PEM)
         with tempfile.NamedTemporaryFile(mode="wb+", dir=tmp_dir, 
prefix="temp_trusted_cert_") as f:
             f.write(data)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/output/__init__.py 
new/osc-1.6.1/osc/output/__init__.py
--- old/osc-1.6.0/osc/output/__init__.py        2024-01-25 09:49:14.000000000 
+0100
+++ new/osc-1.6.1/osc/output/__init__.py        2024-02-23 09:45:57.000000000 
+0100
@@ -1,4 +1,5 @@
 from .key_value_table import KeyValueTable
+from .input import get_user_input
 from .tty import colorize
 from .widechar import wc_ljust
 from .widechar import wc_width
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/output/input.py 
new/osc-1.6.1/osc/output/input.py
--- old/osc-1.6.0/osc/output/input.py   1970-01-01 01:00:00.000000000 +0100
+++ new/osc-1.6.1/osc/output/input.py   2024-02-23 09:45:57.000000000 +0100
@@ -0,0 +1,51 @@
+import sys
+import textwrap
+from typing import Dict
+from typing import Optional
+
+from .. import oscerr
+from .tty import colorize
+
+
+def get_user_input(question: str, answers: Dict[str, str], default_answer: 
Optional[str] = None) -> str:
+    """
+    Ask user a question and wait for reply.
+
+    :param question: The question. The text gets automatically dedented and 
stripped.
+    :param answers: A dictionary with answers. Keys are the expected replies 
and values are their descriptions.
+    :param default_answer: The default answer. Must be ``None`` or match an 
``answers`` entry.
+    """
+
+    if default_answer and default_answer not in answers:
+        raise ValueError(f"Default answer doesn't match any answer: 
{default_answer}")
+
+    question = textwrap.dedent(question)
+    question = question.strip()
+
+    prompt = []
+    for key, value in answers.items():
+        value = f"{colorize(key, 'bold')}){value}"
+        prompt.append(value)
+
+    prompt_str = " / ".join(prompt)
+    if default_answer:
+        prompt_str += f" (default={colorize(default_answer, 'bold')})"
+    prompt_str += ": "
+
+    print(question, file=sys.stderr)
+
+    while True:
+        try:
+            reply = input(prompt_str)
+        except EOFError:
+            # interpret ctrl-d as user abort
+            raise oscerr.UserAbort()  # pylint: disable=raise-missing-from
+
+        if reply in answers:
+            return reply
+        if reply.strip() in answers:
+            return reply.strip()
+        if not reply.strip():
+            return default_answer
+
+        print(f"Invalid reply: {colorize(reply, 'bold,red')}", file=sys.stderr)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/osc/util/git_version.py 
new/osc-1.6.1/osc/util/git_version.py
--- old/osc-1.6.0/osc/util/git_version.py       2024-01-25 09:49:14.000000000 
+0100
+++ new/osc-1.6.1/osc/util/git_version.py       2024-02-23 09:45:57.000000000 
+0100
@@ -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.6.0"
+    version = "1.6.1"
     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.6.0/osc/util/xml.py 
new/osc-1.6.1/osc/util/xml.py
--- old/osc-1.6.0/osc/util/xml.py       1970-01-01 01:00:00.000000000 +0100
+++ new/osc-1.6.1/osc/util/xml.py       2024-02-23 09:45:57.000000000 +0100
@@ -0,0 +1,81 @@
+"""
+Functions that manipulate with XML.
+"""
+
+
+import xml.sax.saxutils
+from xml.etree import ElementTree as ET
+
+
+def xml_escape(string):
+    """
+    Escape the string so it's safe to use in XML and xpath.
+    """
+    entities = {
+        '"': "&quot;",
+        "'": "&apos;",
+    }
+    if isinstance(string, bytes):
+        return xml.sax.saxutils.escape(string.decode("utf-8"), 
entities=entities).encode("utf-8")
+    return xml.sax.saxutils.escape(string, entities=entities)
+
+
+def xml_unescape(string):
+    """
+    Decode XML entities in the string.
+    """
+    entities = {
+        "&quot;": '"',
+        "&apos;": "'",
+    }
+    if isinstance(string, bytes):
+        return xml.sax.saxutils.unescape(string.decode("utf-8"), 
entities=entities).encode("utf-8")
+    return xml.sax.saxutils.unescape(string, entities=entities)
+
+
+def xml_strip_text(node):
+    """
+    Recursively strip inner text in nodes:
+    - if text contains only whitespaces
+    - if node contains child nodes
+    """
+    if node.text and not node.text.strip():
+        node.text = None
+    elif len(node) != 0:
+        node.text = None
+    for child in node:
+        xml_strip_text(child)
+
+
+def xml_indent_compat(elem, level=0):
+    """
+    XML indentation code for python < 3.9.
+    Source: http://effbot.org/zone/element-lib.htm#prettyprint
+    """
+    i = "\n" + level * "  "
+    if isinstance(elem, ET.ElementTree):
+        elem = elem.getroot()
+    if len(elem):
+        if not elem.text or not elem.text.strip():
+            elem.text = i + "  "
+        for e in elem:
+            xml_indent_compat(e, level + 1)
+            if not e.tail or not e.tail.strip():
+                e.tail = i + "  "
+        if not e.tail or not e.tail.strip():
+            e.tail = i
+    else:
+        if level and (not elem.tail or not elem.tail.strip()):
+            elem.tail = i
+
+
+def xml_indent(root):
+    """
+    Indent XML so it looks pretty after printing or saving to file.
+    """
+    if hasattr(ET, "indent"):
+        # ElementTree supports indent() in Python 3.9 and newer
+        xml_strip_text(root)
+        ET.indent(root)
+    else:
+        xml_indent_compat(root)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/tests/test_core.py 
new/osc-1.6.1/tests/test_core.py
--- old/osc-1.6.0/tests/test_core.py    2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/tests/test_core.py    2024-02-23 09:45:57.000000000 +0100
@@ -1,5 +1,7 @@
 import unittest
 
+from osc.core import makeurl
+from osc.core import UrlQueryArray
 from osc.core import parseRevisionOption
 from osc.oscerr import OscInvalidRevision
 
@@ -47,5 +49,94 @@
         self.assertRaises(OscInvalidRevision, parseRevisionOption, rev)
 
 
+class TestMakeurl(unittest.TestCase):
+    def test_basic(self):
+        url = makeurl("https://example.com/api/v1";, ["path", "to", 
"resource"], {"k1": "v1", "k2": ["v2", "v3"]})
+        self.assertEqual(url, 
"https://example.com/api/v1/path/to/resource?k1=v1&k2=v2&k2=v3";)
+
+    def test_array(self):
+        url = makeurl("https://example.com/api/v1";, ["path", "to", 
"resource"], {"k1": "v1", "k2": UrlQueryArray(["v2", "v3"])})
+        self.assertEqual(url, 
"https://example.com/api/v1/path/to/resource?k1=v1&k2%5B%5D=v2&k2%5B%5D=v3";)
+
+    def test_query_none(self):
+        url = makeurl("https://example.com/api/v1";, [], {"none": None})
+        self.assertEqual(url, "https://example.com/api/v1";)
+
+    def test_query_empty_list(self):
+        url = makeurl("https://example.com/api/v1";, [], {"empty_list": []})
+        self.assertEqual(url, "https://example.com/api/v1";)
+
+    def test_query_int(self):
+        url = makeurl("https://example.com/api/v1";, [], {"int": 1})
+        self.assertEqual(url, "https://example.com/api/v1?int=1";)
+
+    def test_query_bool(self):
+        url = makeurl("https://example.com/api/v1";, [], {"bool": True})
+        self.assertEqual(url, "https://example.com/api/v1?bool=1";)
+
+        url = makeurl("https://example.com/api/v1";, [], {"bool": False})
+        self.assertEqual(url, "https://example.com/api/v1?bool=0";)
+
+    def test_quote_path(self):
+        mapping = (
+            # (character, expected encoded character)
+            (" ", "%20"),
+            ("!", "%21"),
+            ('"', "%22"),
+            ("#", "%23"),
+            ("$", "%24"),
+            ("%", "%25"),
+            ("&", "%26"),
+            ("'", "%27"),
+            ("(", "%28"),
+            (")", "%29"),
+            ("*", "%2A"),
+            ("+", "%2B"),
+            (",", "%2C"),
+            ("/", "/"),
+            (":", ":"),  # %3A
+            (";", "%3B"),
+            ("=", "%3D"),
+            ("?", "%3F"),
+            ("@", "%40"),
+            ("[", "%5B"),
+            ("]", "%5D"),
+        )
+
+        for char, encoded_char in mapping:
+            url = makeurl("https://example.com/api/v1";, 
[f"PREFIX_{char}_SUFFIX"])
+            self.assertEqual(url, 
f"https://example.com/api/v1/PREFIX_{encoded_char}_SUFFIX";)
+
+    def test_quote_query(self):
+        mapping = (
+            # (character, expected encoded character)
+            (" ", "+"),
+            ("!", "%21"),
+            ('"', "%22"),
+            ("#", "%23"),
+            ("$", "%24"),
+            ("%", "%25"),
+            ("&", "%26"),
+            ("'", "%27"),
+            ("(", "%28"),
+            (")", "%29"),
+            ("*", "%2A"),
+            ("+", "%2B"),
+            (",", "%2C"),
+            ("/", "%2F"),
+            (":", "%3A"),
+            (";", "%3B"),
+            ("=", "%3D"),
+            ("?", "%3F"),
+            ("@", "%40"),
+            ("[", "%5B"),
+            ("]", "%5D"),
+        )
+
+        for char, encoded_char in mapping:
+            url = makeurl("https://example.com/api/v1";, [], {char: char})
+            self.assertEqual(url, 
f"https://example.com/api/v1?{encoded_char}={encoded_char}";)
+
+
 if __name__ == "__main__":
     unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.6.0/tests/test_update.py 
new/osc-1.6.1/tests/test_update.py
--- old/osc-1.6.0/tests/test_update.py  2024-01-25 09:49:14.000000000 +0100
+++ new/osc-1.6.1/tests/test_update.py  2024-02-23 09:45:57.000000000 +0100
@@ -193,8 +193,8 @@
 
     @GET('http://localhost/source/osctest/services?rev=latest', 
file='testUpdateServiceFilesAddDelete_filesremote')
     @GET('http://localhost/source/osctest/services/bigfile?rev=2', 
file='testUpdateServiceFilesAddDelete_bigfile')
-    @GET('http://localhost/source/osctest/services/_service%3Abar?rev=2', 
file='testUpdateServiceFilesAddDelete__service:bar')
-    @GET('http://localhost/source/osctest/services/_service%3Afoo?rev=2', 
file='testUpdateServiceFilesAddDelete__service:foo')
+    @GET('http://localhost/source/osctest/services/_service:bar?rev=2', 
file='testUpdateServiceFilesAddDelete__service:bar')
+    @GET('http://localhost/source/osctest/services/_service:foo?rev=2', 
file='testUpdateServiceFilesAddDelete__service:foo')
     @GET('http://localhost/source/osctest/services/_meta', file='meta.xml')
     def testUpdateAddDeleteServiceFiles(self):
         """update package with _service:* files"""

++++++ osc.dsc ++++++
--- /var/tmp/diff_new_pack.kd3hFl/_old  2024-02-27 22:48:46.999462668 +0100
+++ /var/tmp/diff_new_pack.kd3hFl/_new  2024-02-27 22:48:46.999462668 +0100
@@ -1,6 +1,6 @@
 Format: 1.0
 Source: osc
-Version: 1.6.0-0
+Version: 1.6.1-0
 Binary: osc
 Maintainer: Adrian Schroeter <adr...@suse.de>
 Architecture: any

Reply via email to