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-10-12 22:07:24 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/osc (Old) and /work/SRC/openSUSE:Factory/.osc.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "osc" Mon Oct 12 22:07:24 2026 rev:229 rq:1384264 version:1.28.0 Changes: -------- --- /work/SRC/openSUSE:Factory/osc/osc.changes 2026-08-04 21:37:22.694347402 +0200 +++ /work/SRC/openSUSE:Factory/.osc.new.3698736/osc.changes 2026-10-12 22:07:32.979132159 +0200 @@ -1,0 +2,36 @@ +Mon Oct 12 09:01:57 UTC 2026 - Daniel Mach <[email protected]> + +- 1.28.0 + - Command-line: + - Added support for the 'VISUAL' environment variable + - Allow 'osc fork project/package' as a single argument + - Add 'osc log' --limit/-l option to limit the number of returned revisions + - Prefill interactive acceptance comments from the request message option + - Honor XDG_CONFIG_HOME in git-obs configuration lookup + - Expand user paths in XDG configuration overrides + - Fix interactive request review test compatibility + - Fix 'git-obs pr dump' to error out cleanly on changed submodule metadata + - Fix do_downloadassets missing help text + - Error out on deprecated apisrv option is set + - Library: + - Use any avaiable -p/--prefer-pkgs packages in containers builds + - Fix 'osc copypac' to error out when copying a package with an empty name + - Change XmlModel to check if the top-level element matches the expected XML_TAG + - Validate if we're working with a package in core.copy_pac() + - Fix 'osc branch' crash on packages with an empty <scmsync/> element + - Include multibuild flavors in interactive request review + - Fix stale tardiff cache by rebuilding it + - Set tardiff cache commit git username and email + - Fix pylint error by accessing Action members via getattr rather than directly + - Fix lint errors in request review branch + - Fix NoWorkingCopy exception silencing in get_store() + - Docs: + - Add AGENTS.md as a guideline for AI assisted coding + - Document git-obs pr forward workflow and merge modes + - Highlight 'sync' mode and clarify traditional OBS alignment in pr forward + - Spec: + - Drop no-compile + - Adapt use_python macro generation to support the legacy primary_python fallback introduced in python-rpm-macros + - Fix the regexp generating the use_python definition so that 'python3' doesn't turn into 'python3.' + +------------------------------------------------------------------- Old: ---- osc-1.27.3.tar.gz New: ---- osc-1.28.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ osc.spec ++++++ --- /var/tmp/diff_new_pack.dfyYjT/_old 2026-10-12 22:07:34.052176453 +0200 +++ /var/tmp/diff_new_pack.dfyYjT/_new 2026-10-12 22:07:34.053176494 +0200 @@ -17,7 +17,11 @@ %if %{defined primary_python} -%define use_python %(echo %{primary_python} | sed -e 's|python3|python3.|g') +%if "%{primary_python}" == "python3" +%define use_python python3 +%else +%define use_python %(echo %{primary_python} | sed -E 's|python3([0-9]+)|python3.\\\1|g') +%endif %define use_python_pkg %{primary_python} %else %define use_python python3 @@ -84,7 +88,7 @@ %endif Name: osc -Version: 1.27.3 +Version: 1.28.0 Release: 0 Summary: Command-line client for the Open Build Service License: GPL-2.0-or-later @@ -247,7 +251,6 @@ %{use_python} -mpip install \ --verbose --progress-bar off --disable-pip-version-check \ --root %{buildroot} \ - --no-compile \ --ignore-installed --no-deps \ --no-index --find-links ./build osc ++++++ PKGBUILD ++++++ --- /var/tmp/diff_new_pack.dfyYjT/_old 2026-10-12 22:07:34.087177897 +0200 +++ /var/tmp/diff_new_pack.dfyYjT/_new 2026-10-12 22:07:34.090178021 +0200 @@ -1,5 +1,5 @@ pkgname=osc -pkgver=1.27.3 +pkgver=1.28.0 pkgrel=0 pkgdesc="Command-line client for the Open Build Service" arch=('x86_64') ++++++ debian.changelog ++++++ --- /var/tmp/diff_new_pack.dfyYjT/_old 2026-10-12 22:07:34.140180085 +0200 +++ /var/tmp/diff_new_pack.dfyYjT/_new 2026-10-12 22:07:34.145180291 +0200 @@ -1,4 +1,4 @@ -osc (1.27.3-0) unstable; urgency=low +osc (1.28.0-0) unstable; urgency=low * Placeholder ++++++ osc-1.27.3.tar.gz -> osc-1.28.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/AGENTS.md new/osc-1.28.0/AGENTS.md --- old/osc-1.27.3/AGENTS.md 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.28.0/AGENTS.md 2026-10-12 10:58:19.000000000 +0200 @@ -0,0 +1,62 @@ +# AGENTS.md — osc + +`osc` is the command-line client for the Open Build Service (OBS). +`git-obs` is the command-line client for the Git Packaging Workflow on top of OBS. + +## Project structure - osc + +- `osc/commands/` - osc CLI commands. +- `osc/commandline.py` - CLI setup and legacy `do_*` commands. Do not add new ones. +- `osc/obs_api/` - typed OBS API objects. +- `osc/conf.py` - configuration and `oscrc` handling. +- `osc/core.py` - legacy API and implementation. Use and extend `obs_api` instead if applicable. +- `osc/credentials.py` - credential handling. +- `osc/oscerr.py` - osc-specific exception hierarchy. +- `osc/output/` - user-facing output, prompts and TTY handling. +- `osc/util/` - shared utilities. +- `osc/cmdln.py` - vendored framework. Do not modify. +- `osc/babysitter.py` - entry point; maps `oscerr.*` to messages and exit codes. +- `osc/_private/` - new internals. Prefer over the legacy `osc/core.py`. + +## Project structure - git-obs + +- `osc/commands_git/` - git-obs CLI commands. +- `osc/commandline_git.py` - CLI setup. +- `osc/gitea_api/` - typed Gitea API objects. +- `osc/gitea_api/conf.py` - configuration and `~/.config/tea/config.yml` handling. + +## Development rules + +- Minimal changes leading to the requested behavior. +- Python 3.6 is the floor. f-strings yes; no walrus, no `match`, no `X | Y`. +- Preferred line length <= 120. +- Use `oscerr.OscBaseError` subclasses for new osc-specific error conditions. +- Configuration descriptions are used to generate documentation. +- Use attributes of the configuration objects, avoid legacy dict interface. +- Keep imports lazy where practical; startup time matters. +- Preserve backwards compatibility, especially the public and plugin API. + +## Adding a command + +- Add module in `osc/commands/<name>.py` with a `Command` subclass. +- Modules are auto-discovered, no registration needed. +- Docstring is user-facing help: `help` (1 line), empty line, description. + +## Handling input + +- Use `osc.util.helper.raw_input()` for normal stdin input. EOF raises `oscerr.UserAbort`. +- Use `osc.output.input.get_user_input()` for choice prompts. + +## Tests + +- `tests` - unit tests + - Run `python3 -m unittest` after `pip install -e .`. + - Simple, fast, offline tests. Avoid mocking unless necessary. + - `tests/common.py` - shared harness. +- `behave` - integration tests + - Require an OBS/podman environment. +- Linters + - Code is old and has many issues, focus on diffs to produce new clean code. + - `darker --check --line-length=120 --formatter ruff .` + - `ruff check .` + - `pylint --errors-only osc` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/NEWS new/osc-1.28.0/NEWS --- old/osc-1.27.3/NEWS 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/NEWS 2026-10-12 10:58:19.000000000 +0200 @@ -1,3 +1,39 @@ +- 1.28.0 + - Command-line: + - Added support for the 'VISUAL' environment variable + - Allow 'osc fork project/package' as a single argument + - Add 'osc log' --limit/-l option to limit the number of returned revisions + - Prefill interactive acceptance comments from the request message option + - Honor XDG_CONFIG_HOME in git-obs configuration lookup + - Expand user paths in XDG configuration overrides + - Fix interactive request review test compatibility + - Fix 'git-obs pr dump' to error out cleanly on changed submodule metadata + - Fix do_downloadassets missing help text + - Error out on deprecated apisrv option is set + - Library: + - Use any avaiable -p/--prefer-pkgs packages in containers builds + - Fix 'osc copypac' to error out when copying a package with an empty name + - Change XmlModel to check if the top-level element matches the expected XML_TAG + - Validate if we're working with a package in core.copy_pac() + - Fix 'osc branch' crash on packages with an empty <scmsync/> element + - Include multibuild flavors in interactive request review + - Fix stale tardiff cache by rebuilding it + - Set tardiff cache commit git username and email + - Fix pylint error by accessing Action members via getattr rather than directly + - Fix lint errors in request review branch + - Fix NoWorkingCopy exception silencing in get_store() + - Docs: + - Add AGENTS.md as a guideline for AI assisted coding + - Document git-obs pr forward workflow and merge modes + - Highlight 'sync' mode and clarify traditional OBS alignment in pr forward + - Spec: + - Drop no-compile + - Adapt use_python macro generation to support the legacy primary_python fallback introduced in python-rpm-macros + - Fix the regexp generating the use_python definition so that 'python3' doesn't turn into 'python3.' + +- 1.27.4 + - Command-line: + - Add 'osc log' --limit/-l option to limit the number of revisions returned by the server - 1.27.3 - Command-line: - Fix 'osc lbl' to work with git based packages diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/doc/git-obs-quickstart.rst new/osc-1.28.0/doc/git-obs-quickstart.rst --- old/osc-1.27.3/doc/git-obs-quickstart.rst 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/doc/git-obs-quickstart.rst 2026-10-12 10:58:19.000000000 +0200 @@ -14,7 +14,9 @@ Before executing any ``git-obs`` commands, you need to configure your credentials, also known as logins. These logins are shared with the ``tea`` -client and are stored in ``~/.config/tea/config.yml``. +client and are stored in ``$XDG_CONFIG_HOME/tea/config.yml``. If +``XDG_CONFIG_HOME`` is unset or empty, ``~/.config/tea/config.yml`` +is used instead. ``--gitea-config`` and ``GIT_OBS_CONFIG`` override this location. .. note:: See ``git-obs login --help`` for all commands related to managing login entries. @@ -200,6 +202,107 @@ ... +Workflow: Forwarding sources between branches (``git-obs pr forward``) +---------------------------------------------------------------------- + +The ``git-obs pr forward`` command is the Git-native counterpart to ``osc sr`` +across branches or code streams (for example, promoting changes from ``factory`` +to a downstream release branch such as ``slfo-main`` or ``16.0``). + +How it works: +~~~~~~~~~~~~~ + +1. **Reuses your existing fork:** + Checks whether you already have a fork of the target repository under your + Gitea account. If so, it reuses your existing fork without creating redundant + repositories or affecting other remotes. + +2. **Guarantees safety of your existing work:** + The command never modifies your default branch (``main`` / ``master``) or + any ongoing work in your other branches. Instead, it creates a dedicated, + isolated branch specifically for the forward operation: + + .. code:: + + for/<target_branch>/forward-<source_commit_sha> + + *(e.g., ``for/slfo-main/forward-7f9a2b...``)* + + Only this namespaced branch is pushed to your remote fork. + +3. **Performs clean, automated synchronization:** + The operation executes inside an isolated temporary directory. It integrates + the changes, verifies file additions and deletions, and synchronizes any + associated Git LFS assets. + +4. **Leaves no litter on your filesystem:** + The temporary directory is deleted automatically as soon as the command + finishes. No residual files are left behind unless you explicitly specify + ``--no-cleanup`` or designate a custom ``--workdir``. + +5. **Submits the Pull Request:** + Once the branch is pushed to your fork, a standard Gitea Pull Request is + opened against the upstream target branch. + + +Understanding the merge modes (``--mode``): +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When forwarding sources between branches that may have evolved separately, +you can select how changes should be reconciled: + +- **``auto`` (recommended):** + The best choice for routine forwarding. If the target branch has not diverged + from the source branch, it simply fast-forwards without adding unnecessary merge + commits. If the branches have diverged, it merges the source changes and + ensures the resulting file tree matches the source state. + +- **``sync``:** + Closest to traditional OBS behavior (``osc sr``). Rather than reconciling + disparate Git commit histories, this mode creates a single synchronization + commit that brings the destination package contents into exact alignment with + the source branch state. This is particularly useful when forwarding across + distribution releases or between branches that have diverged significantly, + ensuring the target cleanly reflects the source package without history conflicts. + +- **``fast-forward``:** + Applies changes only if the target branch can be cleanly fast-forwarded to + the source branch. If the target branch contains independent commits not present + in the source, the command halts without making changes. Use this when you + strictly require a linear commit history. + +- **``merge``:** + Integrates changes by creating a merge commit on the target branch. If conflicts + arise, incoming changes from the source take precedence, and files deleted in + the source are cleanly removed from the target. + +- **``merge-unrelated``:** + Behaves like ``merge``, but permits combining branches that do not share a common + historical ancestor (such as branches initialized independently in different repositories). + + +Usage examples: +~~~~~~~~~~~~~~~ + +Forward package changes from ``factory`` to ``slfo-main``: + +.. code:: + + git-obs -G opensuse pr forward products/slfo factory slfo-main --mode auto + +Preview all planned actions without modifying remotes or creating a PR (dry-run): + +.. code:: + + git-obs -G opensuse pr forward products/slfo factory slfo-main --mode auto --dry-run + +Provide a custom title and interactively edit the PR message: + +.. code:: + + git-obs -G opensuse pr forward products/slfo factory slfo-main --mode auto --title "Sync slfo-main with Factory" --edit + + Workflow: Querying pull requests -------------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/__init__.py new/osc-1.28.0/osc/__init__.py --- old/osc-1.27.3/osc/__init__.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/__init__.py 2026-10-12 10:58:19.000000000 +0200 @@ -13,7 +13,7 @@ from .util import git_version -__version__ = git_version.get_version('1.27.3') +__version__ = git_version.get_version('1.28.0') # vim: sw=4 et diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/build.py new/osc-1.28.0/osc/build.py --- old/osc-1.27.3/osc/build.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/build.py 2026-10-12 10:58:19.000000000 +0200 @@ -614,6 +614,14 @@ def get_prefer_pkgs(dirs, wanted_arch, type, cpio): + """ + Gather all packages from directories specified via -p/--prefer-pkgs + to make them available in the build environment. + + We're gathering all known package types, because they can be used + for building both packages and containers and we don't know + what's going to be used in the build recipe in the end. + """ paths = [] repositories = [] @@ -623,11 +631,7 @@ if repository: repositories.append(repository) else: - # we don't know binary type as we haven't received buildconfig yet - # that's why we add all files for any matching build types for build_type in BUILD_TYPES: - if build_type.name != type: - continue for pattern in build_type.prefer_packages_paths: for pkg_path in glob.glob(os.path.join(os.path.abspath(pkgs_dir), pattern), recursive=True): use_package = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/commandline.py new/osc-1.28.0/osc/commandline.py --- old/osc-1.27.3/osc/commandline.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/commandline.py 2026-10-12 10:58:19.000000000 +0200 @@ -192,6 +192,9 @@ if conf.config["show_download_progress"]: self.download_progress = create_text_meter() + if conf.config["apisrv"]: + raise oscerr.ConfigError("apisrv in config is deprecated, use apiurl instead", "") + if not args.apiurl: self.parser.error("Could not determine apiurl, use -A/--apiurl to specify one") @@ -1359,11 +1362,12 @@ @cmdln.alias('da') def do_downloadassets(self, subcmd, opts, *args): - from .core import download_assets - """ Download all assets referenced in the build descriptions """ + + from .core import download_assets + download_assets(".") @cmdln.alias('sdp') @@ -1590,7 +1594,8 @@ will be the pattern file to view or edit. With the --edit switch, the metadata can be edited. Per default, osc - opens the program specified by the environmental variable EDITOR with a + opens the program specified by the VISUAL environment variable, + or if not set the EDITOR environment variable, with a temporary file. Alternatively, content to be saved can be supplied via the --file switch. If the argument is '-', input is taken from stdin: osc meta prjconf home:user | sed ... | osc meta prjconf home:user -F - @@ -2974,7 +2979,7 @@ @cmdln.option('--no-devel', action='store_true', help='Do not attempt to forward to devel project') @cmdln.option('-m', '--message', metavar='TEXT', - help='specify message TEXT') + help='specify message TEXT; prefill the acceptance comment editor in interactive mode') @cmdln.option('-t', '--type', metavar='TYPE', help='limit to requests which contain a given action type (submit/delete/change_devel/add_role/set_bugowner/maintenance_incident/maintenance_release)') @cmdln.option('-a', '--all', action='store_true', @@ -3373,7 +3378,8 @@ ignore_reviews = subcmd != 'review' request_interactive_review(apiurl, result, group=opts.group, ignore_reviews=ignore_reviews, - source_buildstatus=source_buildstatus) + source_buildstatus=source_buildstatus, + message=opts.message) else: print(result.list_view(), '\n') else: @@ -3409,12 +3415,15 @@ if not r.get_actions('submit'): raise oscerr.WrongOptions('\'--edit\' not possible ' '(request has no \'submit\' action)') - return request_interactive_review(apiurl, r, 'e') + request_interactive_review(apiurl, r, 'e', message=opts.message) + return elif (opts.interactive or conf.config['request_show_interactive']) and not opts.non_interactive: ignore_reviews = subcmd != 'review' - return request_interactive_review(apiurl, r, group=opts.group, - ignore_reviews=ignore_reviews, - source_buildstatus=source_buildstatus) + request_interactive_review(apiurl, r, group=opts.group, + ignore_reviews=ignore_reviews, + source_buildstatus=source_buildstatus, + message=opts.message) + return else: print(r) print_comments(apiurl, 'request', reqid) @@ -5914,15 +5923,15 @@ osc ci file1 file2 ... """ - from .core import get_default_editor + from .core import _editor_command try: self._commit(subcmd, opts, args) except oscerr.ExtRuntimeError as e: pattern = re.compile("No such file") if "No such file" in e.msg: - editor = os.getenv('EDITOR', default=get_default_editor()) - print(f"Editor {editor} not found") + editor = _editor_command() + print(f"Editor {editor[0]} not found") return 1 print("ERROR: service run failed", e, file=sys.stderr) return 1 @@ -8267,6 +8276,8 @@ help='work on deleted package') @cmdln.option('-M', '--meta', action='store_true', default=None, help='checkout out meta data instead of sources') + @cmdln.option('-l', '--limit', metavar='N', type=int, default=None, + help='limit the number of revisions returned by the server') def do_log(self, subcmd, opts, *args): """ Shows the commit log of a package @@ -8305,7 +8316,7 @@ if opts.xml: format = 'xml' - lines = get_commitlog(apiurl, project, package, rev, format, opts.meta, opts.deleted, rev_upper, patch=opts.patch) + lines = get_commitlog(apiurl, project, package, rev, format, opts.meta, opts.deleted, rev_upper, patch=opts.patch, limit=opts.limit) pipe_to_pager(lines, add_newlines=True) @cmdln.option('-v', '--verbose', action='store_true', @@ -10815,8 +10826,9 @@ or can be specified via mailaddr environment variable. - By default, osc vc opens the program specified by the EDITOR - environment variable (and it uses Vim if that variable is not set) with + By default, osc vc opens the program specified by the VISUAL environment variable + or if not set the EDITOR environment variable + (and it uses Vim if that variable is not set), with a temporary file that should replace the *.changes file when saved by the editor, or discarded otherwise. """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/commandline_git.py new/osc-1.28.0/osc/commandline_git.py --- old/osc-1.27.3/osc/commandline_git.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/commandline_git.py 2026-10-12 10:58:19.000000000 +0200 @@ -250,7 +250,10 @@ self.add_argument( "--gitea-config", - help="Path to gitea config. Default: $GIT_OBS_CONFIG or ~/.config/tea/config.yml.", + help=( + "Path to gitea config. Default: $GIT_OBS_CONFIG or $XDG_CONFIG_HOME/tea/config.yml " + "(~/.config/tea/config.yml when XDG_CONFIG_HOME is unset or empty)." + ), ) self.add_argument( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/commands/fork.py new/osc-1.28.0/osc/commands/fork.py --- old/osc-1.27.3/osc/commands/fork.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/commands/fork.py 2026-10-12 10:58:19.000000000 +0200 @@ -91,6 +91,10 @@ package = args.package target_project = self._process_project_name(args.target_project) + if package is None and project and "/" in project: + # allow a single "project/package" argument, matching commands like 'co' and 'branch' + project, package = project.split("/", 1) + if project and "/" in project: self.parser.error(f"Invalid project: {project}") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/commands_git/pr_dump.py new/osc-1.28.0/osc/commands_git/pr_dump.py --- old/osc-1.27.3/osc/commands_git/pr_dump.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/commands_git/pr_dump.py 2026-10-12 10:58:19.000000000 +0200 @@ -325,7 +325,9 @@ elif is_project: base_value = base_submodules[i].get(key, None) head_value = head_submodules[i].get(key, None) - assert base_value == head_value, f"Submodule metadata has changed: submodule='{i}', key='{key}', base_value='{base_value}', head_value='{head_value}'" + if base_value != head_value: + msg = f"Submodule metadata has changed: submodule='{i}', key='{key}', base_value='{base_value}', head_value='{head_value}'" + raise gitea_api.GitObsRuntimeError(msg) base_commit = base_submodules[i].get("commit","") head_commit = head_submodules[i].get("commit","") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/conf.py new/osc-1.28.0/osc/conf.py --- old/osc-1.27.3/osc/conf.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/conf.py 2026-10-12 10:58:19.000000000 +0200 @@ -511,6 +511,15 @@ ), ) # type: ignore[assignment] + apisrv: Optional[str] = Field( + default=None, + description=textwrap.dedent( + """ + Deprecated option. Use apiurl instead. + """ + ), + ) # type: ignore[assignment] + section_auth: str = Field( default="Authentication options", exclude=True, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/core.py new/osc-1.28.0/osc/core.py --- old/osc-1.27.3/osc/core.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/core.py 2026-10-12 10:58:19.000000000 +0200 @@ -597,12 +597,10 @@ src_package_meta_releasename = self.src_pkg_object.get_meta_value("releasename") tgt_package = src_package_meta_releasename.split(".")[0] else: - if not getattr(self, "tgt_project", None) or not getattr(self, "tgt_package", None): + tgt_project = getattr(self, "tgt_project", None) + tgt_package = getattr(self, "tgt_package", None) + if not tgt_project or not tgt_package: return None - # tgt_project and tgt_package are checked above - # pylint: disable=no-member - tgt_project = self.tgt_project - tgt_package = self.tgt_package self._tgt_pkg_object = _private.ApiPackage(self.apiurl, tgt_project, tgt_package) return self._tgt_pkg_object @@ -2051,8 +2049,9 @@ def _editor_command(): + visual = os.getenv("VISUAL", default="").strip() editor = os.getenv("EDITOR", default="").strip() - editor = editor or get_default_editor() + editor = visual or editor or get_default_editor() try: cmd = shlex.split(editor) except SyntaxError: @@ -3810,11 +3809,12 @@ root = xml_fromstring(m) # error out if we're branching a scmsync package (we'd end up with garbage anyway) - if root is not None and root.find("scmsync") is not None: + scmsync_node = root.find("scmsync") if root is not None else None + if scmsync_node is not None and scmsync_node.text: msg = ("osc cannot branch packages with <scmsync>, i.e. externally " "managed sources. Often, the URL for cloning is also the URL " "for a collaborative web interface where you can fork (branch). " - "The scmsync URL was: " + root.find("scmsync").text) + "The scmsync URL was: " + scmsync_node.text) if devel_project: raise oscerr.PackageError(devel_project, devel_package, msg) raise oscerr.PackageError(src_project, src_package, msg) @@ -3918,6 +3918,8 @@ them into the other by uploading them (client-side copy) -- or by the server, in a single api call. """ + from . import obs_api + if (src_apiurl, src_project, src_package) == (dst_apiurl, dst_project, dst_package): # special cases when source and target can be the same: # * expanding sources @@ -3925,10 +3927,15 @@ if not any([expand, revision]): raise oscerr.OscValueError("Cannot copy package. Source and target are the same.") + if not src_package: + raise oscerr.OscValueError("Cannot copy a package without a name.") + meta = new_meta = src_meta = None if not (src_apiurl == dst_apiurl and src_project == dst_project and src_package == dst_package): src_meta = show_package_meta(src_apiurl, src_project, src_package) + # initialize Package to validate the XML; we're not replacing the existing code with an object for now + obs_api.Package.from_string(b''.join(src_meta)) dst_userid = conf.get_apiurl_usr(dst_apiurl) meta = replace_pkg_meta(src_meta, dst_package, dst_project, keep_maintainers, dst_userid, keep_develproject, keep_scmsync=(not client_side_copy)) @@ -4933,12 +4940,13 @@ deleted: Optional[bool] = None, revision_upper: Optional[str] = None, patch: Optional[bool] = None, + limit: Optional[int] = None, ): if package is None: package = "_project" from . import obs_api - revision_list = obs_api.Package.get_revision_list(apiurl, prj, package, deleted=deleted, meta=meta) + revision_list = obs_api.Package.get_revision_list(apiurl, prj, package, deleted=deleted, meta=meta, limit=limit) # TODO: consider moving the following block to Package.get_revision_list() # keep only entries matching the specified revision @@ -5910,7 +5918,7 @@ def request_interactive_review(apiurl, request, initial_cmd='', group=None, - ignore_reviews=False, source_buildstatus=False): + ignore_reviews=False, source_buildstatus=False, message=None): """review the request interactively""" tmpfile = None @@ -5936,16 +5944,19 @@ """ result = [] for action in src_actions: - disabled = show_package_disabled_repos(apiurl, action.src_project, action.src_package) - for repo in get_repos_of_project(apiurl, action.src_project): - if (disabled is None) or (repo.name not in [d["repo"] for d in disabled]): - entry = { - "proj": action.src_project, - "pkg": action.src_package, - "repo": repo.name, - "arch": repo.arch - } - result.append(entry) + for results in get_package_results(apiurl, action.src_project, action.src_package, multibuild=True): + root = xml_fromstring(results) + for repo in root.findall('result'): + for status in repo.findall('status'): + if status.get('code') in ('disabled', 'excluded'): + continue + entry = { + "proj": action.src_project, + "pkg": status.get('package'), + "repo": repo.get('repository'), + "arch": repo.get('arch') + } + result.append(entry) return result def select_repo(src_actions): @@ -6007,7 +6018,7 @@ for action in src_actions: print(f'{action.src_project}/{action.src_package}:') try: - print('\n'.join(get_results(apiurl, action.src_project, action.src_package))) + print('\n'.join(get_results(apiurl, action.src_project, action.src_package, multibuild=True))) except HTTPError as e: if e.code != 404: raise @@ -6133,6 +6144,8 @@ footer = 'changing request from state \'%s\' to \'%s\'\n\n' \ % (request.state.name, state) msg_template = change_request_state_template(request, state) + if state == 'accepted' and message is not None: + msg_template = message if tmpfile is None: footer += str(request) if tmpfile is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/gitea_api/common.py new/osc-1.28.0/osc/gitea_api/common.py --- old/osc-1.27.3/osc/gitea_api/common.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/gitea_api/common.py 2026-10-12 10:58:19.000000000 +0200 @@ -63,8 +63,11 @@ import shlex from .exceptions import GitObsRuntimeError + visual = os.getenv("VISUAL") editor = os.getenv("EDITOR", None) - if editor: + if visual: + candidates = [visual] + elif editor: candidates = [editor] else: candidates = ["vim", "vi"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/gitea_api/conf.py new/osc-1.28.0/osc/gitea_api/conf.py --- old/osc-1.27.3/osc/gitea_api/conf.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/gitea_api/conf.py 2026-10-12 10:58:19.000000000 +0200 @@ -102,7 +102,8 @@ def __init__(self, path: Optional[str] = None): if not path: - path = "~/.config/tea/config.yml" + config_home = os.environ.get("XDG_CONFIG_HOME") or "~/.config" + path = os.path.join(config_home, "tea", "config.yml") self.path = os.path.abspath(os.path.expanduser(path)) self.logins: List[Login] = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/gitea_api/tardiff.py new/osc-1.28.0/osc/gitea_api/tardiff.py --- old/osc-1.27.3/osc/gitea_api/tardiff.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/gitea_api/tardiff.py 2026-10-12 10:58:19.000000000 +0200 @@ -30,10 +30,30 @@ def __init__(self, path): self.git = git.Git(path) os.makedirs(self.path, exist_ok=True) + if self._has_incompatible_git_cache(): + shutil.rmtree(self.path) + os.makedirs(self.path, exist_ok=True) + self.git = git.Git(path) self.git.init(initial_branch="empty", quiet=True, mute_stderr=True) + self.git.set_config("user.name", "osc tardiff") + self.git.set_config("user.email", "[email protected]") # the git repo is switched to this branch by default to hide the files from disk self.git.commit("empty branch", allow_empty=True) + def _has_incompatible_git_cache(self) -> bool: + if not os.path.isdir(os.path.join(self.path, ".git")): + return False + + proc = subprocess.run( + ["git", "rev-parse", "--show-object-format"], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + encoding="utf-8", + cwd=self.path, + check=False, + ) + return proc.returncode == 0 and proc.stdout.strip() != "sha256" + @property def path(self): return self.git.abspath diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/obs_api/package.py new/osc-1.28.0/osc/obs_api/package.py --- old/osc-1.27.3/osc/obs_api/package.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/obs_api/package.py 2026-10-12 10:58:19.000000000 +0200 @@ -164,13 +164,22 @@ return Status.from_file(response, apiurl=apiurl) @classmethod - def get_revision_list(cls, apiurl: str, project: str, package: str, deleted: Optional[bool] = None, meta: Optional[bool] = None): + def get_revision_list( + cls, + apiurl: str, + project: str, + package: str, + deleted: Optional[bool] = None, + meta: Optional[bool] = None, + limit: Optional[int] = None, + ): from ..util.xml import xml_parse url_path = ["source", project, package, "_history"] url_query = { "meta": meta, "deleted": deleted, + "limit": limit, } response = cls.xml_request("GET", apiurl, url_path, url_query) root = xml_parse(response).getroot() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/store.py new/osc-1.28.0/osc/store.py --- old/osc-1.27.3/osc/store.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/store.py 2026-10-12 10:58:19.000000000 +0200 @@ -34,7 +34,12 @@ store = git_scm.GitStore(path, check=check) if print_warnings: git_scm.warn_experimental() - except oscerr.NoWorkingCopy: + except oscerr.NoWorkingCopy as e: + # GitStore raises "is not a Git SCM working copy" if the path is not a git repo. + # Any other NoWorkingCopy error indicates a valid git repo with a specific issue + # (e.g. detached HEAD, missing metadata), which we want to propagate to the user. + if "is not a Git SCM working copy" not in str(e): + raise pass if not store: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/osc/util/git_version.py new/osc-1.28.0/osc/util/git_version.py --- old/osc-1.27.3/osc/util/git_version.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/util/git_version.py 2026-10-12 10:58:19.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.3" + version = "1.28.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.27.3/osc/util/models.py new/osc-1.28.0/osc/util/models.py --- old/osc-1.27.3/osc/util/models.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/osc/util/models.py 2026-10-12 10:58:19.000000000 +0200 @@ -721,6 +721,9 @@ """ Instantiate model from a XML root. """ + if cls.XML_TAG and cls.XML_TAG != root.tag: + print(cls.XML_TAG, root.tag) + raise ValueError(f"{cls.__name__}: Expected '{cls.XML_TAG}' element, found '{root.tag}'") # We need to make sure we parse all data # and that's why we remove processed elements and attributes and check that nothing remains. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/tests/test_git_editor.py new/osc-1.28.0/tests/test_git_editor.py --- old/osc-1.27.3/tests/test_git_editor.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/tests/test_git_editor.py 2026-10-12 10:58:19.000000000 +0200 @@ -6,17 +6,23 @@ from osc.gitea_api.common import get_editor_command +vim_executable = shutil.which("vim") +nano_executable = shutil.which("nano") + + class TestGitEditor(unittest.TestCase): + @unittest.skipIf(vim_executable is None, "vim executable not found") def test_default(self): with unittest.mock.patch.dict(os.environ, {"EDITOR": ""}): c = get_editor_command("test") - self.assertEqual(c, ["/usr/bin/vim", "test"]) + self.assertEqual(c, [vim_executable, "test"]) + @unittest.skipIf(nano_executable is None, "nano executable not found") def test_custom(self): with unittest.mock.patch.dict(os.environ, {"EDITOR": "nano"}): c = get_editor_command("test") - self.assertEqual(c, ["/usr/bin/nano", "test"]) + self.assertEqual(c, [nano_executable, "test"]) def test_custom_with_param(self): with unittest.mock.patch.dict(os.environ, {"EDITOR": "/usr/bin/emacs -nv"}): @@ -33,11 +39,11 @@ c = get_editor_command("test") self.assertEqual(c, ["/usr/bin/mycmd", "-A", "my fancy parameter ", "test"]) + def test_visual(self): + with unittest.mock.patch.dict(os.environ, { "VISUAL": "/usr/bin/emacs", "EDITOR": "/usr/bin/mycmd" }): + c = get_editor_command("test") + self.assertEqual(c, ["/usr/bin/emacs", "test"]) -if shutil.which("nano") != "/usr/bin/nano": - TestGitEditor = unittest.skip("nano is not /usr/bin/nano")(TestGitEditor) -elif shutil.which("vim") != "/usr/bin/vim": - TestGitEditor = unittest.skip("vim is not /usr/bin/vim")(TestGitEditor) if __name__ == "__main__": unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/tests/test_gitea_api_conf.py new/osc-1.28.0/tests/test_gitea_api_conf.py --- old/osc-1.27.3/tests/test_gitea_api_conf.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.28.0/tests/test_gitea_api_conf.py 2026-10-12 10:58:19.000000000 +0200 @@ -0,0 +1,32 @@ +import os +import unittest +from unittest.mock import patch + +from osc.gitea_api.conf import Config + + +class TestGiteaApiConfig(unittest.TestCase): + def test_xdg_config_home(self): + with patch.dict(os.environ, {"XDG_CONFIG_HOME": "/custom/config"}, clear=True): + self.assertEqual(Config().path, "/custom/config/tea/config.yml") + + def test_default_config_home(self): + with patch.dict(os.environ, {}, clear=True): + self.assertEqual(Config().path, os.path.expanduser("~/.config/tea/config.yml")) + + def test_empty_config_home(self): + with patch.dict(os.environ, {"XDG_CONFIG_HOME": ""}, clear=True): + self.assertEqual(Config().path, os.path.expanduser("~/.config/tea/config.yml")) + + def test_relative_config_home(self): + with patch.dict(os.environ, {"XDG_CONFIG_HOME": "relative/config"}, clear=True): + with patch("os.getcwd", return_value="/worktree"): + self.assertEqual(Config().path, "/worktree/relative/config/tea/config.yml") + + def test_tilde_config_home(self): + with patch.dict(os.environ, {"XDG_CONFIG_HOME": "~/foo"}, clear=True): + self.assertEqual(Config().path, os.path.expanduser("~/foo/tea/config.yml")) + + def test_explicit_path_takes_precedence(self): + with patch.dict(os.environ, {"XDG_CONFIG_HOME": "/custom/config"}, clear=True): + self.assertEqual(Config("~/custom-tea.yml").path, os.path.expanduser("~/custom-tea.yml")) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/tests/test_gitea_api_tardiff.py new/osc-1.28.0/tests/test_gitea_api_tardiff.py --- old/osc-1.27.3/tests/test_gitea_api_tardiff.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.28.0/tests/test_gitea_api_tardiff.py 2026-10-12 10:58:19.000000000 +0200 @@ -0,0 +1,38 @@ +import os +import shutil +import subprocess +import tempfile +import unittest + +from osc.gitea_api import TarDiff + + [email protected](not shutil.which("git"), "The 'git' executable is not available") +class TestGiteaApiTarDiff(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix="osc_test_tardiff_") + + def tearDown(self): + try: + shutil.rmtree(self.tmpdir) + except OSError: + pass + + def test_init_recreates_stale_sha1_cache(self): + subprocess.check_call(["git", "init", "-q"], cwd=self.tmpdir) + with open(os.path.join(self.tmpdir, "stale.txt"), "w", encoding="utf-8") as f: + f.write("stale cache contents") + + TarDiff(self.tmpdir) + + object_format = subprocess.check_output( + ["git", "rev-parse", "--show-object-format"], + cwd=self.tmpdir, + encoding="utf-8", + ).strip() + self.assertEqual(object_format, "sha256") + self.assertFalse(os.path.exists(os.path.join(self.tmpdir, "stale.txt"))) + + +if __name__ == "__main__": + unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/tests/test_request_interactive_review.py new/osc-1.28.0/tests/test_request_interactive_review.py --- old/osc-1.27.3/tests/test_request_interactive_review.py 1970-01-01 01:00:00.000000000 +0100 +++ new/osc-1.28.0/tests/test_request_interactive_review.py 2026-10-12 10:58:19.000000000 +0200 @@ -0,0 +1,195 @@ +import io +import os +import unittest +from contextlib import redirect_stderr, redirect_stdout +from unittest.mock import patch + +from osc import core + +from .common import OscTestCase + + +class TestRequestInteractiveReview(unittest.TestCase): + apiurl = 'https://api.example.com' + results = b'''<resultlist> + <result project="source" repository="standard" arch="x86_64"> + <status package="foo" code="succeeded"/> + <status package="foo:test" code="failed"/> + <status package="foo:disabled" code="disabled"/> + <status package="foo:excluded" code="excluded"/> + </result> + <result project="source" repository="standard" arch="aarch64"> + <status package="foo:test" code="building"/> + </result> + <result project="source" repository="broken" arch="x86_64" code="broken"/> + </resultlist>''' + + def setUp(self): + self.request = core.Request() + self.request.reqid = '123' + self.request.add_action('submit', src_project='source', src_package='foo', + tgt_project='target', tgt_package='foo') + # Request rendering and comments are unrelated to log selection. + self.patchers = [ + patch.object(core.Request, '__str__', return_value='request 123'), + patch.object(core, 'print_comments'), + patch.object(core, 'run_pager'), + ] + for patcher in self.patchers: + patcher.start() + self.addCleanup(patcher.stop) + + def review(self, replies, **kwargs): + output = io.StringIO() + with patch.object(core, 'raw_input', side_effect=replies), \ + redirect_stdout(output), redirect_stderr(io.StringIO()): + core.request_interactive_review(self.apiurl, self.request, **kwargs) + return output.getvalue() + + def test_accept_message_template(self): + self.request.state = core.RequestState(core.xml_fromstring('<state name="new"/>')) + for message, expected in [('ok', 'ok'), (None, 'configured approval'), ('', '')]: + with self.subTest(message=message), \ + patch.object(core, 'change_request_state_template', return_value='configured approval'), \ + patch.object(core, 'edit_message', return_value='edited approval') as editor, \ + patch.object(core, 'change_request_state', return_value=True) as change_state, \ + patch('osc._private.forward_request'): + self.review(['a'], message=message) + self.assertEqual(editor.call_args[1]['template'], expected) + self.assertEqual(change_state.call_args[0], + (self.apiurl, '123', 'accepted', 'edited approval')) + + def test_inline_accept_message_overrides_default(self): + with patch.object(core, 'edit_message') as editor, \ + patch.object(core, 'change_request_state', return_value=True) as change_state, \ + patch('osc._private.forward_request'): + self.review(['a -m explicit approval'], message='ok') + editor.assert_not_called() + self.assertEqual(change_state.call_args[0], + (self.apiurl, '123', 'accepted', 'explicit approval')) + + def test_accept_message_does_not_override_decline_template(self): + self.request.state = core.RequestState(core.xml_fromstring('<state name="new"/>')) + with patch.object(core, 'change_request_state_template', return_value='decline reason'), \ + patch.object(core, 'edit_message', return_value='edited reason') as editor, \ + patch.object(core, 'change_request_state', return_value=True): + self.review(['d'], message='ok') + self.assertEqual(editor.call_args[1]['template'], 'decline reason') + + def test_buildlog_flavor(self): + with patch.object(core, 'get_package_results', return_value=iter([self.results])) as results, \ + patch.object(core, 'print_buildlog') as buildlog: + output = self.review(['bl', '1', 's']) + results.assert_called_once_with(self.apiurl, 'source', 'foo', multibuild=True) + self.assertEqual(buildlog.call_args[0], + (self.apiurl, 'source', 'foo:test', 'standard', 'x86_64')) + self.assertIn('(0) source/foo/standard/x86_64', output) + self.assertIn('(1) source/foo:test/standard/x86_64', output) + self.assertIn('(2) source/foo:test/standard/aarch64', output) + self.assertNotIn('foo:disabled', output) + self.assertNotIn('foo:excluded', output) + self.assertNotIn('/broken/', output) + + def test_buildlog_base_package(self): + results_xml = b'''<resultlist><result repository="standard" arch="x86_64"> + <status package="foo" code="succeeded"/> + </result></resultlist>''' + with patch.object(core, 'get_package_results', return_value=iter([results_xml])), \ + patch.object(core, 'print_buildlog') as buildlog: + self.review(['bl', '0', 's']) + self.assertEqual(buildlog.call_args[0], + (self.apiurl, 'source', 'foo', 'standard', 'x86_64')) + + def test_rpmlint_flavor(self): + with patch.object(core, 'get_package_results', return_value=iter([self.results])), \ + patch.object(core, 'get_rpmlint_log', return_value='lint log') as lintlog: + self.review(['li', '1', 's']) + lintlog.assert_called_once_with(self.apiurl, proj='source', pkg='foo:test', + repo='standard', arch='x86_64') + + def test_empty_results(self): + with patch.object(core, 'get_package_results', return_value=iter([b'<resultlist/>'])), \ + patch.object(core, 'print_buildlog') as buildlog: + output = self.review(['bl', 's']) + self.assertIn('No repos', output) + buildlog.assert_not_called() + + def test_multiple_actions(self): + self.request.add_action('submit', src_project='other', src_package='bar', + tgt_project='target', tgt_package='bar') + other_results = b'''<resultlist><result repository="ports" arch="aarch64"> + <status package="bar:test" code="failed"/> + </result></resultlist>''' + with patch.object(core, 'get_package_results', + side_effect=[iter([self.results]), iter([other_results])]), \ + patch.object(core, 'print_buildlog') as buildlog: + self.review(['bl', '3', 's']) + self.assertEqual(buildlog.call_args[0], + (self.apiurl, 'other', 'bar:test', 'ports', 'aarch64')) + + def test_missing_buildlog(self): + error = core.HTTPError('https://api.example.com', 404, 'Not Found', {}, None) + with patch.object(core, 'get_package_results', return_value=iter([self.results])), \ + patch.object(core, 'print_buildlog', side_effect=error): + output = self.review(['bl', '1', 's']) + self.assertIn('No build log for standard/x86_64', output) + + def test_buildstatus_includes_multibuild(self): + with patch.object(core, 'get_results', return_value=['foo:test failed']) as results: + output = self.review(['b', 's']) + results.assert_called_once_with(self.apiurl, 'source', 'foo', multibuild=True) + self.assertIn('foo:test failed', output) + + def test_initial_buildstatus_includes_multibuild(self): + with patch.object(core, 'get_results', return_value=['foo:test failed']) as results: + output = self.review(['s'], source_buildstatus=True) + results.assert_called_once_with(self.apiurl, 'source', 'foo', multibuild=True) + self.assertIn('foo:test failed', output) + + +class TestRequestInteractiveReviewCommandline(OscTestCase): + def _get_fixtures_dir(self): + return os.path.join(os.path.dirname(__file__), 'request_fixtures') + + def setUp(self): + super().setUp(copytree=False) + self.request = core.Request() + self.request.reqid = '123' + self.request.state = core.RequestState(core.xml_fromstring('<state name="new"/>')) + self.request.add_action('submit', src_project='source', src_package='foo', + tgt_project='target', tgt_package='foo') + + def test_list_forwards_message(self): + for subcmd in ('request', 'review'): + for message in ('ok', '', None): + with self.subTest(subcmd=subcmd, message=message), \ + patch.object(core, 'get_request_collection', return_value=[self.request]), \ + patch.object(core, 'get_review_list', return_value=[self.request]), \ + patch.object(core, 'request_interactive_review') as review: + args = [subcmd, 'list', '-i', '-U', 'Admin', '-D', '0'] + if message is not None: + args.extend(['-m', message]) + self._run_osc(*args) + review.assert_called_once_with('http://localhost', self.request, group=None, + ignore_reviews=subcmd != 'review', + source_buildstatus=False, message=message) + + def test_show_forwards_message(self): + for subcmd in ('request', 'review'): + with self.subTest(subcmd=subcmd), \ + patch.object(core, 'get_request', return_value=self.request), \ + patch.object(core, 'request_interactive_review') as review: + self._run_osc(subcmd, 'show', '-i', '--source-buildstatus', '-m', 'ok', '123') + review.assert_called_once_with('http://localhost', self.request, group=None, + ignore_reviews=subcmd != 'review', + source_buildstatus=True, message='ok') + + def test_edit_forwards_message(self): + with patch.object(core, 'get_request', return_value=self.request), \ + patch.object(core, 'request_interactive_review') as review: + self._run_osc('request', 'show', '--edit', '-m', 'ok', '123') + review.assert_called_once_with('http://localhost', self.request, 'e', message='ok') + + +if __name__ == '__main__': + unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/osc-1.27.3/tests/test_store.py new/osc-1.28.0/tests/test_store.py --- old/osc-1.27.3/tests/test_store.py 2026-08-03 16:54:23.000000000 +0200 +++ new/osc-1.28.0/tests/test_store.py 2026-10-12 10:58:19.000000000 +0200 @@ -1,4 +1,5 @@ import os +import shutil import sys import tempfile import unittest @@ -18,10 +19,7 @@ self.store.package = "package name" def tearDown(self): - try: - shutil.rmtree(self.tmpdir) - except: - pass + shutil.rmtree(self.tmpdir, ignore_errors=True) def fileEquals(self, fn, expected_value): path = os.path.join(self.tmpdir, ".osc", fn) @@ -252,5 +250,58 @@ self.assertTrue(os.path.exists(os.path.join(self.tmpdir, ".osc", "sources", "sources"))) +class TestGetStore(unittest.TestCase): + def setUp(self): + import shutil + self.tmpdir = tempfile.mkdtemp(prefix='osc_test') + + def tearDown(self): + import shutil + shutil.rmtree(self.tmpdir, ignore_errors=True) + + def _git_init(self, path, *, branch="master"): + import subprocess + os.makedirs(path, exist_ok=True) + cmd = ["git", "init"] + if branch: + cmd += ["-b", branch] + subprocess.check_call(cmd, cwd=path) + subprocess.check_call(["git", "config", "user.name", "Test User"], cwd=path) + subprocess.check_call(["git", "config", "user.email", "[email protected]"], cwd=path) + + def test_get_store_generic_dir(self): + from osc.store import get_store + from osc import oscerr + with self.assertRaises(oscerr.NoWorkingCopy) as ctx: + get_store(self.tmpdir, check=True) + self.assertIn("is not a working copy", str(ctx.exception)) + + def test_get_store_git_branch(self): + from osc.store import get_store + from osc import oscerr + self._git_init(self.tmpdir, branch="master") + with self.assertRaises(oscerr.NoWorkingCopy) as ctx: + get_store(self.tmpdir, check=True) + # Should raise detailed metadata error because it's inside a git repo + self.assertIn("Git SCM package working copy doesn't have the following metadata set", str(ctx.exception)) + + def test_get_store_git_detached_head(self): + from osc.store import get_store + from osc import oscerr + import subprocess + self._git_init(self.tmpdir, branch="master") + # Create an initial commit to allow checkout + with open(os.path.join(self.tmpdir, "file.txt"), "w") as f: + f.write("test") + subprocess.check_call(["git", "add", "file.txt"], cwd=self.tmpdir) + subprocess.check_call(["git", "commit", "-m", "initial"], cwd=self.tmpdir) + # Detach HEAD + subprocess.check_call(["git", "checkout", "HEAD~0"], cwd=self.tmpdir) + + with self.assertRaises(oscerr.NoWorkingCopy) as ctx: + get_store(self.tmpdir, check=True) + self.assertIn("detached HEAD state", str(ctx.exception)) + + if __name__ == "__main__": unittest.main() ++++++ osc.dsc ++++++ --- /var/tmp/diff_new_pack.dfyYjT/_old 2026-10-12 22:07:34.737204729 +0200 +++ /var/tmp/diff_new_pack.dfyYjT/_new 2026-10-12 22:07:34.742204936 +0200 @@ -1,6 +1,6 @@ Format: 1.0 Source: osc -Version: 1.27.3-0 +Version: 1.28.0-0 Binary: osc Maintainer: Adrian Schroeter <[email protected]> Architecture: any
