From ece94c918a77e54cf9e2ca4b1d375452554517c9 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Tue, 3 Mar 2026 16:14:35 +0100 Subject: [PATCH 1/2] Change 'git-obs staging group' to rely on the workflow-pr bot --- behave/features/git-obs-staging-group.feature | 127 +++++++ osc/commands_git/staging_group.py | 349 ++++++++---------- 2 files changed, 281 insertions(+), 195 deletions(-) create mode 100644 behave/features/git-obs-staging-group.feature diff --git a/behave/features/git-obs-staging-group.feature b/behave/features/git-obs-staging-group.feature new file mode 100644 index 00000000..d6c3f2bc --- /dev/null +++ b/behave/features/git-obs-staging-group.feature @@ -0,0 +1,127 @@ +Feature: `git-obs staging group --no-ssh-strict-host-key-checking` command + +Background: + Given I set working directory to "{context.osc.temp}" + # it's meant to work with project pull requests, but we'll work with pool/test-GitPkgA for simplicity + And I execute git-obs with args "-G alice repo fork pool/test-GitPkgA" + And I execute git-obs with args "-G alice repo clone Alice/test-GitPkgA --no-ssh-strict-host-key-checking" + And I set working directory to "{context.osc.temp}/test-GitPkgA" + And I execute "git config user.email 'alice@example.com'" + And I execute "git config user.name 'Alice'" + + # Create labels + And I execute git-obs with args "api -X POST /repos/pool/test-GitPkgA/labels --data='{{"name": "staging/Backlog", "color": "#ffffff"}}'" + And I execute git-obs with args "api -X POST /repos/pool/test-GitPkgA/labels --data='{{"name": "staging/In Progress", "color": "#0000ff"}}'" + + # Alice makes a new branch feature/1, no changes on top, and makes a pull request with "PR: foo/bar!1" description + And I execute "git checkout -b feature/1" + And I execute "git commit --allow-empty -m 'feature/1'" + And I execute "git push origin feature/1" + And I execute git-obs with args "-G alice pr create --title 'feature/1' --description='PR: foo/bar!1' --target-branch factory" + And I execute git-obs with args "api -X POST /repos/pool/test-GitPkgA/issues/1/labels --data='{{"labels": ["staging/Backlog"]}}'" + + # Alice makes a new branch feature/2, no changes on top, and makes a pull request with "PR: foo/bar!2" description + And I execute "git checkout factory" + And I execute "git checkout -b feature/2" + And I execute "git commit --allow-empty -m 'feature/2'" + And I execute "git push origin feature/2" + And I execute git-obs with args "-G alice pr create --title 'feature/2' --description='PR: foo/bar!2' --target-branch factory" + And I execute git-obs with args "api -X POST /repos/pool/test-GitPkgA/issues/2/labels --data='{{"labels": ["staging/Backlog"]}}'" + +@destructive +Scenario: Scenario 1: staging group --no-ssh-strict-host-key-checking with --target and existing PR + When I execute git-obs with args "-G alice staging group --no-ssh-strict-host-key-checking --target=pool/test-GitPkgA#1 pool/test-GitPkgA#2" + Then the exit code is 0 + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#1" + Then the exit code is 0 + And stdout contains "State : open" + And stdout contains "Description : PR: foo/bar!1" + # It should also contain the reference from #2 + And stdout contains "PR: foo/bar!2" + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#2" + Then the exit code is 0 + And stdout contains "State : closed" + And stdout contains "Description : PR: foo/bar!2" + +@destructive +Scenario: Scenario 2: staging group --no-ssh-strict-host-key-checking with --target and --remove-pr-references + When I execute git-obs with args "-G alice staging group --no-ssh-strict-host-key-checking --target=pool/test-GitPkgA#1 pool/test-GitPkgA#2 --remove-pr-references" + Then the exit code is 0 + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#2" + Then the exit code is 0 + And stdout contains "State : closed" + And stdout doesn't contain "PR: foo/bar!2" + +@destructive +Scenario: Scenario 3: staging group --no-ssh-strict-host-key-checking without --target (creates new PR) + When I execute git-obs with args "-G alice staging group --no-ssh-strict-host-key-checking pool/test-GitPkgA#1 pool/test-GitPkgA#2" + Then the exit code is 0 + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#3" + Then the exit code is 0 + And stdout contains "State : open" + And stdout contains "PR: foo/bar!1" + And stdout contains "PR: foo/bar!2" + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#1" + Then the exit code is 0 + # PR #1 should be closed because it was merged into #3 + And stdout contains "State : closed" + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#2" + Then the exit code is 0 + # PR #2 should be closed because it was merged into #3 + And stdout contains "State : closed" + +@destructive +Scenario: Scenario 4: staging group --no-ssh-strict-host-key-checking without --target and --remove-pr-references + When I execute git-obs with args "-G alice staging group --no-ssh-strict-host-key-checking pool/test-GitPkgA#1 pool/test-GitPkgA#2 --remove-pr-references" + Then the exit code is 0 + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#3" + Then the exit code is 0 + And stdout contains "PR: foo/bar!1" + And stdout contains "PR: foo/bar!2" + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#1" + Then the exit code is 0 + And stdout contains "State : closed" + And stdout doesn't contain "PR: foo/bar!1" + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#2" + Then the exit code is 0 + And stdout contains "State : closed" + And stdout doesn't contain "PR: foo/bar!2" + +@destructive +Scenario: Scenario 5: staging group --no-ssh-strict-host-key-checking by another user with push permissions + And I execute git-obs with args "-G alice api -X PUT /repos/Alice/test-GitPkgA/collaborators/Bob" + And I set working directory to "{context.osc.temp}" + # Bob clones Alice's fork + And I execute git-obs with args "-G bob repo clone Alice/test-GitPkgA --directory Alice-test-GitPkgA --no-ssh-strict-host-key-checking" + And I set working directory to "{context.osc.temp}/Alice-test-GitPkgA" + And I execute "git config user.email 'bob@example.com'" + And I execute "git config user.name 'Bob'" + When I execute git-obs with args "-G bob staging group --no-ssh-strict-host-key-checking pool/test-GitPkgA#1 pool/test-GitPkgA#2 --fork-owner=Alice" + Then the exit code is 0 + And I execute git-obs with args "-G bob pr get pool/test-GitPkgA#3" + Then the exit code is 0 + And stdout contains "State : open" + And stdout contains "Author : Bob \(bob@example.com\)" + And stdout contains "PR: foo/bar!1" + And stdout contains "PR: foo/bar!2" + +@destructive +Scenario: Scenario 6: staging group --no-ssh-strict-host-key-checking by another user with push permissions to the target repository + And I execute git-obs with args "-G admin api -X PUT /repos/pool/test-GitPkgA/collaborators/Bob" + And I set working directory to "{context.osc.temp}" + # Bob clones the target repository + And I execute git-obs with args "-G bob repo clone pool/test-GitPkgA --directory pool-test-GitPkgA --no-ssh-strict-host-key-checking" + And I set working directory to "{context.osc.temp}/pool-test-GitPkgA" + # Bob's git identity + And I execute "git config user.email 'bob@example.com'" + And I execute "git config user.name 'Bob'" + When I execute git-obs with args "-G bob staging group --no-ssh-strict-host-key-checking pool/test-GitPkgA#1 pool/test-GitPkgA#2" + Then the exit code is 0 + And I execute git-obs with args "-G bob pr get pool/test-GitPkgA#3" + Then the exit code is 0 + And stdout contains "State : open" + # pr get output shows user login + And stdout contains "Author : Bob \(bob@example.com\)" + And stdout contains "Source : pool/test-GitPkgA" + And stdout contains "PR: foo/bar!1" + And stdout contains "PR: foo/bar!2" diff --git a/osc/commands_git/staging_group.py b/osc/commands_git/staging_group.py index 94719bf5..8acf1c90 100644 --- a/osc/commands_git/staging_group.py +++ b/osc/commands_git/staging_group.py @@ -51,14 +51,14 @@ class StagingGroupCommand(osc.commandline_git.GitObsCommand): ).completer = osc.commandline_git.complete_pr self.add_argument( - "--cache-dir", - help="Path to a git cache.", + "--no-ssh-strict-host-key-checking", + action="store_true", + help="Set 'StrictHostKeyChecking no' ssh option", ) self.add_argument( - "--keep-temp-dir", - action="store_true", - help="Don't delete the temporary directory with git checkouts", + "--cache-dir", + help="Path to a git cache.", ) def run(self, args): @@ -82,207 +82,166 @@ class StagingGroupCommand(osc.commandline_git.GitObsCommand): self.print_gitea_settings() - with TemporaryDirectory(prefix="git-obs-staging_", dir=".", delete=not args.keep_temp_dir) as temp_dir: - user_obj = gitea_api.User.get(self.gitea_conn) + pr_obj_list = [] + pr_references = [] + target_owner = None + target_repo = None + target_branch = None + for owner, repo, number in args.pr_list: + pr_obj = gitea_api.PullRequest.get(self.gitea_conn, owner, repo, number) - if args.target: - target_owner, target_repo, target_number = args.target - pr_obj = gitea_api.PullRequest.get(self.gitea_conn, target_owner, target_repo, target_number) - # # to update a pull request, we either need to be its creator or an admin in the repo - # if not (pr_obj._data["base"]["repo"]["permissions"]["admin"] or pr_obj.user == user_obj.login): - # raise gitea_api.GitObsRuntimeError(f"You don't have sufficient permissions to modify pull request {target_owner}/{target_repo}#{target_number}") + if pr_obj.state != "open": + # we don't care about the state of the package pull requests - they can be merged already + raise gitea_api.GitObsRuntimeError(f"Pull request {owner}/{repo}#{number} is not open (the state is '{pr_obj.state}')") - # get pull request data from gitea - pr_map = {} - for owner, repo, number in args.pr_list: - pr = gitea_api.StagingPullRequestWrapper(self.gitea_conn, owner, repo, number, topdir=temp_dir, cache_directory=cache_dir) - pr_map[(owner.lower(), repo.lower(), number)] = pr - - # run checks - target_owner = None - target_repo = None - target_branch = None - for owner, repo, number in args.pr_list: - pr = pr_map[(owner.lower(), repo.lower(), number)] - - if pr.pr_obj.state != "open": - # we don't care about the state of the package pull requests - they can be merged already - raise gitea_api.GitObsRuntimeError(f"Pull request {owner}/{repo}#{number} is not open (the state is '{pr.pr_obj.state}')") - - # if not (pr.pr_obj._data["base"]["repo"]["permissions"]["admin"] or pr.pr_obj.user == user_obj.login): - # raise gitea_api.GitObsRuntimeError(f"You don't have sufficient permissions to modify pull request {owner}/{repo}#{number}") - - if gitea_api.StagingPullRequestWrapper.BACKLOG_LABEL not in pr.pr_obj.labels: - raise gitea_api.GitObsRuntimeError(f"Pull request {owner}/{repo}#{number} is missing the '{gitea_api.StagingPullRequestWrapper.BACKLOG_LABEL}' label.") - - # test that all PRs go to the same branch - if target_owner is None: - target_owner = pr.pr_obj.base_owner - else: - assert target_owner == pr.pr_obj.base_owner, f"{target_owner} != {pr.pr_obj.base_owner}" - - if target_repo is None: - target_repo = pr.pr_obj.base_repo - else: - assert target_repo == pr.pr_obj.base_repo, f"{target_repo} != {pr.pr_obj.base_repo}" - - if target_branch is None: - target_branch = pr.pr_obj.base_branch - else: - assert target_branch == pr.pr_obj.base_branch, f"{target_branch} != {pr.pr_obj.base_branch}" - - # clone the git repos, cache submodule data - for owner, repo, number in args.pr_list: - pr = pr_map[(owner.lower(), repo.lower(), number)] - pr.clone() - - # run checks #2 - for owner, repo, number in args.pr_list: - pr = pr_map[(owner.lower(), repo.lower(), number)] - if not pr.package_pr_map: - # TODO: we don't know if the submodules are packages or not, we should cross-reference those with _manifest - raise gitea_api.GitObsRuntimeError(f"Pull request {owner}/{repo}#{number} doesn't have any submodules changed.") - - if not args.target: - target_repo_full_name = f"{target_owner}/{target_repo}".lower() - - # determine fork_owner and fork_repo for pull request creation - has_push_access = False - - user_repos = gitea_api.Repo.list_my_repos(self.gitea_conn) - for repo in user_repos: - repo_name = repo._data["full_name"] - if target_repo_full_name == repo_name.lower() and repo.can_push: - has_push_access = True - print(f"You have push access to the target repository {target_owner}/{target_repo}, the pull request will be created from a branch in the target repository.") - break - - if has_push_access and not args.fork_owner: - fork_owner = target_owner - fork_repo = target_repo - else: - fork_owner = args.fork_owner if args.fork_owner else user_obj.login - fork_repo = None - forks = gitea_api.Fork.list(self.gitea_conn, target_owner, target_repo) - for repo in forks: - if repo.owner.lower() == fork_owner.lower(): - fork_repo = repo.repo - break - if not fork_repo: - raise gitea_api.GitObsRuntimeError(f"Cannot find a matching fork of {target_owner}/{target_repo} for user {fork_owner}") - - # dates in ISO 8601 format cannot be part of a valid branch name, we need a custom format - fork_branch = args.fork_branch if args.fork_branch else f"for/{target_branch}/group-{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}" - - - clone_dir = gitea_api.Repo.clone( - self.gitea_conn, - fork_owner, - fork_repo, - directory=os.path.join(temp_dir, f"{fork_owner}_{fork_repo}"), - add_remotes=True, - cache_directory=cache_dir, - ssh_private_key_path=self.gitea_conn.login.ssh_key, - ) - clone_git = gitea_api.Git(clone_dir) - clone_git._run_git(["fetch", "origin", f"{target_branch}:{fork_branch}", "--force", "--update-head-ok", "--depth=1"]) - clone_git.switch(fork_branch) - clone_git.push(remote="origin", branch=fork_branch, set_upstream=True, force=args.force) - - # target project pull request wasn't specified, let's create it - desc = "" - updated_packages = [] - for owner, repo, number in args.pr_list: - pr = pr_map[(owner.lower(), repo.lower(), number)] - for (pkg_owner, pkg_repo, pkg_number), pr_obj in pr.package_pr_map.items(): - desc += f"PR: {pkg_owner}/{pkg_repo}!{pkg_number}\n" - updated_packages.append(os.path.basename(pr.submodules_by_owner_repo[pkg_owner.lower(), pkg_repo.lower()]["path"])) - - # TODO: it would be nice to mention the target OBS project - # we keep only the first ``max_packages``, because the title might get too long quite easily - max_packages = 5 - updated_packages_str = ", ".join(sorted(updated_packages)[:max_packages]) - if len(updated_packages) > max_packages: - updated_packages_str += f" + {len(updated_packages) - max_packages} more" - title = args.title if args.title else f"Update packages: {updated_packages_str}" - - pr_obj = gitea_api.PullRequest.create( - self.gitea_conn, - target_owner=target_owner, - target_repo=target_repo, - target_branch=target_branch, - source_owner=fork_owner, - # source_repo is not required because the information lives in Gitea database - source_branch=fork_branch, - title=title, - description=desc, - labels=[gitea_api.StagingPullRequestWrapper.INPROGRESS_LABEL], - ) - target_number = pr_obj.number - - # clone the target git repo, cache submodule data - target = gitea_api.StagingPullRequestWrapper(self.gitea_conn, target_owner, target_repo, target_number, topdir=temp_dir, cache_directory=cache_dir) - target.clone() - - has_push_access = False - if target.pr_obj.head_can_push: - print(f"You have push access to the head repository of the target pull request {target_owner}/{target_repo}#{target_number}, the pull request will be updated by pushing to the head branch.") - has_push_access = True - - if target.pr_obj._data['head']['repo']['fork'] and has_push_access: - # if the head repo is a fork and we have push access to it, we can push directly to the head branch - target.git._run_git(["remote", "set-url", "fork", target.pr_obj._data['head']['repo']['ssh_url']]) - - # locally merge package pull requests to the target project pull request (don't change anything on server yet) - updated_packages = [] - for owner, repo, number in args.pr_list: - pr = pr_map[(owner.lower(), repo.lower(), number)] - target.merge(pr) - for (pkg_owner, pkg_repo, pkg_number), pr_obj in pr.package_pr_map.items(): - updated_packages.append(os.path.basename(pr.submodules_by_owner_repo[pkg_owner.lower(), pkg_repo.lower()]["path"])) - - if target.pr_obj._data['head']['repo']['fork']: - remote="fork" + # test that all PRs go to the same branch + if target_owner is None: + target_owner = pr_obj.base_owner else: - remote="origin" + assert target_owner == pr_obj.base_owner, f"{target_owner} != {pr_obj.base_owner}" - # push to git repo associated with the target pull request - print(f"Pushing changes to {remote} on pull/{target.pr_obj.number}:{target.pr_obj.head_branch}") - target.git.push(remote=remote, branch=f"pull/{target.pr_obj.number}:{target.pr_obj.head_branch}") - # update target pull request - if args.target: - # we keep only the first ``max_packages``, because the title might get too long quite easily - max_packages = 5 - updated_packages_str = ", ".join(sorted(updated_packages)[:max_packages]) - if len(updated_packages) > max_packages: - updated_packages_str += f" + {len(updated_packages) - max_packages} more" - title = args.title if args.title else f"{target.pr_obj.title}, {updated_packages_str}" + if target_repo is None: + target_repo = pr_obj.base_repo + else: + assert target_repo == pr_obj.base_repo, f"{target_repo} != {pr_obj.base_repo}" - # if args.target is not set, we've created a new pull request with all 'PR:' references included - # if args.target is set (which is the case here), we need to update the description with the newly added 'PR:' references - target.pr_obj.set(self.gitea_conn, target_owner, target_repo, target_number, title=title, description=target.pr_obj.body) + if target_branch is None: + target_branch = pr_obj.base_branch + else: + assert target_branch == pr_obj.base_branch, f"{target_branch} != {pr_obj.base_branch}" - for owner, repo, number in args.pr_list: - pr = pr_map[(owner.lower(), repo.lower(), number)] - if args.remove_pr_references: - try: - # apply the removed 'PR:' reference to the package pull request - pr.pr_obj.set(self.gitea_conn, owner, repo, number, description=pr.pr_obj.body) - except Exception as e: - print(f"Unable to remove 'PR:' references from pull request {owner}/{repo}#{number}: {e}") + if gitea_api.StagingPullRequestWrapper.BACKLOG_LABEL not in pr_obj.labels: + raise gitea_api.GitObsRuntimeError(f"Pull request {owner}/{repo}#{number} is missing the '{gitea_api.StagingPullRequestWrapper.BACKLOG_LABEL}' label.") - # close the pull request that was merged into the target + pr_obj_list.append(pr_obj) + pr_references.extend(pr_obj.parse_pr_references()) + + # deduplicate entries + pr_references = sorted(set(pr_references)) + + # create title + if args.title: + title = args.title + else: + updated_packages = sorted([i[1] for i in pr_references]) + # TODO: it would be nice to mention the target OBS project + # we keep only the first ``max_packages``, because the title might get too long quite easily + max_packages = 5 + updated_packages_str = ", ".join(sorted(updated_packages)[:max_packages]) + if len(updated_packages) > max_packages: + updated_packages_str += f" + {len(updated_packages) - max_packages} more" + title = f"Update packages: {updated_packages_str}" + + if args.target: + target_owner, target_repo, target_number = args.target + target_pr_obj = gitea_api.PullRequest.get(self.gitea_conn, target_owner, target_repo, target_number) + + # extend the 'PR: ' references in the target pull request + target_pr_obj._data["body"] = gitea_api.PullRequest.add_pr_references(target_pr_obj.body, pr_references) + + # update description of the target pull request in Gitea + # it is crucial to allow maintainer edits so the bot can push the related PRs + target_pr_obj = gitea_api.PullRequest.set( + self.gitea_conn, + target_pr_obj.base_owner, + target_pr_obj.base_repo, + int(target_pr_obj.number), + title=title, + description=target_pr_obj.body, + allow_maintainer_edit=True, + ) + + else: + has_push_access = False + if not args.fork_owner: + target_repo_obj = gitea_api.Repo.get(self.gitea_conn, target_owner, target_repo) + # determine if we have write access to the target repo and create the branch there if possible + if target_repo_obj.can_push: + print(f"You have push access to the target repository {target_owner}/{target_repo}, the pull request will be created from a branch in the target repository.") + has_push_access = True + + fork_branch = args.fork_branch if args.fork_branch else f"for/{target_branch}/group-{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}" + + if has_push_access: + fork_owner = target_owner + fork_repo = target_repo + else: + user_obj = gitea_api.User.get(self.gitea_conn) + fork_owner = args.fork_owner if args.fork_owner else user_obj.login + + fork_repo = None + forks = gitea_api.Fork.list(self.gitea_conn, target_owner, target_repo) + for repo in forks: + if repo.owner.lower() == fork_owner.lower(): + fork_repo = repo.repo + if not fork_repo: + raise gitea_api.GitObsRuntimeError(f"Cannot find a matching fork of {target_owner}/{target_repo} for user {fork_owner}") + + # create the branch + if has_push_access: + gitea_api.Branch.create(self.gitea_conn, target_owner, target_repo, old_ref_name=target_branch, new_branch_name=fork_branch) + else: + fork_repo_obj = gitea_api.Repo.get(self.gitea_conn, fork_owner, fork_repo) + with TemporaryDirectory(prefix="git-obs-staging_", dir=".") as temp_dir: + # we need to create a branch that matches the target branch using git; Gitea doesn't have any API for that + clone_dir = gitea_api.Repo.clone( + self.gitea_conn, + fork_owner, + fork_repo, + directory=os.path.join(temp_dir, f"{fork_owner}_{fork_repo}"), + add_remotes=False, + cache_directory=cache_dir, + ssh_private_key_path=self.gitea_conn.login.ssh_key, + ssh_strict_host_key_checking=not(args.no_ssh_strict_host_key_checking), + ) + clone_git = gitea_api.Git(clone_dir) + clone_git._run_git(["fetch", "origin", f"{target_branch}:{fork_branch}", "--force", "--update-head-ok", "--depth=1"]) + clone_git.switch(fork_branch) + clone_git.add_remote("fork", fork_repo_obj.ssh_url) + clone_git.push(remote="fork", branch=fork_branch, set_upstream=True, force=args.force) + + # target project pull request wasn't specified, let's create it + desc = gitea_api.PullRequest.add_pr_references("", pr_references) + target_pr_obj = gitea_api.PullRequest.create( + self.gitea_conn, + target_owner=target_owner, + target_repo=target_repo, + target_branch=target_branch, + source_owner=fork_owner, + # source_repo is not required because the information lives in Gitea database + source_branch=fork_branch, + title=title, + description=desc, + labels=[gitea_api.StagingPullRequestWrapper.INPROGRESS_LABEL], + ) + + # it is crucial to allow maintainer edits so the bot can push the related PRs + target_pr_obj = gitea_api.PullRequest.set( + self.gitea_conn, + target_pr_obj.base_owner, + target_pr_obj.base_repo, + int(target_pr_obj.number), + allow_maintainer_edit=True, + ) + + for pr_obj in pr_obj_list: + if args.remove_pr_references: try: - gitea_api.PullRequest.close(self.gitea_conn, owner, repo, number) + # remove the 'PR:' references from the original pull request + refs = pr_obj.parse_pr_references() + body = gitea_api.PullRequest.remove_pr_references(pr_obj.body, refs) + pr_obj.set(self.gitea_conn, pr_obj.base_owner, pr_obj.base_repo, pr_obj.number, description=body) except Exception as e: - print(f"Unable to close pull request {owner}/{repo}#{number}: {e}") + print(f"Unable to remove 'PR:' references from pull request {pr_obj.id}: {e}") + + try: + # close the pull request that was merged into the target + gitea_api.PullRequest.close(self.gitea_conn, pr_obj.base_owner, pr_obj.base_repo, pr_obj.number) + except Exception as e: + print(f"Unable to close pull request {pr_obj.id}: {e}") print() - print(target.pr_obj.to_human_readable_string()) + print(target_pr_obj.to_human_readable_string()) print() print("Staging project pull requests have been successfully merged") - - if args.keep_temp_dir: - print() - print(f"Temporary files are available here: {temp_dir}") From 6beefac68ff46130ec7c5bb83e53690d4085ed51 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 4 Mar 2026 15:15:17 +0100 Subject: [PATCH 2/2] Fix working with labels in 'git-obs staging group' --- behave/features/git-obs-staging-group.feature | 19 +++++++++++++++ osc/commands_git/staging_group.py | 24 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/behave/features/git-obs-staging-group.feature b/behave/features/git-obs-staging-group.feature index d6c3f2bc..c1649642 100644 --- a/behave/features/git-obs-staging-group.feature +++ b/behave/features/git-obs-staging-group.feature @@ -32,12 +32,16 @@ Background: Scenario: Scenario 1: staging group --no-ssh-strict-host-key-checking with --target and existing PR When I execute git-obs with args "-G alice staging group --no-ssh-strict-host-key-checking --target=pool/test-GitPkgA#1 pool/test-GitPkgA#2" Then the exit code is 0 + And stdout contains "Unable to add the 'staging/In Progress' label to pull request pool/test-GitPkgA#2" + And stdout contains "Unable to remove the 'staging/Backlog' label from pull request pool/test-GitPkgA#2" And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#1" Then the exit code is 0 And stdout contains "State : open" And stdout contains "Description : PR: foo/bar!1" # It should also contain the reference from #2 And stdout contains "PR: foo/bar!2" + And stdout doesn't contain "staging/In Progress" + And stdout contains "staging/Backlog" And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#2" Then the exit code is 0 And stdout contains "State : closed" @@ -47,6 +51,13 @@ Scenario: Scenario 1: staging group --no-ssh-strict-host-key-checking with --tar Scenario: Scenario 2: staging group --no-ssh-strict-host-key-checking with --target and --remove-pr-references When I execute git-obs with args "-G alice staging group --no-ssh-strict-host-key-checking --target=pool/test-GitPkgA#1 pool/test-GitPkgA#2 --remove-pr-references" Then the exit code is 0 + And stdout contains "Unable to add the 'staging/In Progress' label to pull request pool/test-GitPkgA#2" + And stdout contains "Unable to remove the 'staging/Backlog' label from pull request pool/test-GitPkgA#2" + And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#1" + Then the exit code is 0 + And stdout contains "State : open" + And stdout doesn't contain "staging/In Progress" + And stdout contains "staging/Backlog" And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#2" Then the exit code is 0 And stdout contains "State : closed" @@ -61,6 +72,8 @@ Scenario: Scenario 3: staging group --no-ssh-strict-host-key-checking without -- And stdout contains "State : open" And stdout contains "PR: foo/bar!1" And stdout contains "PR: foo/bar!2" + And stdout contains "Labels : staging/In Progress" + And stdout doesn't contain "staging/Backlog" And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#1" Then the exit code is 0 # PR #1 should be closed because it was merged into #3 @@ -78,6 +91,8 @@ Scenario: Scenario 4: staging group --no-ssh-strict-host-key-checking without -- Then the exit code is 0 And stdout contains "PR: foo/bar!1" And stdout contains "PR: foo/bar!2" + And stdout contains "Labels : staging/In Progress" + And stdout doesn't contain "staging/Backlog" And I execute git-obs with args "-G alice pr get pool/test-GitPkgA#1" Then the exit code is 0 And stdout contains "State : closed" @@ -104,6 +119,8 @@ Scenario: Scenario 5: staging group --no-ssh-strict-host-key-checking by another And stdout contains "Author : Bob \(bob@example.com\)" And stdout contains "PR: foo/bar!1" And stdout contains "PR: foo/bar!2" + And stdout contains "Labels : staging/In Progress" + And stdout doesn't contain "staging/Backlog" @destructive Scenario: Scenario 6: staging group --no-ssh-strict-host-key-checking by another user with push permissions to the target repository @@ -125,3 +142,5 @@ Scenario: Scenario 6: staging group --no-ssh-strict-host-key-checking by another And stdout contains "Source : pool/test-GitPkgA" And stdout contains "PR: foo/bar!1" And stdout contains "PR: foo/bar!2" + And stdout contains "Labels : staging/In Progress" + And stdout doesn't contain "staging/Backlog" diff --git a/osc/commands_git/staging_group.py b/osc/commands_git/staging_group.py index 8acf1c90..ca66b5b5 100644 --- a/osc/commands_git/staging_group.py +++ b/osc/commands_git/staging_group.py @@ -151,6 +151,30 @@ class StagingGroupCommand(osc.commandline_git.GitObsCommand): allow_maintainer_edit=True, ) + # update labels + try: + gitea_api.PullRequest.add_labels( + self.gitea_conn, + target_pr_obj.base_owner, + target_pr_obj.base_repo, + int(target_pr_obj.number), + labels=[gitea_api.StagingPullRequestWrapper.INPROGRESS_LABEL], + ) + except Exception as e: + print(f"Unable to add the '{gitea_api.StagingPullRequestWrapper.INPROGRESS_LABEL}' label to pull request {pr_obj.id}: {e}") + + try: + if gitea_api.StagingPullRequestWrapper.BACKLOG_LABEL in target_pr_obj.labels: + gitea_api.PullRequest.remove_labels( + self.gitea_conn, + target_pr_obj.base_owner, + target_pr_obj.base_repo, + int(target_pr_obj.number), + labels=[gitea_api.StagingPullRequestWrapper.BACKLOG_LABEL], + ) + except Exception as e: + print(f"Unable to remove the '{gitea_api.StagingPullRequestWrapper.BACKLOG_LABEL}' label from pull request {pr_obj.id}: {e}") + else: has_push_access = False if not args.fork_owner: