Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package osc for openSUSE:Factory checked in 
at 2026-07-01 17:12:25
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/osc (Old)
 and      /work/SRC/openSUSE:Factory/.osc.new.11887 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "osc"

Wed Jul  1 17:12:25 2026 rev:227 rq:1362992 version:1.27.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/osc/osc.changes  2026-06-04 18:58:06.022251452 
+0200
+++ /work/SRC/openSUSE:Factory/.osc.new.11887/osc.changes       2026-07-01 
17:12:31.303474578 +0200
@@ -1,0 +2,16 @@
+Wed Jul  1 11:26:16 UTC 2026 - Daniel Mach <[email protected]>
+
+- 1.27.2
+  - Command-line:
+    - Extend 'osc maintained' to also display maintained branches in git
+    - Add 'mergeable' field to the output of 'git-obs pr get' and other places 
using the same output format
+    - Add 'osc build' --buildinfo and --buildinfo-debug options
+    - Fix 'osc buildinfo --alternative-project' in git checkouts
+    - Fix superseding in 'osc submitrequest' command to be resilient to 403 
HTTP errors
+    - Display a descriptive error message when token is not specified in 
git-obs
+  - Library:
+    - Fix Manifest.get_package_paths_bare_git() by using normpath() in path 
comparison
+    - Enable allow_maintainer_edit setting, src.opensuse.org is now on gitea 
1.26.2
+    - Make 'osc up' and possibly other operations more robust by ignoring 
FileNotFoundError exception during deletions
+
+-------------------------------------------------------------------

Old:
----
  osc-1.27.1.tar.gz

New:
----
  osc-1.27.2.tar.gz

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

Other differences:
------------------
++++++ osc.spec ++++++
--- /var/tmp/diff_new_pack.981h3p/_old  2026-07-01 17:12:41.555827920 +0200
+++ /var/tmp/diff_new_pack.981h3p/_new  2026-07-01 17:12:41.555827920 +0200
@@ -80,7 +80,7 @@
 %endif
 
 Name:           osc
-Version:        1.27.1
+Version:        1.27.2
 Release:        0
 Summary:        Command-line client for the Open Build Service
 License:        GPL-2.0-or-later

++++++ PKGBUILD ++++++
--- /var/tmp/diff_new_pack.981h3p/_old  2026-07-01 17:12:41.635830677 +0200
+++ /var/tmp/diff_new_pack.981h3p/_new  2026-07-01 17:12:41.647831091 +0200
@@ -1,5 +1,5 @@
 pkgname=osc
-pkgver=1.27.1
+pkgver=1.27.2
 pkgrel=0
 pkgdesc="Command-line client for the Open Build Service"
 arch=('x86_64')

++++++ debian.changelog ++++++
--- /var/tmp/diff_new_pack.981h3p/_old  2026-07-01 17:12:41.747834538 +0200
+++ /var/tmp/diff_new_pack.981h3p/_new  2026-07-01 17:12:41.759834951 +0200
@@ -1,4 +1,4 @@
-osc (1.27.1-0) unstable; urgency=low
+osc (1.27.2-0) unstable; urgency=low
 
   * Placeholder
 

++++++ osc-1.27.1.tar.gz -> osc-1.27.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/NEWS new/osc-1.27.2/NEWS
--- old/osc-1.27.1/NEWS 2026-06-04 11:56:02.000000000 +0200
+++ new/osc-1.27.2/NEWS 2026-07-01 13:24:02.000000000 +0200
@@ -1,3 +1,16 @@
+- 1.27.2
+  - Command-line:
+    - Extend 'osc maintained' to also display maintained branches in git
+    - Add 'mergeable' field to the output of 'git-obs pr get' and other places 
using the same output format
+    - Add 'osc build' --buildinfo and --buildinfo-debug options
+    - Fix 'osc buildinfo --alternative-project' in git checkouts
+    - Fix superseding in 'osc submitrequest' command to be resilient to 403 
HTTP errors
+    - Display a descriptive error message when token is not specified in 
git-obs
+  - Library:
+    - Fix Manifest.get_package_paths_bare_git() by using normpath() in path 
comparison
+    - Enable allow_maintainer_edit setting, src.opensuse.org is now on gitea 
1.26.2
+    - Make 'osc up' and possibly other operations more robust by ignoring 
FileNotFoundError exception during deletions
+
 - 1.27.1
   - Command-line:
     - Fix 'osc maintaner' not to error out before it prints maintainers in git
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/__init__.py 
new/osc-1.27.2/osc/__init__.py
--- old/osc-1.27.1/osc/__init__.py      2026-06-04 11:56:02.000000000 +0200
+++ new/osc-1.27.2/osc/__init__.py      2026-07-01 13:24:02.000000000 +0200
@@ -13,7 +13,7 @@
 
 
 from .util import git_version
-__version__ = git_version.get_version('1.27.1')
+__version__ = git_version.get_version('1.27.2')
 
 
 # vim: sw=4 et
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/build.py new/osc-1.27.2/osc/build.py
--- old/osc-1.27.1/osc/build.py 2026-06-04 11:56:02.000000000 +0200
+++ new/osc-1.27.2/osc/build.py 2026-07-01 13:24:02.000000000 +0200
@@ -299,6 +299,7 @@
             if error.startswith('unresolvable: '):
                 sys.stderr.write('unresolvable: ')
                 sys.stderr.write('\n     '.join(error[14:].split(',')))
+                sys.stderr.write('\nHint: use osc build with --buildinfo-debug 
for more info')
             else:
                 sys.stderr.write(error)
             sys.stderr.write('\n')
@@ -1150,7 +1151,11 @@
                                               repo,
                                               arch,
                                               specfile=build_descr_data,
-                                              addlist=extra_pkgs))
+                                              addlist=extra_pkgs,
+                                              debug=opts.buildinfo_debug))
+            if opts.buildinfo or opts.buildinfo_debug:
+                print(bi_text)
+                sys.exit(0)
             if not bi_file:
                 bi_file = open(bi_filename, 'w')
             # maybe we should check for errors before saving the file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/commandline.py 
new/osc-1.27.2/osc/commandline.py
--- old/osc-1.27.1/osc/commandline.py   2026-06-04 11:56:02.000000000 +0200
+++ new/osc-1.27.2/osc/commandline.py   2026-07-01 13:24:02.000000000 +0200
@@ -693,6 +693,8 @@
     raise oscerr.WrongArgs(f"Unexpected args: {args_str}")
 
 
+
+
 class Osc(cmdln.Cmdln):
     """
     openSUSE commander is a command-line interface to the Open Build Service.
@@ -2143,8 +2145,7 @@
 
             if len(myreqs) > 0:
                 for req in myreqs:
-                    change_request_state(apiurl, str(req), 'superseded',
-                                         f'superseded by {sr_ids[0]}', 
sr_ids[0])
+                    change_request_state(apiurl, str(req), 'superseded', 
f'superseded by {sr_ids[0]}', sr_ids[0], can_fail=True)
 
             sys.exit('Successfully finished')
 
@@ -2300,8 +2301,7 @@
 
         if supersede_existing:
             for req in reqs:
-                change_request_state(apiurl, req.reqid, 'superseded',
-                                     f'superseded by {result}', result)
+                change_request_state(apiurl, req.reqid, 'superseded', 
f'superseded by {result}', result, can_fail=True)
 
         if opts.supersede:
             change_request_state(apiurl, opts.supersede, 'superseded',
@@ -2733,8 +2733,7 @@
         rid = root.get('id')
         print(f"Request {rid} created")
         for srid in supersede:
-            change_request_state(apiurl, srid, 'superseded',
-                                 f'superseded by {rid}', rid)
+            change_request_state(apiurl, srid, 'superseded', f'superseded by 
{rid}', rid, can_fail=True)
 
     @cmdln.option('-m', '--message', metavar='TEXT',
                   help='specify message TEXT')
@@ -4412,6 +4411,17 @@
                 for d in r.findall('devel'):
                     line += f" using sources from 
{d.get('project')}/{d.get('package')}"
                 print(line)
+
+            if subcmd == "maintained":
+                # print maintained branches in git
+                from .gitea_api.cache import gitea_cache_maintained
+
+                org_branches = gitea_cache_maintained(package=package)
+                for org, branches in sorted(org_branches.items()):
+                    branches.sort()
+                    for branch in branches:
+                        print(f"[git] {org}/{package}:{branch}")
+
             return
 
         apiopt = ''
@@ -7166,14 +7176,14 @@
 
         project = package = repository = arch = build_descr = None
         if len(args) <= 3:
-            if not is_package_dir('.'):
-                raise oscerr.WrongArgs('Incorrect number of arguments (Note: 
\'.\' is no package wc)')
             if opts.alternative_project:
                 project = opts.alternative_project
                 package = '_repository'
-            else:
+            elif is_package_dir('.'):
                 project = store_read_project('.')
                 package = store_read_package('.')
+            else:
+                raise oscerr.WrongArgs('Incorrect number of arguments (Note: 
\'.\' is no package wc)')
             repository, arch, build_descr = self.parse_repoarchdescr(args, 
alternative_project=opts.alternative_project, ignore_descr=True, 
multibuild_package=opts.multibuild_package)
         elif len(args) == 4 or len(args) == 5:
             project = self._process_project_name(args[0])
@@ -7754,6 +7764,10 @@
                   help='Do not use preinstall images for creating the build 
root.')
     @cmdln.option("--just-print-buildroot", action="store_true",
                   help="Print build root path and exit.")
+    @cmdln.option("--buildinfo", action="store_true",
+                  help="Print buildinfo and exit.")
+    @cmdln.option("--buildinfo-debug", action="store_true",
+                  help="Print buildinfo in debug mode and exit.")
     @cmdln.option('--no-timestamps', '-s', '--strip-time', action='store_true',
                   help='Hide the time prefix in output.')
     @cmdln.alias('chroot')
@@ -7841,6 +7855,9 @@
         if opts.debuginfo and opts.disable_debuginfo:
             raise oscerr.WrongOptions('osc: --debuginfo and 
--disable-debuginfo are mutual exclusive')
 
+        if (opts.buildinfo or opts.buildinfo_debug) and (opts.offline or 
opts.noinit):
+            raise oscerr.WrongOptions('osc: --buildinfo(-debug) is mutually 
exclusive with --offline and --no-init')
+
         if subcmd == 'wipe':
             opts.wipe = True
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/core.py new/osc-1.27.2/osc/core.py
--- old/osc-1.27.1/osc/core.py  2026-06-04 11:56:02.000000000 +0200
+++ new/osc-1.27.2/osc/core.py  2026-07-01 13:24:02.000000000 +0200
@@ -2287,7 +2287,7 @@
     return root.get('code')
 
 
-def change_request_state(apiurl: str, reqid, newstate, message="", 
supersed=None, force=False, keep_packages_locked=False):
+def change_request_state(apiurl: str, reqid, newstate, message="", 
supersed=None, force=False, keep_packages_locked=False, can_fail: bool = False):
     query = {"cmd": "changestate", "newstate": newstate}
     if supersed:
         query['superseded_by'] = supersed
@@ -2297,7 +2297,14 @@
         query['keep_packages_locked'] = "1"
     u = makeurl(apiurl,
                 ['request', reqid], query=query)
-    f = http_POST(u, data=message)
+    try:
+        f = http_POST(u, data=message)
+    except HTTPError as e:
+        if can_fail:
+            from .output import print_msg
+            print_msg(f"could not change state of request {reqid}: {e}", 
print_to="warning")
+            return None
+        raise
 
     root = xml_parse(f).getroot()
     return root.get('code', 'unknown')
@@ -5345,12 +5352,24 @@
     elif os.path.abspath(dir) == '/':
         raise oscerr.OscIOError(None, 'cannot remove \'/\'')
 
+    if not os.path.exists(dir):
+        return
+
     for dirpath, dirnames, filenames in os.walk(dir, topdown=False):
         for filename in filenames:
-            os.unlink(os.path.join(dirpath, filename))
+            try:
+                os.unlink(os.path.join(dirpath, filename))
+            except FileNotFoundError:
+                pass
         for dirname in dirnames:
-            os.rmdir(os.path.join(dirpath, dirname))
-    os.rmdir(dir)
+            try:
+                os.rmdir(os.path.join(dirpath, dirname))
+            except FileNotFoundError:
+                pass
+    try:
+        os.rmdir(dir)
+    except FileNotFoundError:
+        pass
 
 
 def unpack_srcrpm(srpm, dir, *files):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/git_scm/manifest.py 
new/osc-1.27.2/osc/git_scm/manifest.py
--- old/osc-1.27.1/osc/git_scm/manifest.py      2026-06-04 11:56:02.000000000 
+0200
+++ new/osc-1.27.2/osc/git_scm/manifest.py      2026-07-01 13:24:02.000000000 
+0200
@@ -137,7 +137,7 @@
             for i in directories:
                 if os.path.basename(i).startswith("."):
                     continue
-                if os.path.dirname(i) != path:
+                if os.path.normpath(os.path.dirname(i)) != 
os.path.normpath(path):
                     continue
                 result.append(i)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/gitea_api/cache.py 
new/osc-1.27.2/osc/gitea_api/cache.py
--- old/osc-1.27.1/osc/gitea_api/cache.py       2026-06-04 11:56:02.000000000 
+0200
+++ new/osc-1.27.2/osc/gitea_api/cache.py       2026-07-01 13:24:02.000000000 
+0200
@@ -10,47 +10,61 @@
     return result
 
 
-def ignore_http_errors(func):
+def ignore_http_errors(func=None, *, default=None):
     """
-    Return [] if `base_url` host is not found or doesn't return the expected 
status.
-    This is needed because majority of OBS deployments don't have the new 
service for searching.
+    Decorator to ignore 4xx HTTP errors and name resolution/max retry errors.
+
+    If an error occurs, returns the specified `default` value (defaults to 
`None`).
+    This is needed because majority of OBS deployments don't have the new 
service deployed.
+
+    Args:
+        func (callable, optional): The function to wrap.
+        default (any, optional): The value to return on HTTP 4xx or connection 
errors.
+
+    Returns:
+        callable: The decorated function or a decorator.
     """
+    def decorator(f):
+        @functools.wraps(f)
+        def wrapper(*args, **kwargs):
+            from urllib.error import HTTPError
+
+            # HACK: NameResolutionError and MaxRetryError are not available in 
urllib3 v1, let's ignore all exceptions in this case
+            try:
+                from urllib3.exceptions import NameResolutionError
+            except ImportError:
+                NameResolutionError = Exception
 
-    @functools.wraps(func)
-    def wrapper(*args, **kwargs):
-        from urllib.error import HTTPError
-
-        # HACK: NameResolutionError and MaxRetryError are not available in 
urllib3 v1, let's ignore all exceptions in this case
-        try:
-            from urllib3.exceptions import NameResolutionError
-        except ImportError:
-            NameResolutionError = Exception
-
-        try:
-            from urllib3.exceptions import MaxRetryError
-        except ImportError:
-            MaxRetryError = Exception
+            try:
+                from urllib3.exceptions import MaxRetryError
+            except ImportError:
+                MaxRetryError = Exception
 
-        try:
             try:
-                response = func(*args, **kwargs)
-            except HTTPError as e:
-                if 400 <= e.status < 500:
-                    return []
-                raise
+                try:
+                    response = f(*args, **kwargs)
+                except HTTPError as e:
+                    if 400 <= e.status < 500:
+                        return default
+                    raise
+
+                if hasattr(response, "status") and 400 <= response.status < 
500:
+                    return default
 
-            if hasattr(response, "status") and 400 <= response.status < 500:
-                return []
+                return response
 
-            return response
+            except (NameResolutionError, MaxRetryError):
+                return default
 
-        except (NameResolutionError, MaxRetryError):
-            return []
+        return wrapper
 
-    return wrapper
+    if func is None:
+        return decorator
+    else:
+        return decorator(func)
 
 
-@ignore_http_errors
+@ignore_http_errors(default=[])
 def gitea_cache_search_packages(
     base_url: Optional[str] = None,
     names: Optional[List[str]] = None,
@@ -75,7 +89,7 @@
     return response.json()
 
 
-@ignore_http_errors
+@ignore_http_errors(default=[])
 def gitea_cache_search_projects(
     base_url: Optional[str] = None,
     names: Optional[List[str]] = None,
@@ -100,7 +114,7 @@
     return response.json()
 
 
-@ignore_http_errors
+@ignore_http_errors(default=[])
 def gitea_cache_search_package_maintainers(
     base_url: Optional[str] = None,
     users: Optional[List[str]] = None,
@@ -129,7 +143,7 @@
     return response.json()
 
 
-@ignore_http_errors
+@ignore_http_errors(default=[])
 def gitea_cache_search_project_maintainers(
     base_url: Optional[str] = None,
     users: Optional[List[str]] = None,
@@ -152,3 +166,21 @@
     url = makeurl(base_url, ["api", "v1", "project", "maintainer", "search"], 
q)
     response = http_request("GET", url)
     return response.json()
+
+
+@ignore_http_errors(default={})
+def gitea_cache_maintained(
+    *,
+    package: str,
+    base_url: Optional[str] = None,
+):
+    from ..core import http_request
+    from ..core import makeurl
+
+    if not base_url:
+        base_url = get_default_base_url()
+
+    q = {}
+    url = makeurl(base_url, ["api", "v1", "maintained", package], q)
+    response = http_request("GET", url)
+    return response.json()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/gitea_api/exceptions.py 
new/osc-1.27.2/osc/gitea_api/exceptions.py
--- old/osc-1.27.1/osc/gitea_api/exceptions.py  2026-06-04 11:56:02.000000000 
+0200
+++ new/osc-1.27.2/osc/gitea_api/exceptions.py  2026-07-01 13:24:02.000000000 
+0200
@@ -248,5 +248,20 @@
         return result
 
 
+class UnauthorizedTokenRequired(GiteaException):
+    RESPONSE_STATUS = 401
+    RESPONSE_MESSAGE_RE = [
+        re.compile(r"token is required"),
+    ]
+
+    def __str__(self):
+        result = (
+            "A token hasn't been provided.\n"
+            " - Set token in the config file via `git-obs login update 
--new-token=- <login-name>`.\n"
+            " - Alternatively set the `GIT_OBS_LOGIN_<LOGIN-NAME>_TOKEN` env 
variable."
+        )
+        return result
+
+
 # gather all exceptions from this module that inherit from GiteaException
 EXCEPTION_CLASSES = [i for i in globals().values() if hasattr(i, 
"RESPONSE_MESSAGE_RE") and inspect.isclass(i) and issubclass(i, GiteaException)]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/gitea_api/pr.py 
new/osc-1.27.2/osc/gitea_api/pr.py
--- old/osc-1.27.1/osc/gitea_api/pr.py  2026-06-04 11:56:02.000000000 +0200
+++ new/osc-1.27.2/osc/gitea_api/pr.py  2026-07-01 13:24:02.000000000 +0200
@@ -185,6 +185,12 @@
         return self._data["merged_at"]
 
     @property
+    def mergeable(self) -> Optional[bool]:
+        if not self.is_pull_request:
+            return None
+        return self._data.get("mergeable")
+
+    @property
     def allow_maintainer_edit(self) -> Optional[bool]:
         if not self.is_pull_request:
             return None
@@ -302,6 +308,7 @@
         if self.is_pull_request:
             table.add("Draft", yes_no(self.draft))
             table.add("Merged", yes_no(self.merged))
+            table.add("Mergeable", yes_no(self.mergeable))
             table.add("Allow edit", yes_no(self.allow_maintainer_edit))
         table.add("Author", f"{self.user_obj.login_full_name_email}")
         if self.is_pull_request:
@@ -390,15 +397,11 @@
             "title": title,
             "body": description,
             "labels": labels,
-            # TODO: use after we migrate to sufficiently new Gitea that 
supports it
-            # "allow_maintainer_edit": allow_maintainer_edit,
+            "allow_maintainer_edit": allow_maintainer_edit,
         }
         response = conn.request("POST", url, json_data=data)
         obj = cls(response.json(), response=response, conn=conn)
 
-        # FIXME: older Gitea versions can't set the maintainer edits on PR 
create
-        obj = cls.set(conn, obj.base_owner, obj.base_repo, obj.number, 
allow_maintainer_edit=allow_maintainer_edit)
-
         return obj
 
     @classmethod
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/obs_scm/package.py 
new/osc-1.27.2/osc/obs_scm/package.py
--- old/osc-1.27.1/osc/obs_scm/package.py       2026-06-04 11:56:02.000000000 
+0200
+++ new/osc-1.27.2/osc/obs_scm/package.py       2026-07-01 13:24:02.000000000 
+0200
@@ -638,8 +638,7 @@
         tdir = None
         try:
             tdir = os.path.join(self.storedir, '_in_commit')
-            if os.path.isdir(tdir):
-                shutil.rmtree(tdir)
+            shutil.rmtree(tdir, ignore_errors=True)
             os.mkdir(tdir)
             while send and tries:
                 for filename in send[:]:
@@ -660,8 +659,8 @@
             # update store with the committed files
             self.__commit_update_store(tdir)
         finally:
-            if tdir is not None and os.path.isdir(tdir):
-                shutil.rmtree(tdir)
+            if tdir is not None:
+                shutil.rmtree(tdir, ignore_errors=True)
         self.rev = sfilelist.get('rev')
         print()
         print(f'Committed revision {self.rev}.')
@@ -752,7 +751,10 @@
         if mtime:
             utime(filename, (-1, mtime))
         if origfile is not None:
-            os.unlink(origfile)
+            try:
+                os.unlink(origfile)
+            except FileNotFoundError:
+                pass
 
     @fail_if_git()
     def mergefile(self, n, revision, mtime=None):
@@ -778,7 +780,10 @@
             # don't try merging
             shutil.copyfile(upfilename, filename)
             shutil.copyfile(upfilename, storefilename)
-            os.unlink(origfile)
+            try:
+                os.unlink(origfile)
+            except FileNotFoundError:
+                pass
             self.in_conflict.append(n)
             self.write_conflictlist()
             return 'C'
@@ -795,14 +800,26 @@
             if ret == 0:
                 # merge was successful... clean up
                 shutil.copyfile(upfilename, storefilename)
-                os.unlink(upfilename)
-                os.unlink(myfilename)
-                os.unlink(origfile)
+                try:
+                    os.unlink(upfilename)
+                except FileNotFoundError:
+                    pass
+                try:
+                    os.unlink(myfilename)
+                except FileNotFoundError:
+                    pass
+                try:
+                    os.unlink(origfile)
+                except FileNotFoundError:
+                    pass
                 return 'G'
             elif ret == 1:
                 # unsuccessful merge
                 shutil.copyfile(upfilename, storefilename)
-                os.unlink(origfile)
+                try:
+                    os.unlink(origfile)
+                except FileNotFoundError:
+                    pass
                 self.in_conflict.append(n)
                 self.write_conflictlist()
                 return 'C'
@@ -1501,7 +1518,10 @@
                 if origfile.endswith('.copy'):
                     # ok it seems we aborted at some point during the copy 
process
                     # (copy process == copy wcfile to the _in_update dir). 
remove file+continue
-                    os.unlink(origfile)
+                    try:
+                        os.unlink(origfile)
+                    except FileNotFoundError:
+                        pass
                 elif self.findfilebyname(broken_file[0]) is None:
                     # should we remove this file from _in_update? if we don't
                     # the user has no chance to continue without removing the 
file manually
@@ -1520,7 +1540,10 @@
                     os.rename(origfile, wcfile)
                 else:
                     # everything seems to be ok
-                    os.unlink(origfile)
+                    try:
+                        os.unlink(origfile)
+                    except FileNotFoundError:
+                        pass
             elif len(broken_file) > 1:
                 raise oscerr.PackageInternalError(self.prjname, self.name, 
'too many files in \'_in_update\' dir')
             tmp = rfiles[:]
@@ -1537,8 +1560,14 @@
             if not service_files:
                 services = []
             self.__update(kept, added, deleted, services, ET.tostring(root, 
encoding=ET_ENCODING), root.get('rev'))
-            os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
-            os.rmdir(os.path.join(self.storedir, '_in_update'))
+            try:
+                os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
+            except FileNotFoundError:
+                pass
+            try:
+                os.rmdir(os.path.join(self.storedir, '_in_update'))
+            except FileNotFoundError:
+                pass
         # ok everything is ok (hopefully)...
         fm = self.get_files_meta(revision=rev)
         root = xml_fromstring(fm)
@@ -1548,9 +1577,15 @@
         if not service_files:
             services = []
         self.__update(kept, added, deleted, services, fm, root.get('rev'))
-        os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
+        try:
+            os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
+        except FileNotFoundError:
+            pass
         if os.path.isdir(os.path.join(self.storedir, '_in_update')):
-            os.rmdir(os.path.join(self.storedir, '_in_update'))
+            try:
+                os.rmdir(os.path.join(self.storedir, '_in_update'))
+            except FileNotFoundError:
+                pass
         self.size_limit = old_size_limit
 
     @fail_if_git()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/osc/util/git_version.py 
new/osc-1.27.2/osc/util/git_version.py
--- old/osc-1.27.1/osc/util/git_version.py      2026-06-04 11:56:02.000000000 
+0200
+++ new/osc-1.27.2/osc/util/git_version.py      2026-07-01 13:24:02.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.27.1"
+    version = "1.27.2"
     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.27.1/tests/test_commandline.py 
new/osc-1.27.2/tests/test_commandline.py
--- old/osc-1.27.1/tests/test_commandline.py    2026-06-04 11:56:02.000000000 
+0200
+++ new/osc-1.27.2/tests/test_commandline.py    2026-07-01 13:24:02.000000000 
+0200
@@ -4,6 +4,7 @@
 import tempfile
 import unittest
 import unittest.mock
+from urllib.error import HTTPError
 
 from osc.commandline import Command
 from osc.commandline import MainCommand
@@ -12,6 +13,7 @@
 from osc.commandline import pop_project_package_repository_arch_from_args
 from osc.commandline import 
pop_project_package_targetproject_targetpackage_from_args
 from osc.commandline import pop_repository_arch_from_args
+from osc.core import change_request_state
 from osc.oscerr import NoWorkingCopy, OscValueError
 from osc.store import Store
 
@@ -821,5 +823,41 @@
         )
 
 
+class TestChangeRequestStateCanFail(unittest.TestCase):
+    """Regression tests for openSUSE/osc#2130: can_fail kwarg."""
+
+    def _make_http_error(self, code, reqid="1"):
+        return HTTPError(
+            url=f"https://example.com/request/{reqid}";,
+            code=code,
+            msg="Error",
+            hdrs=None,
+            fp=None,
+        )
+
+    def test_can_fail_suppresses_any_http_error(self):
+        """can_fail=True returns None and warns instead of raising."""
+        with unittest.mock.patch(
+            "osc.core.http_POST", side_effect=self._make_http_error(403),
+        ), unittest.mock.patch(
+            "osc.output.print_msg",
+        ) as warn:
+            result = change_request_state("https://api";, "815980", 
"superseded", can_fail=True)
+
+        self.assertIsNone(result)
+        self.assertEqual(warn.call_count, 1)
+        # call_args is (args, kwargs); .kwargs attr exists on Python >= 3.8
+        call_kwargs = warn.call_args[1] if warn.call_args else {}
+        self.assertEqual(call_kwargs.get("print_to"), "warning")
+
+    def test_without_can_fail_raises(self):
+        """Without can_fail, HTTPErrors still propagate."""
+        with unittest.mock.patch(
+            "osc.core.http_POST", side_effect=self._make_http_error(403),
+        ):
+            with self.assertRaises(HTTPError):
+                change_request_state("https://api";, "1", "superseded")
+
+
 if __name__ == "__main__":
     unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/tests/test_gitea_api_pr.py 
new/osc-1.27.2/tests/test_gitea_api_pr.py
--- old/osc-1.27.1/tests/test_gitea_api_pr.py   2026-06-04 11:56:02.000000000 
+0200
+++ new/osc-1.27.2/tests/test_gitea_api_pr.py   2026-07-01 13:24:02.000000000 
+0200
@@ -19,6 +19,7 @@
             "allow_maintainer_edit": False,
             "draft": False,
             "merged": False,
+            "mergeable": True,
             "base": {
                 "ref": "base-branch",
                 "sha": "base-commit",
@@ -54,6 +55,7 @@
         self.assertEqual(obj.user_obj.login, "alice")
         self.assertEqual(obj.draft, False)
         self.assertEqual(obj.merged, False)
+        self.assertEqual(obj.mergeable, True)
         self.assertEqual(obj.allow_maintainer_edit, False)
 
         self.assertEqual(obj.base_owner, "base-owner")
@@ -97,6 +99,7 @@
         self.assertEqual(obj.user_obj.login, "alice")
         self.assertEqual(obj.draft, None)
         self.assertEqual(obj.merged, None)
+        self.assertEqual(obj.mergeable, None)
         self.assertEqual(obj.allow_maintainer_edit, None)
 
         self.assertEqual(obj.base_owner, "base-owner")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/tests/test_init_project.py 
new/osc-1.27.2/tests/test_init_project.py
--- old/osc-1.27.1/tests/test_init_project.py   2026-06-04 11:56:02.000000000 
+0200
+++ new/osc-1.27.2/tests/test_init_project.py   2026-07-01 13:24:02.000000000 
+0200
@@ -67,6 +67,34 @@
         self._check_list(os.path.join(storedir, '_apiurl'), 
'http://localhost\n')
         self.assertFalse(os.path.exists(os.path.join(storedir, '_packages')))
 
+    def test_delete_dir_robustness(self):
+        """delete_dir successfully ignores FileNotFoundError during recursive 
deletion"""
+        import tempfile
+        import shutil
+        from osc.core import delete_dir
+
+        temp_dir = tempfile.mkdtemp(dir=self.tmpdir)
+        try:
+            # create a file inside temp_dir
+            file_path = os.path.join(temp_dir, "somefile")
+            with open(file_path, "w") as f:
+                f.write("content")
+
+            # mock os.unlink to raise FileNotFoundError on call to simulate 
concurrent deletion
+            original_unlink = os.unlink
+            def mocked_unlink(path):
+                if os.path.exists(path):
+                    original_unlink(path)
+                # This call will raise FileNotFoundError
+                original_unlink(path)
+
+            from unittest.mock import patch
+            with patch("os.unlink", mocked_unlink):
+                # this should not raise FileNotFoundError because delete_dir 
handles it gracefully
+                delete_dir(temp_dir)
+        finally:
+            shutil.rmtree(temp_dir, ignore_errors=True)
+
 
 if __name__ == '__main__':
     unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osc-1.27.1/tests/test_update.py 
new/osc-1.27.2/tests/test_update.py
--- old/osc-1.27.1/tests/test_update.py 2026-06-04 11:56:02.000000000 +0200
+++ new/osc-1.27.2/tests/test_update.py 2026-07-01 13:24:02.000000000 +0200
@@ -301,6 +301,30 @@
         self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 
'added')))
         self._check_digests('testUpdateResumeDeletedFile_files')
 
+    @GET("http://localhost/source/osctest/simple?rev=latest";, 
file="testUpdateNoChanges_files")
+    @GET("http://localhost/source/osctest/simple/_meta";, file="meta.xml")
+    def testUpdateMissingInUpdateFiles(self):
+        """update handles missing _in_update/_files or _in_update dir at 
cleanup gracefully"""
+        from unittest.mock import patch
+        import shutil
+
+        self._change_to_pkg("simple")
+        p = osc.core.Package(".")
+
+        original_update = osc.core.Package._Package__update
+
+        def mocked_update(self_obj, *args, **kwargs):
+            res = original_update(self_obj, *args, **kwargs)
+            # simulate parallel cleanup by another concurrent osc process
+            shutil.rmtree(os.path.join(self_obj.storedir, "_in_update"), 
ignore_errors=True)
+            return res
+
+        with patch.object(osc.core.Package, "_Package__update", mocked_update):
+            # this should succeed without raising FileNotFoundError
+            p.update()
+
+        self.assertEqual(sys.stdout.getvalue(), "At revision 1.\n")
+
 
 if __name__ == '__main__':
     unittest.main()

++++++ osc.dsc ++++++
--- /var/tmp/diff_new_pack.981h3p/_old  2026-07-01 17:12:43.039879067 +0200
+++ /var/tmp/diff_new_pack.981h3p/_new  2026-07-01 17:12:43.067880032 +0200
@@ -1,6 +1,6 @@
 Format: 1.0
 Source: osc
-Version: 1.27.1-0
+Version: 1.27.2-0
 Binary: osc
 Maintainer: Adrian Schroeter <[email protected]>
 Architecture: any

Reply via email to