Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package osc for openSUSE:Factory checked in at 2023-07-15 23:15:19 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/osc (Old) and /work/SRC/openSUSE:Factory/.osc.new.3193 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "osc" Sat Jul 15 23:15:19 2023 rev:179 rq:1098818 version:1.2.0 Changes: -------- --- /work/SRC/openSUSE:Factory/osc/osc.changes 2023-05-25 23:53:05.851808525 +0200 +++ /work/SRC/openSUSE:Factory/.osc.new.3193/osc.changes 2023-07-15 23:15:24.723603895 +0200 @@ -1,0 +2,21 @@ +Fri Jul 14 09:10:36 UTC 2023 - Daniel Mach <[email protected]> + +- 1.2.0 + - Command-line: + - Add 'repo' command and subcommands for managing repositories in project meta + - Extend 'browse' command to open requests in a web browser + - Add highlighting for 'osc diff' and similar commands + - Fix 'api' command to stream output to avoid running out of memory + - Fix printing utf-8 characters to stdout + - Connection: + - Fix ValueError: Cannot set verify_mode to CERT_NONE when check_hostname is enabled + - Authentication: + - Correctly handle passwords with utf-8 characters + - Library: + - Fix crash when submiting a SCM package which has no _link + - Fix local service execution of scmsync packages + - Detect target package by its full name, instead of assuming its origin is identical to the source package type + - Other: + - Spell openSUSE correctly + +------------------------------------------------------------------- Old: ---- osc-1.1.4.tar.gz New: ---- osc-1.2.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ osc.spec ++++++ --- /var/tmp/diff_new_pack.2bTLvJ/_old 2023-07-15 23:15:25.451608165 +0200 +++ /var/tmp/diff_new_pack.2bTLvJ/_new 2023-07-15 23:15:25.459608212 +0200 @@ -49,7 +49,7 @@ %endif Name: osc -Version: 1.1.4 +Version: 1.2.0 Release: 0 Summary: Command-line client for the Open Build Service License: GPL-2.0-or-later @@ -119,7 +119,7 @@ Provides: %{use_python_pkg}-osc %description -OpenSUSE Commander is a command-line client for the Open Build Service. +openSUSE Commander is a command-line client for the Open Build Service. See http://en.opensuse.org/openSUSE:OSC, as well as http://en.opensuse.org/openSUSE:Build_Service_Tutorial @@ -145,7 +145,7 @@ --function=get_parser \ --project-name=osc \ --prog=osc \ - --description="OpenSUSE Commander" \ + --description="openSUSE Commander" \ --author="Contributors to the osc project. See the project's GIT history for the complete list." \ --url="https://github.com/openSUSE/osc/" %endif ++++++ PKGBUILD ++++++ --- /var/tmp/diff_new_pack.2bTLvJ/_old 2023-07-15 23:15:25.487608376 +0200 +++ /var/tmp/diff_new_pack.2bTLvJ/_new 2023-07-15 23:15:25.491608400 +0200 @@ -1,5 +1,5 @@ pkgname=osc -pkgver=1.1.4 +pkgver=1.2.0 pkgrel=0 pkgdesc="Command-line client for the Open Build Service" arch=('x86_64') ++++++ debian.changelog ++++++ --- /var/tmp/diff_new_pack.2bTLvJ/_old 2023-07-15 23:15:25.531608634 +0200 +++ /var/tmp/diff_new_pack.2bTLvJ/_new 2023-07-15 23:15:25.539608681 +0200 @@ -1,4 +1,4 @@ -osc (1.1.4-0) unstable; urgency=low +osc (1.2.0-0) unstable; urgency=low * Placeholder ++++++ osc-1.1.4.tar.gz -> osc-1.2.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/NEWS new/osc-1.2.0/NEWS --- old/osc-1.1.4/NEWS 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/NEWS 2023-07-14 11:08:24.000000000 +0200 @@ -1,3 +1,21 @@ +- 1.2.0 + - Command-line: + - Add 'repo' command and subcommands for managing repositories in project meta + - Extend 'browse' command to open requests in a web browser + - Add highlighting for 'osc diff' and similar commands + - Fix 'api' command to stream output to avoid running out of memory + - Fix printing utf-8 characters to stdout + - Connection: + - Fix ValueError: Cannot set verify_mode to CERT_NONE when check_hostname is enabled + - Authentication: + - Correctly handle passwords with utf-8 characters + - Library: + - Fix crash when submiting a SCM package which has no _link + - Fix local service execution of scmsync packages + - Detect target package by its full name, instead of assuming its origin is identical to the source package type + - Other: + - Spell openSUSE correctly + - 1.1.4 - Command-line: - Change 'review list' command to display open requests (state: new, review, declined) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/README.md new/osc-1.2.0/README.md --- old/osc-1.1.4/README.md 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/README.md 2023-07-14 11:08:24.000000000 +0200 @@ -7,7 +7,7 @@ # openSUSE Commander -OpenSUSE Commander (osc) is a command-line interface to the +openSUSE Commander (osc) is a command-line interface to the [Open Build Service (OBS)](https://github.com/openSUSE/open-build-service/). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/__init__.py new/osc-1.2.0/osc/__init__.py --- old/osc-1.1.4/osc/__init__.py 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/osc/__init__.py 2023-07-14 11:08:24.000000000 +0200 @@ -13,7 +13,7 @@ from .util import git_version -__version__ = git_version.get_version('1.1.4') +__version__ = git_version.get_version('1.2.0') # vim: sw=4 et diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/_private/api.py new/osc-1.2.0/osc/_private/api.py --- old/osc-1.1.4/osc/_private/api.py 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/osc/_private/api.py 2023-07-14 11:08:24.000000000 +0200 @@ -64,6 +64,34 @@ return root +def put(apiurl, path, query=None, data=None): + """ + Send a PUT request to OBS. + + :param apiurl: OBS apiurl. + :type apiurl: str + :param path: URL path segments. + :type path: list(str) + :param query: URL query values. + :type query: dict(str, str) + :returns: Parsed XML root. + :rtype: xml.etree.ElementTree.Element + """ + from osc import connection as osc_connection + from osc import core as osc_core + + assert apiurl + assert path + + if not isinstance(path, (list, tuple)): + raise TypeError("Argument `path` expects a list of strings") + + url = osc_core.makeurl(apiurl, path, query) + with osc_connection.http_PUT(url, data=data) as f: + root = osc_core.ET.parse(f).getroot() + return root + + def _to_xpath(*args): """ Convert strings and dictionaries to xpath: @@ -139,6 +167,30 @@ return root.find(_to_xpath(*args)) +def group_child_nodes(node): + nodes = node[:] + result = [] + + while nodes: + # look at the tag of the first node + tag = nodes[0].tag + + # collect all nodes with the same tag and append them to the result + # then repeat the step for the next tag(s) + matches = [] + others = [] + for i in nodes: + if i.tag == tag: + matches.append(i) + else: + others.append(i) + + result += matches + nodes = others + + node[:] = result + + def write_xml_node_to_file(node, path, indent=True): """ Write a XML node to a file. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/_private/project.py new/osc-1.2.0/osc/_private/project.py --- old/osc-1.1.4/osc/_private/project.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/osc/_private/project.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,154 @@ +from . import api +from .api import ET +from .. import core as osc_core +from .. import oscerr + + +class APIXMLBase: + def __init__(self, xml_root, apiurl=None): + self.root = xml_root + self.apiurl = apiurl + + def to_bytes(self): + ET.indent(self.root, space=" ", level=0) + return ET.tostring(self.root, encoding="utf-8") + + def to_string(self): + return self.to_bytes().decode("utf-8") + + +class ProjectMeta(APIXMLBase): + @classmethod + def from_api(cls, apiurl, project): + url_path = ["source", project, "_meta"] + root = api.get(apiurl, url_path) + obj = cls(root, apiurl=apiurl) + return obj + + def to_api(self, apiurl, project): + url_path = ["source", project, "_meta"] + api.put(apiurl, url_path, data=self.to_bytes()) + + def repository_list(self): + result = [] + repo_nodes = api.find_nodes(self.root, "project", "repository") + for repo_node in repo_nodes: + arch_nodes = api.find_nodes(repo_node, "repository", "arch") + path_nodes = api.find_nodes(repo_node, "repository", "path") + repo = { + "name": repo_node.attrib["name"], + "archs": [i.text.strip() for i in arch_nodes], + "paths": [i.attrib.copy() for i in path_nodes], + } + result.append(repo) + return result + + def repository_add(self, name, arches, paths): + node = api.find_node(self.root, "project") + + existing = api.find_node(self.root, "project", "repository", {"name": name}) + if existing: + raise oscerr.OscValueError(f"Repository '{name}' already exists in project meta") + + repo_node = ET.SubElement(node, "repository", attrib={"name": name}) + + for path_data in paths: + ET.SubElement(repo_node, "path", attrib={ + "project": path_data["project"], + "repository": path_data["repository"], + }) + + for arch in arches: + arch_node = ET.SubElement(repo_node, "arch") + arch_node.text = arch + + api.group_child_nodes(repo_node) + api.group_child_nodes(node) + + def repository_remove(self, name): + repo_node = api.find_node(self.root, "project", "repository", {"name": name}) + if repo_node is None: + return + self.root.remove(repo_node) + + def publish_add_disable_repository(self, name: str): + publish_node = api.find_node(self.root, "project", "publish") + if publish_node is None: + project_node = api.find_node(self.root, "project") + publish_node = ET.SubElement(project_node, "publish") + else: + disable_node = api.find_node(publish_node, "publish", "disable", {"repository": name}) + if disable_node is not None: + return + + ET.SubElement(publish_node, "disable", attrib={"repository": name}) + api.group_child_nodes(publish_node) + + def publish_remove_disable_repository(self, name: str): + publish_node = api.find_node(self.root, "project", "publish") + if publish_node is None: + return + + disable_node = api.find_node(publish_node, "publish", "disable", {"repository": name}) + if disable_node is not None: + publish_node.remove(disable_node) + + if len(publish_node) == 0: + self.root.remove(publish_node) + + REPOSITORY_FLAGS_TEMPLATE = { + "build": None, + "debuginfo": None, + "publish": None, + "useforbuild": None, + } + + def _update_repository_flags(self, repository_flags, xml_root): + """ + Update `repository_flags` with data from the `xml_root`. + """ + for flag in self.REPOSITORY_FLAGS_TEMPLATE: + flag_node = xml_root.find(flag) + if flag_node is None: + continue + for node in flag_node: + action = node.tag + repo = node.get("repository") + arch = node.get("arch") + for (entry_repo, entry_arch), entry_data in repository_flags.items(): + match = False + if (repo, arch) == (entry_repo, entry_arch): + # apply to matching repository and architecture + match = True + elif repo == entry_repo and not arch: + # apply to all matching repositories + match = True + elif not repo and arch == entry_arch: + # apply to all matching architectures + match = True + elif not repo and not arch: + # apply to everything + match = True + if match: + entry_data[flag] = True if action == "enable" else False + + def resolve_repository_flags(self, package=None): + """ + Resolve the `build`, `debuginfo`, `publish` and `useforbuild` flags + and return their values for each repository and build arch. + + :returns: {(repo_name, repo_buildarch): {flag_name: bool} for all available repos + """ + result = {} + # TODO: avoid calling get_repos_of_project(), use self.root instead + for repo in osc_core.get_repos_of_project(self.apiurl, self.root.attrib["name"]): + result[(repo.name, repo.arch)] = self.REPOSITORY_FLAGS_TEMPLATE.copy() + + self._update_repository_flags(result, self.root) + + if package: + m = osc_core.show_package_meta(self.apiurl, self.root.attrib["name"], package) + root = ET.fromstring(b''.join(m)) + self._update_repository_flags(result, root) + + return result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/build.py new/osc-1.2.0/osc/build.py --- old/osc-1.1.4/osc/build.py 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/osc/build.py 2023-07-14 11:08:24.000000000 +0200 @@ -1367,7 +1367,7 @@ for i in bi.deps: if i.hdrmd5: - if not i.name.startswith('container:') and i.pacsuffix != 'rpm': + if not i.name.startswith('container:') and not i.fullfilename.endswith(".rpm"): continue if i.name.startswith('container:'): hdrmd5 = dgst(i.fullfilename) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/commandline.py new/osc-1.2.0/osc/commandline.py --- old/osc-1.1.4/osc/commandline.py 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/osc/commandline.py 2023-07-14 11:08:24.000000000 +0200 @@ -1683,10 +1683,10 @@ url = makeurl(apiurl, url_path, query) f = http_POST(url) while True: - buf = f.read(16384) - if not buf: + data = f.read(16384) + if not data: break - sys.stdout.write(decode_it(buf)) + sys.stdout.buffer.write(data) elif opts.delete: print("Delete token") @@ -1713,7 +1713,7 @@ # just list token url = makeurl(apiurl, url_path) for data in streamfile(url, http_GET): - sys.stdout.write(decode_it(data)) + sys.stdout.buffer.write(data) @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE', help='affect only a given attribute') @@ -1985,7 +1985,7 @@ d = '<attributes><attribute namespace=\'%s\' name=\'%s\' >%s</attribute></attributes>' % (aname[0], aname[1], values) url = makeurl(apiurl, attributepath) for data in streamfile(url, http_POST, data=d): - sys.stdout.write(decode_it(data)) + sys.stdout.buffer.write(data) # upload file if opts.file: @@ -2052,7 +2052,7 @@ attributepath.append(opts.attribute) u = makeurl(apiurl, attributepath) for data in streamfile(u, http_DELETE): - sys.stdout.write(decode_it(data)) + sys.stdout.buffer.write(data) else: raise oscerr.WrongOptions('The --delete switch is only for pattern metadata or attributes.') @@ -2340,7 +2340,7 @@ rdiff = b'' if opts.diff: - run_pager(rdiff) + run_pager(highlight_diff(rdiff)) return if rdiff is not None: rdiff = decode_it(rdiff) @@ -3399,7 +3399,7 @@ action.tgt_project.encode(), action.tgt_package.encode()) diff += submit_action_diff(apiurl, action) diff += b'\n\n' - run_pager(diff, tmp_suffix='') + run_pager(highlight_diff(diff), tmp_suffix="") # checkout elif cmd in ('checkout', 'co'): @@ -4620,8 +4620,21 @@ print("diff working copy against last committed version\n") else: print("diff committed package against linked revision %s\n" % baserev) - run_pager(server_diff(self.get_api_url(), linkinfo.get('project'), linkinfo.get('package'), baserev, - args[0], args[1], linkinfo.get('lsrcmd5'), not opts.plain, opts.missingok)) + run_pager( + highlight_diff( + server_diff( + self.get_api_url(), + linkinfo.get("project"), + linkinfo.get("package"), + baserev, + args[0], + args[1], + linkinfo.get("lsrcmd5"), + not opts.plain, + opts.missingok, + ) + ) + ) return if opts.change: @@ -4655,7 +4668,7 @@ diff += server_diff_noex(pac.apiurl, pac.prjname, pac.name, rev1, pac.prjname, pac.name, rev2, not opts.plain, opts.missingok, opts.meta, not opts.unexpand, files=files) - run_pager(diff) + run_pager(highlight_diff(diff)) @cmdln.option('--issues-only', action='store_true', help='show only issues in diff') @@ -4746,7 +4759,7 @@ if opts.issues_only: print(decode_it(rdiff)) else: - run_pager(rdiff) + run_pager(highlight_diff(rdiff)) def _pdiff_raise_non_existing_package(self, project, package, msg=None): raise oscerr.PackageMissing(project, package, msg or '%s/%s does not exist.' % (project, package)) @@ -4893,7 +4906,7 @@ rdiff = server_diff(apiurl, parent_project, parent_package, None, project, package, None, unified=unified, missingok=noparentok) - run_pager(rdiff) + run_pager(highlight_diff(rdiff)) def _get_branch_parent(self, prj): m = re.match('^home:[^:]+:branches:(.+)', prj) @@ -5099,20 +5112,25 @@ usage: osc browse [PROJECT [PACKAGE]] + osc browse [REQUEST_ID] """ - apiurl = self.get_api_url() - args = list(args) - project, package = pop_project_package_from_args( - args, default_project=".", default_package=".", package_is_optional=True - ) - ensure_no_remaining_args(args) - + apiurl = self.get_api_url() obs_url = _private.get_configuration_value(apiurl, "obs_url") - if package: - url = f"{obs_url}/package/show/{project}/{package}" + + if len(args) == 1 and args[0].isnumeric(): + reqid = args.pop(0) + url = f"{obs_url}/request/show/{reqid}" else: - url = f"{obs_url}/project/show/{project}" + project, package = pop_project_package_from_args( + args, default_project=".", default_package=".", package_is_optional=True + ) + if package: + url = f"{obs_url}/package/show/{project}/{package}" + else: + url = f"{obs_url}/project/show/{project}" + + ensure_no_remaining_args(args) run_external('xdg-open', url) @@ -6374,11 +6392,11 @@ f = open(logfile, 'rb') f.seek(offset) data = f.read(BUFSIZE) - data = decode_it(data) while len(data): if opts.strip_time or conf.config['buildlog_strip_time']: + # FIXME: this is not working when the time is split between 2 chunks data = buildlog_strip_time(data) - sys.stdout.write(decode_it(data)) + sys.stdout.buffer.write(data) data = f.read(BUFSIZE) f.close() @@ -8695,20 +8713,22 @@ data=opts.data, file=opts.file, headers=opts.headers) - out = r.read() if opts.edit: + # to edit the output, we need to read all of it + # it's going to run ouf of memory if the data is too big + out = r.read() text = edit_text(out) r = http_request("PUT", url, data=text, headers=opts.headers) - out = r.read() - if isinstance(out, str): - sys.stdout.write(out) - else: - sys.stdout.buffer.write(out) + while True: + data = r.read(8192) + if not data: + break + sys.stdout.buffer.write(data) @cmdln.option('-b', '--bugowner-only', action='store_true', help='Show only the bugowner') @@ -9554,10 +9574,10 @@ raise while True: - buf = f.read(16384) - if not buf: + data = f.read(16384) + if not data: break - sys.stdout.write(decode_it(buf)) + sys.stdout.buffer.write(data) @cmdln.option('-m', '--message', help='add MESSAGE to changes (do not open an editor)') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/commands/repo.py new/osc-1.2.0/osc/commands/repo.py --- old/osc-1.1.4/osc/commands/repo.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/osc/commands/repo.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,11 @@ +import osc.commandline + + +class RepoCommand(osc.commandline.OscCommand): + """ + Manage repositories in project meta + """ + name = "repo" + + def run(self, args): + pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/commands/repo_add.py new/osc-1.2.0/osc/commands/repo_add.py --- old/osc-1.1.4/osc/commands/repo_add.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/osc/commands/repo_add.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,82 @@ +import difflib + +import osc.commandline +from .. import oscerr +from .._private.project import ProjectMeta +from ..core import raw_input + + +class RepoAddCommand(osc.commandline.OscCommand): + """ + Add a repository to project meta + """ + + name = "add" + parent = "RepoCommand" + + def init_arguments(self): + self.add_argument( + "project", + help="Name of the project", + ) + self.add_argument( + "--repo", + metavar="NAME", + required=True, + help="Name of the repository we're adding", + ) + self.add_argument( + "--arch", + dest="arches", + metavar="[ARCH]", + action="append", + required=True, + help="Architecture of the repository. Can be specified multiple times.", + ) + self.add_argument( + "--path", + dest="paths", + metavar="[PROJECT/REPO]", + action="append", + required=True, + help="Path associated to the repository. Format is PROJECT/REPO. Can be specified multiple times.", + ) + self.add_argument( + "--disable-publish", + action="store_true", + default=False, + help="Disable publishing the added repository", + ) + self.add_argument( + "--yes", + action="store_true", + help="Proceed without asking", + ) + + def run(self, args): + paths = [] + for path in args.paths: + if "/" not in path: + self.parser.error(f"Invalid path (expected format is PROJECT/REPO): {path}") + project, repo = path.split("/") + paths.append({"project": project, "repository": repo}) + + meta = ProjectMeta.from_api(args.apiurl, args.project) + old_meta = meta.to_string().splitlines() + + meta.repository_add(args.repo, args.arches, paths) + if args.disable_publish: + meta.publish_add_disable_repository(args.repo) + + new_meta = meta.to_string().splitlines() + diff = difflib.unified_diff(old_meta, new_meta, fromfile="old", tofile="new") + print("\n".join(diff)) + + if not args.yes: + print() + print(f"You're changing meta of project '{args.project}'") + reply = raw_input("Do you want to apply the changes? [y/N] ").lower() + if reply != "y": + raise oscerr.UserAbort() + + meta.to_api(args.apiurl, args.project) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/commands/repo_list.py new/osc-1.2.0/osc/commands/repo_list.py --- old/osc-1.1.4/osc/commands/repo_list.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/osc/commands/repo_list.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,51 @@ +import osc.commandline +from ..output import KeyValueTable +from .._private.project import ProjectMeta + + +class RepoListCommand(osc.commandline.OscCommand): + """ + List repositories in project meta + """ + + name = "list" + aliases = ["ls"] + parent = "RepoCommand" + + def init_arguments(self): + self.add_argument( + "project", + help="Name of the project", + ) + + def run(self, args): + meta = ProjectMeta.from_api(args.apiurl, args.project) + + repo_flags = meta.resolve_repository_flags() + flag_map = {} + for (repo_name, arch), data in repo_flags.items(): + for flag_name, flag_value in data.items(): + if flag_value is None: + continue + action = "enable" if flag_value else "disable" + flag_map.setdefault(repo_name, {}).setdefault(flag_name, {}).setdefault(action, []).append(arch) + + table = KeyValueTable() + for repo in meta.repository_list(): + table.add("Repository", repo["name"], color="bold") + table.add("Architectures", ", ".join(repo["archs"])) + if repo["paths"]: + paths = [f"{path['project']}/{path['repository']}" for path in repo["paths"]] + table.add("Paths", paths) + + if repo["name"] in flag_map: + table.add("Flags", None) + for flag_name in flag_map[repo["name"]]: + lines = [] + for action, archs in flag_map[repo["name"]][flag_name].items(): + lines.append(f"{action + ':':<8s} {', '.join(archs)}") + lines.sort() + table.add(flag_name, lines, indent=4) + + table.newline() + print(str(table)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/commands/repo_remove.py new/osc-1.2.0/osc/commands/repo_remove.py --- old/osc-1.1.4/osc/commands/repo_remove.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/osc/commands/repo_remove.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,55 @@ +import difflib + +import osc.commandline +from .. import oscerr +from .._private.project import ProjectMeta +from ..core import raw_input + + +class RepoRemoveCommand(osc.commandline.OscCommand): + """ + Remove repositories from project meta + """ + + name = "remove" + aliases = ["rm"] + parent = "RepoCommand" + + def init_arguments(self): + self.add_argument( + "project", + help="Name of the project", + ) + self.add_argument( + "--repo", + metavar="[NAME]", + action="append", + required=True, + help="Name of the repository we're removing. Can be specified multiple times.", + ) + self.add_argument( + "--yes", + action="store_true", + help="Proceed without asking", + ) + + def run(self, args): + meta = ProjectMeta.from_api(args.apiurl, args.project) + old_meta = meta.to_string().splitlines() + + for repo in args.repo: + meta.repository_remove(repo) + meta.publish_remove_disable_repository(repo) + + new_meta = meta.to_string().splitlines() + diff = difflib.unified_diff(old_meta, new_meta, fromfile="old", tofile="new") + print("\n".join(diff)) + + if not args.yes: + print() + print(f"You're changing meta of project '{args.project}'") + reply = raw_input("Do you want to apply the changes? [y/N] ").lower() + if reply != "y": + raise oscerr.UserAbort() + + meta.to_api(args.apiurl, args.project) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/connection.py new/osc-1.2.0/osc/connection.py --- old/osc-1.1.4/osc/connection.py 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/osc/connection.py 2023-07-14 11:08:24.000000000 +0200 @@ -104,10 +104,14 @@ proxy_url = f"{proxy_purl.scheme}://{proxy_purl.host}" proxy_headers = urllib3.make_headers( - proxy_basic_auth=proxy_purl.auth, user_agent=f"osc/{__version__}", ) + proxy_basic_auth = urllib.parse.unquote(proxy_purl.auth) + proxy_basic_auth = proxy_basic_auth.encode("utf-8") + proxy_basic_auth = base64.b64encode(proxy_basic_auth).decode() + proxy_headers["Proxy-Authorization"] = f"Basic {proxy_basic_auth:s}" + manager = urllib3.ProxyManager(proxy_url, proxy_headers=proxy_headers) return manager @@ -252,9 +256,19 @@ if purl.scheme == "https": ssl_context = oscssl.create_ssl_context() ssl_context.load_default_certs() + pool_kwargs["ssl_context"] = ssl_context # turn cert verification off if sslcertck = 0 + + # urllib3 v1 pool_kwargs["cert_reqs"] = "CERT_REQUIRED" if options["sslcertck"] else "CERT_NONE" - pool_kwargs["ssl_context"] = ssl_context + + # urllib3 v2 + if options["sslcertck"]: + ssl_context.check_hostname = True + ssl_context.verify_mode = ssl.CERT_REQUIRED + else: + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE if purl.scheme == "http" and HTTP_PROXY_MANAGER and not urllib.request.proxy_bypass(url): # connection through HTTP proxy @@ -539,7 +553,12 @@ return False if not self.user or not self.password: return False - request_headers.update(urllib3.make_headers(basic_auth=f"{self.user}:{self.password}")) + + basic_auth = f"{self.user:s}:{self.password:s}" + basic_auth = basic_auth.encode("utf-8") + basic_auth = base64.b64encode(basic_auth).decode() + request_headers["Authorization"] = f"Basic {basic_auth:s}" + return True def process_response(self, url, request_headers, response): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/core.py new/osc-1.2.0/osc/core.py --- old/osc-1.1.4/osc/core.py 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/osc/core.py 2023-07-14 11:08:24.000000000 +0200 @@ -1937,8 +1937,8 @@ self.linkrepair = None self.rev = None self.srcmd5 = None - self.linkinfo = None - self.serviceinfo = None + self.linkinfo = Linkinfo() + self.serviceinfo = DirectoryServiceinfo() self.size_limit = None self.meta = None self.excluded = [] @@ -2630,14 +2630,13 @@ os.chdir(self.absdir) # e.g. /usr/lib/obs/service/verify_file fails if not inside the project dir. si = Serviceinfo() if os.path.exists('_service'): - if self.filenamelist.count('_service') or self.filenamelist_unvers.count('_service'): - try: - service = ET.parse(os.path.join(self.absdir, '_service')).getroot() - except ET.ParseError as v: - line, column = v.position - print('XML error in _service file on line %s, column %s' % (line, column)) - sys.exit(1) - si.read(service) + try: + service = ET.parse(os.path.join(self.absdir, '_service')).getroot() + except ET.ParseError as v: + line, column = v.position + print('XML error in _service file on line %s, column %s' % (line, column)) + sys.exit(1) + si.read(service) si.getProjectGlobalServices(self.apiurl, self.prjname, self.name) r = si.execute(self.absdir, mode, singleservice, verbose) os.chdir(curdir) @@ -4369,6 +4368,24 @@ return 'more' +def format_diff_line(line): + if line.startswith(b"+++") or line.startswith(b"---") or line.startswith(b"Index:"): + line = b"\x1b[1m" + line + b"\x1b[0m" + elif line.startswith(b"+"): + line = b"\x1b[32m" + line + b"\x1b[0m" + elif line.startswith(b"-"): + line = b"\x1b[31m" + line + b"\x1b[0m" + elif line.startswith(b"@"): + line = b"\x1b[96m" + line + b"\x1b[0m" + return line + + +def highlight_diff(diff): + if sys.stdout.isatty(): + diff = b"\n".join((format_diff_line(line) for line in diff.split(b"\n"))) + return diff + + def run_pager(message, tmp_suffix=''): if not message: return @@ -6925,13 +6942,7 @@ def print_data(data, strip_time=False): if strip_time: data = buildlog_strip_time(data) - # hmm calling decode_it is a bit problematic because data might begin - # or end with an, for instance, incomplete utf-8 sequence - sys.stdout.write(decode_it(data.translate(all_bytes, remove_bytes))) - - # to protect us against control characters - all_bytes = bytes.maketrans(b'', b'') - remove_bytes = all_bytes[:8] + all_bytes[14:32] # accept tabs and newlines + sys.stdout.buffer.write(data) query = {'nostream': '1', 'start': '%s' % offset} if last: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/credentials.py new/osc-1.2.0/osc/credentials.py --- old/osc-1.1.4/osc/credentials.py 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/osc/credentials.py 2023-07-14 11:08:24.000000000 +0200 @@ -36,6 +36,11 @@ self._password = password return self._password + def __format__(self, format_spec): + if format_spec.endswith("s"): + return f"{self.__str__():{format_spec}}" + return super().__format__(format_spec) + def __len__(self): return len(str(self)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/output/__init__.py new/osc-1.2.0/osc/output/__init__.py --- old/osc-1.1.4/osc/output/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/osc/output/__init__.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,4 @@ +from .key_value_table import KeyValueTable +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.1.4/osc/output/key_value_table.py new/osc-1.2.0/osc/output/key_value_table.py --- old/osc-1.1.4/osc/output/key_value_table.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/osc/output/key_value_table.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,78 @@ +from . import tty +from . import widechar + + +class KeyValueTable: + class NewLine: + pass + + def __init__(self): + self.rows = [] + + def add(self, key, value, color=None, key_color=None, indent=0): + if value is None: + lines = [] + elif isinstance(value, (list, tuple)): + lines = value[:] + else: + lines = value.splitlines() + + if not lines: + lines = [""] + + # add the first line with the key + self.rows.append((key, lines[0], color, key_color, indent)) + + # then add the continuation lines without the key + for line in lines[1:]: + self.rows.append(("", line, color, key_color, 0)) + + def newline(self): + self.rows.append((self.NewLine, None, None, None, 0)) + + def __str__(self): + if not self.rows: + return "" + + col1_width = max([widechar.wc_width(key) + indent for key, _, _, _, indent in self.rows if key != self.NewLine]) + result = [] + skip = False + for row_num in range(len(self.rows)): + if skip: + skip = False + continue + + key, value, color, key_color, indent = self.rows[row_num] + + if key == self.NewLine: + result.append("") + continue + + next_indent = 0 # fake value + if not value and row_num < len(self.rows) - 1: + # let's peek if there's a continuation line we could merge instead of the blank value + next_key, next_value, next_color, next_key_color, next_indent = self.rows[row_num + 1] + if not next_key: + value = next_value + color = next_color + key_color = next_key_color + row_num += 1 + skip = True + + line = indent * " " + + if not value and next_indent > 0: + # no value, the key represents a section followed by indented keys -> skip ljust() and " : " separator + line += tty.colorize(key, key_color) + else: + line += tty.colorize(widechar.wc_ljust(key, col1_width - indent), key_color) + if not key: + # continuation line without a key -> skip " : " separator + line += " " + else: + line += " : " + line += tty.colorize(value, color) + + result.append(line) + + return "\n".join(result) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/output/tty.py new/osc-1.2.0/osc/output/tty.py --- old/osc-1.1.4/osc/output/tty.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/osc/output/tty.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,38 @@ +import os +import sys + + +IS_INTERACTIVE = os.isatty(sys.stdout.fileno()) + + +ESCAPE_CODES = { + "reset": "\033[0m", + "bold": "\033[1m", + "underline": "\033[4m", + "black": "\033[30m", + "red": "\033[31m", + "green": "\033[32m", + "yellow": "\033[33m", + "blue": "\033[34m", + "magenta": "\033[35m", + "cyan": "\033[36m", + "white": "\033[37m", +} + + +def colorize(text, color): + """ + Colorize `text` if the `color` is specified and we're running in an interactive terminal. + """ + if not IS_INTERACTIVE: + return text + + if not color: + return text + + result = "" + for i in color.split(","): + result += ESCAPE_CODES[i] + result += text + result += ESCAPE_CODES["reset"] + return result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/output/widechar.py new/osc-1.2.0/osc/output/widechar.py --- old/osc-1.1.4/osc/output/widechar.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/osc/output/widechar.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,22 @@ +import unicodedata + + +def wc_width(text): + result = 0 + for char in text: + if unicodedata.east_asian_width(char) in ("F", "W"): + result += 2 + else: + result += 1 + return result + + +def wc_ljust(text, width, fillchar=" "): + text_width = wc_width(text) + fill_width = wc_width(fillchar) + + while text_width + fill_width <= width: + text += fillchar + text_width += fill_width + + return text diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/osc/util/git_version.py new/osc-1.2.0/osc/util/git_version.py --- old/osc-1.1.4/osc/util/git_version.py 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/osc/util/git_version.py 2023-07-14 11:08:24.000000000 +0200 @@ -9,7 +9,7 @@ """ # the `version` variable contents get substituted during `git archive` # it requires adding this to .gitattributes: <path to this file> export-subst - version = "1.1.4" + version = "1.2.0" if version.startswith(("$", "%")): # "$": version hasn't been substituted during `git archive` # "%": "Format:" and "$" characters get removed from the version string (a GitHub bug?) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/setup.cfg new/osc-1.2.0/setup.cfg --- old/osc-1.1.4/setup.cfg 2023-05-24 08:59:45.000000000 +0200 +++ new/osc-1.2.0/setup.cfg 2023-07-14 11:08:24.000000000 +0200 @@ -35,6 +35,7 @@ osc osc._private osc.commands + osc.output osc.util install_requires = cryptography diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.1.4/tests/test_output.py new/osc-1.2.0/tests/test_output.py --- old/osc-1.1.4/tests/test_output.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.2.0/tests/test_output.py 2023-07-14 11:08:24.000000000 +0200 @@ -0,0 +1,71 @@ +import unittest + +from osc.output import KeyValueTable + + +class TestKeyValueTable(unittest.TestCase): + def test_empty(self): + t = KeyValueTable() + self.assertEqual(str(t), "") + + def test_simple(self): + t = KeyValueTable() + t.add("Key", "Value") + t.add("FooBar", "Text") + + expected = """ +Key : Value +FooBar : Text +""".strip() + self.assertEqual(str(t), expected) + + def test_newline(self): + t = KeyValueTable() + t.add("Key", "Value") + t.newline() + t.add("FooBar", "Text") + + expected = """ +Key : Value + +FooBar : Text +""".strip() + self.assertEqual(str(t), expected) + + def test_continuation(self): + t = KeyValueTable() + t.add("Key", ["Value1", "Value2"]) + + expected = """ +Key : Value1 + Value2 +""".strip() + self.assertEqual(str(t), expected) + + def test_section(self): + t = KeyValueTable() + t.add("Section", None) + t.add("Key", "Value", indent=4) + t.add("FooBar", "Text", indent=4) + + expected = """ +Section + Key : Value + FooBar : Text +""".strip() + self.assertEqual(str(t), expected) + + def test_wide_chars(self): + t = KeyValueTable() + t.add("Key", "Value") + t.add("ððð", "Value") + + expected = """ +Key : Value +ððð : Value +""".strip() + self.assertEqual(str(t), expected) + + +if __name__ == "__main__": + unittest.main() ++++++ osc.dsc ++++++ --- /var/tmp/diff_new_pack.2bTLvJ/_old 2023-07-15 23:15:25.911610863 +0200 +++ /var/tmp/diff_new_pack.2bTLvJ/_new 2023-07-15 23:15:25.915610886 +0200 @@ -1,6 +1,6 @@ Format: 1.0 Source: osc -Version: 1.1.4-0 +Version: 1.2.0-0 Binary: osc Maintainer: Adrian Schroeter <[email protected]> Architecture: any
