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 = { - "\"": """, - "'": "'", - } - 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 = { - """: "\"", - "'": "'", - } - 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 = { + '"': """, + "'": "'", + } + 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 = { + """: '"', + "'": "'", + } + 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