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
