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

Reply via email to