From 198f1b64a97e81c9a4d53af663e3ab84a9adea4a Mon Sep 17 00:00:00 2001 From: Uwe Schwaeke Date: Tue, 27 Jan 2026 12:03:15 +0100 Subject: [PATCH 1/4] crt/cmds/release: add --local-run option avoid accessing remotes if the flag is set and the subcommand doesn't require remote access. affected subcommands: * start: * skip adding remote URLs. * skip checking if the release already exists on the remote. * create a local release branch from base_ref (which must exist locally). * do not push the created branch or tag to the remote. * list: * skip adding remote urls. * skip fetching from remotes. other subcommands will ignore the flag. Signed-off-by: Uwe Schwaeke --- crt/src/crt/cmds/__init__.py | 18 +++++ crt/src/crt/cmds/crt.py | 11 ++++ crt/src/crt/cmds/release.py | 113 ++++++++++++++++++++++---------- crt/src/crt/crtlib/git_utils.py | 12 ++++ 4 files changed, 121 insertions(+), 33 deletions(-) diff --git a/crt/src/crt/cmds/__init__.py b/crt/src/crt/cmds/__init__.py index fee0dff..83d59a7 100644 --- a/crt/src/crt/cmds/__init__.py +++ b/crt/src/crt/cmds/__init__.py @@ -33,10 +33,12 @@ class Ctx: github_token: str | None patches_repo_path: Path | None + run_locally: bool def __init__(self) -> None: self.github_token = None self.patches_repo_path = None + self.run_locally = False pass_ctx = click.make_pass_decorator(Ctx, ensure=True) @@ -47,6 +49,22 @@ def __init__(self) -> None: _P = ParamSpec("_P") +def with_run_locally( + f: Callable[Concatenate[bool, _P], _R], +) -> Callable[_P, _R]: + """Pass the flag to don't access remotes from the context to the function.""" + + def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R: + curr_ctx = click.get_current_context() + ctx = curr_ctx.find_object(Ctx) + if not ctx: + perror(f"missing context for '{f.__name__}'") + sys.exit(errno.ENOTRECOVERABLE) + return f(ctx.run_locally, *args, **kwargs) + + return update_wrapper(inner, f) + + def with_patches_repo_path(f: Callable[Concatenate[Path, _P], _R]) -> Callable[_P, _R]: """Pass the CES patches repo path from the context to the function.""" diff --git a/crt/src/crt/cmds/crt.py b/crt/src/crt/cmds/crt.py index ae943a2..5742408 100644 --- a/crt/src/crt/cmds/crt.py +++ b/crt/src/crt/cmds/crt.py @@ -78,6 +78,15 @@ required=True, help="Path to CES patches git repository.", ) +@click.option( + "-l", + "--local-run", + "run_locally", + is_flag=True, + default=False, + required=False, + help="run without accessing remotes", +) @pass_ctx def cmd_crt( ctx: Ctx, @@ -85,6 +94,7 @@ def cmd_crt( verbose: bool, github_token: str | None, patches_repo_path: Path, + run_locally: bool, ) -> None: if verbose: set_verbose_logging() @@ -97,6 +107,7 @@ def cmd_crt( ctx.github_token = github_token ctx.patches_repo_path = patches_repo_path + ctx.run_locally = run_locally if debug or verbose: table = Table(show_header=False, show_lines=False, box=None) diff --git a/crt/src/crt/cmds/release.py b/crt/src/crt/cmds/release.py index df67262..1d56601 100644 --- a/crt/src/crt/cmds/release.py +++ b/crt/src/crt/cmds/release.py @@ -33,9 +33,11 @@ git_branch_from, git_cleanup_repo, git_fetch_ref, + git_get_local_head, git_get_remote_ref, git_prepare_remote, git_push, + git_remote, git_reset_head, git_tag, ) @@ -46,10 +48,12 @@ from . import ( console, perror, + pinfo, psuccess, pwarn, with_gh_token, with_patches_repo_path, + with_run_locally, ) from . import ( logger as parent_logger, @@ -65,6 +69,8 @@ def _prepare_release_repo( src_repo: str, dst_repo: str, token: str, + *, + run_locally: bool = False, ) -> None: try: git_cleanup_repo(ceph_repo_path) @@ -73,6 +79,10 @@ def _prepare_release_repo( perror(f"failed to cleanup ceph repo at '{ceph_repo_path}': {e}") raise _ExitError(errno.ENOTRECOVERABLE) from e + if run_locally: + # return because only thing we do next is to set the remotes and fetch from it. + return + try: _ = git_prepare_remote( ceph_repo_path, f"github.com/{src_repo}", src_repo, token @@ -92,18 +102,30 @@ def _prepare_release_branches( src_ref: str, dst_repo: str, dst_branch: str, + *, + run_locally: bool = False, ) -> None: try: - if git_get_remote_ref(ceph_repo_path, dst_branch, dst_repo): - perror(f"destination branch '{dst_branch}' already exists in '{dst_repo}'") - sys.exit(errno.EEXIST) + if run_locally: + if git_get_local_head(ceph_repo_path, dst_branch): + perror(f"destination branch '{dst_branch}' already exists local'") + sys.exit(errno.EEXIST) + else: + if git_get_remote_ref(ceph_repo_path, dst_branch, dst_repo): + perror( + f"destination branch '{dst_branch}' already exists in '{dst_repo}'" + ) + sys.exit(errno.EEXIST) except GitError as e: perror(f"failed to check for existing branch '{dst_branch}': {e}") raise _ExitError(errno.ENOTRECOVERABLE) from e is_tag = False try: - _ = git_fetch_ref(ceph_repo_path, src_ref, dst_branch, src_repo) + if run_locally: + git_branch_from(ceph_repo_path, src_ref, dst_branch) + else: + _ = git_fetch_ref(ceph_repo_path, src_ref, dst_branch, src_repo) except GitIsTagError: logger.debug(f"source ref '{src_ref}' is a tag, fetching as branch") is_tag = True @@ -188,11 +210,13 @@ def cmd_release(): help="Allow a development release, with suffixes.", ) @click.argument("release_name", type=str, required=True, metavar="NAME") +@with_run_locally @with_patches_repo_path @with_gh_token def cmd_release_start( gh_token: str, patches_repo_path: Path, + run_locally: bool, ceph_repo_path: Path, from_manifest: str | None, from_ref: str | None, @@ -276,6 +300,7 @@ def cmd_release_start( base_ref_repo, dst_repo, gh_token, + run_locally=run_locally, ) except _ExitError as e: progress.stop_error() @@ -288,7 +313,9 @@ def cmd_release_start( progress.done_task() progress.new_task("prepare release branches") - if git_get_remote_ref(ceph_repo_path, f"release/{release_name}", dst_repo): + if not run_locally and git_get_remote_ref( + ceph_repo_path, f"release/{release_name}", dst_repo + ): progress.stop_error() perror(f"release '{release_name}' already marked released in '{dst_repo}'") sys.exit(errno.EEXIST) @@ -300,6 +327,7 @@ def cmd_release_start( base_ref, dst_repo, release_base_branch, + run_locally=run_locally, ) except _ExitError as e: progress.stop_error() @@ -309,16 +337,19 @@ def cmd_release_start( perror(f"failed to prepare release branches: {e}") sys.exit(errno.ENOTRECOVERABLE) - try: - _ = git_push(ceph_repo_path, release_base_branch, dst_repo) - except GitError as e: - progress.stop_error() - perror(f"failed to push release branch '{release_base_branch}': {e}") - sys.exit(errno.ENOTRECOVERABLE) - except Exception as e: - progress.stop_error() - perror(f"unexpected error pushing release branch '{release_base_branch}': {e}") - sys.exit(errno.ENOTRECOVERABLE) + if not run_locally: + try: + _ = git_push(ceph_repo_path, release_base_branch, dst_repo) + except GitError as e: + progress.stop_error() + perror(f"failed to push release branch '{release_base_branch}': {e}") + sys.exit(errno.ENOTRECOVERABLE) + except Exception as e: + progress.stop_error() + perror( + f"unexpected error pushing release branch '{release_base_branch}': {e}" + ) + sys.exit(errno.ENOTRECOVERABLE) try: git_tag( @@ -326,7 +357,7 @@ def cmd_release_start( release_base_tag, release_base_branch, msg=f"Base release for {release_name}", - push_to=dst_repo, + push_to=dst_repo if not run_locally else None, ) except GitError as e: progress.stop_error() @@ -364,6 +395,7 @@ def cmd_release_start( summary_table.add_row("From Base Reference", f"{base_ref} from {base_ref_repo}") summary_table.add_row("Release base branch", release_base_branch) summary_table.add_row("Release base tag", release_base_tag) + summary_table.add_row("Is local", str(run_locally)) console.print(Padding(summary_table, (1, 0, 1, 0))) @@ -397,27 +429,47 @@ def cmd_release_start( help="Destination repository.", show_default=True, ) +@with_run_locally @with_patches_repo_path @with_gh_token def cmd_release_list( - gh_token: str, patches_repo_path: Path, ceph_repo_path: Path, dst_repo: str + gh_token: str, + patches_repo_path: Path, + run_locally: bool, + ceph_repo_path: Path, + dst_repo: str, ) -> None: progress = CRTProgress(console) progress.start() - progress.new_task("prepare remote") + table = Table(show_header=True, show_lines=True, box=rich.box.HORIZONTALS) + table.add_column("Name", justify="left", style="bold cyan", no_wrap=True) + table.add_column("Base", justify="left", style="magenta", no_wrap=True) + table.add_column("Status", justify="left", no_wrap=True) - try: - remote = git_prepare_remote( - ceph_repo_path, f"github.com/{dst_repo}", dst_repo, gh_token - ) - except GitError as e: - perror(f"unable to prepare remote repository '{dst_repo}': {e}") - progress.stop_error() - sys.exit(errno.ENOTRECOVERABLE) + if run_locally: + progress.new_task("get remote") + if not (remote := git_remote(ceph_repo_path, dst_repo)): + pinfo(f"remote {dst_repo} doesn't exists local") + console.print(Padding(table, (1, 0, 1, 0))) + progress.done_task() + progress.stop() + return + progress.done_task() + progress.stop() + else: + progress.new_task("prepare remote") + try: + remote = git_prepare_remote( + ceph_repo_path, f"github.com/{dst_repo}", dst_repo, gh_token + ) + except GitError as e: + perror(f"unable to prepare remote repository '{dst_repo}': {e}") + progress.stop_error() + sys.exit(errno.ENOTRECOVERABLE) - progress.done_task() - progress.stop() + progress.done_task() + progress.stop() remote_releases: list[str] = [] remote_base_releases: list[str] = [] @@ -461,11 +513,6 @@ def cmd_release_list( if r not in remote_releases: not_released.append(r) - table = Table(show_header=True, show_lines=True, box=rich.box.HORIZONTALS) - table.add_column("Name", justify="left", style="bold cyan", no_wrap=True) - table.add_column("Base", justify="left", style="magenta", no_wrap=True) - table.add_column("Status", justify="left", no_wrap=True) - for r in remote_releases: rel = releases_meta.get(r) table.add_row( diff --git a/crt/src/crt/crtlib/git_utils.py b/crt/src/crt/crtlib/git_utils.py index e96ac3b..a3d339f 100644 --- a/crt/src/crt/crtlib/git_utils.py +++ b/crt/src/crt/crtlib/git_utils.py @@ -382,6 +382,18 @@ def git_prepare_remote( return remote +def git_remote(repo_path: Path, remote_name: str) -> git.Remote | None: + logger.info(f"get remote '{remote_name}'") + + repo = git.Repo(repo_path) + try: + return repo.remote(remote_name) + except ValueError: + logger.debug(f"remote '{remote_name}' doesn't exists") + + return None + + def _get_remote_ref_name( remote_name: str, remote_ref: str, *, ref_name: str | None = None ) -> tuple[str, str] | None: From 866096dd6a6212134f6d4587f907b5d2fd91b27b Mon Sep 17 00:00:00 2001 From: Uwe Schwaeke Date: Tue, 27 Jan 2026 14:23:26 +0100 Subject: [PATCH 2/4] crt/cmds/manifest: add --local-run option don't access remotes if the flag is set and the subcommand doesn't require remote access. affected subcommands: * patchset add: * don't add remote urls and don't fetch patch branch (assume branch exists locally) * validate: * don't add remote urls and don't fetch from remote other subcommands will ignore the flag Signed-off-by: Uwe Schwaeke --- crt/src/crt/cmds/manifest.py | 90 +++++++++++++++++++++++++++++++-- crt/src/crt/crtlib/apply.py | 11 ++-- crt/src/crt/crtlib/git_utils.py | 36 +++++++++---- crt/src/crt/crtlib/manifest.py | 85 +++++++++++++++++++------------ 4 files changed, 173 insertions(+), 49 deletions(-) diff --git a/crt/src/crt/cmds/manifest.py b/crt/src/crt/cmds/manifest.py index ef25021..b7dfec2 100644 --- a/crt/src/crt/cmds/manifest.py +++ b/crt/src/crt/cmds/manifest.py @@ -38,7 +38,15 @@ NoSuchManifestError, ) from crt.crtlib.errors.patchset import NoSuchPatchSetError, PatchSetError -from crt.crtlib.errors.release import NoSuchReleaseError +from crt.crtlib.errors.release import NoSuchReleaseError, ReleaseError +from crt.crtlib.git_utils import ( + GitError, + git_get_remote_ref, + git_prepare_remote, + git_push, + git_remote, + git_tag_exists_in_remote, +) from crt.crtlib.github import gh_get_pr from crt.crtlib.manifest import ( ManifestExecuteResult, @@ -646,7 +654,12 @@ def _check_repo(repo_path: Path, what: str) -> None: progress.new_task("applying patch set to manifest") try: _, added, skipped = patches_apply_to_manifest( - manifest, patchset, ceph_repo_path, patches_repo_path, ctx.github_token + manifest, + patchset, + ceph_repo_path, + patches_repo_path, + ctx.github_token, + run_locally=ctx.run_locally, ) except (ApplyError, Exception) as e: perror(f"unable to apply to manifest: {e}") @@ -683,6 +696,7 @@ def _manifest_execute( ceph_repo_path: Path, patches_repo_path: Path, no_cleanup: bool = True, + run_locally: bool = False, progress: CRTProgress, ) -> tuple[ManifestExecuteResult, RenderableType]: """ @@ -694,7 +708,12 @@ def _manifest_execute( try: res = manifest_execute( - manifest, ceph_repo_path, patches_repo_path, token, no_cleanup=no_cleanup + manifest, + ceph_repo_path, + patches_repo_path, + token, + no_cleanup=no_cleanup, + run_locally=run_locally, ) except ApplyConflictError as e: progress.stop_error() @@ -870,6 +889,7 @@ def cmd_manifest_validate( ceph_repo_path=ceph_repo_path, patches_repo_path=patches_repo_path, no_cleanup=no_cleanup, + run_locally=ctx.run_locally, progress=progress, ) progress.stop() @@ -904,6 +924,15 @@ def cmd_manifest_validate( default="release-dev", help="Prefix to use for published branch.", ) +@click.option( + "-r", + "--release", + "release_name", + type=str, + required=True, + metavar="RELEASE", + help="Release associated with the manifest.", +) @with_patches_repo_path @pass_ctx def cmd_manifest_publish( @@ -912,6 +941,7 @@ def cmd_manifest_publish( ceph_repo_path: Path, release_branch_prefix: str, manifest_name_or_uuid: str, + release_name: str, ) -> None: """ Publish a manifest. @@ -945,6 +975,27 @@ def cmd_manifest_publish( progress = CRTProgress(console) progress.start() + try: + _prepare_release_repo( + ceph_repo_path, + patches_repo_path, + manifest, + release_name, + ctx.github_token, + ) + except NoSuchReleaseError: + msg = f"release {release_name} does not exist" + logger.error(msg) + sys.exit(errno.ENOENT) + except ReleaseError as e: + msg = f"can't load release {release_name}: '{e}'" + logger.error(msg) + sys.exit(errno.EBADMSG) + except GitError as e: + msg = f"can't publish manifest {manifest.name}: '{e}'" + logger.error(msg) + sys.exit(e.ec) + execute_res, execute_summary = _manifest_execute( manifest, token=ctx.github_token, @@ -1084,3 +1135,36 @@ def cmd_manifest_update(patches_repo_path: Path, manifest_name_or_uuid: str) -> sys.exit(errno.ENOTRECOVERABLE) psuccess(f"updated manifest '{manifest_name_or_uuid}' on-disk representation") + + +def _prepare_release_repo( + ceph_repo_path: Path, + ces_patch_path: Path, + manifest: ReleaseManifest, + release_name: str, + token: str, +) -> None: + release_branch_name = manifest.base_ref + release_tag_name = release_branch_name.replace("/", "-") + remote_name = manifest.dst_repo + + if ( + git_remote(ceph_repo_path, remote_name) + and git_get_remote_ref(ceph_repo_path, release_branch_name, remote_name) + and git_tag_exists_in_remote(ceph_repo_path, remote_name, release_tag_name) + ): + pinfo("release repo allready prepared") + return + + release = load_release(ces_patch_path, release_name) + release_repo_name = release.base_repo + + git_prepare_remote( + ceph_repo_path, f"github.com/{release_repo_name}", release_repo_name, token + ) + if remote_name != release_repo_name: + git_prepare_remote( + ceph_repo_path, f"github.com/{remote_name}", remote_name, token + ) + git_push(ceph_repo_path, release_branch_name, remote_name) + git_push(ceph_repo_path, release_tag_name, remote_name) diff --git a/crt/src/crt/crtlib/apply.py b/crt/src/crt/crtlib/apply.py index 26f2253..65a21d3 100644 --- a/crt/src/crt/crtlib/apply.py +++ b/crt/src/crt/crtlib/apply.py @@ -146,6 +146,7 @@ def apply_manifest( token: str, *, no_cleanup: bool = False, + run_locally: bool = False, ) -> tuple[bool, list[ManifestPatchEntry], list[ManifestPatchEntry]]: ceph_repo = git.Repo(ceph_repo_path) @@ -191,9 +192,10 @@ def _apply_patches( try: _prepare_repo(ceph_repo_path) repo_name = f"{manifest.base_ref_org}/{manifest.base_ref_repo}" - _ = git_prepare_remote( - ceph_repo_path, f"github.com/{repo_name}", repo_name, token - ) + if not run_locally: + _ = git_prepare_remote( + ceph_repo_path, f"github.com/{repo_name}", repo_name, token + ) except ApplyError as e: logger.error(e) raise e from None @@ -232,6 +234,8 @@ def patches_apply_to_manifest( ceph_repo_path: Path, patches_repo_path: Path, token: str, + *, + run_locally: bool = False, ) -> tuple[bool, list[ManifestPatchEntry], list[ManifestPatchEntry]]: manifest = orig_manifest.model_copy(deep=True) if not manifest.add_patches(patch): @@ -247,4 +251,5 @@ def patches_apply_to_manifest( target_branch, token, no_cleanup=False, + run_locally=run_locally, ) diff --git a/crt/src/crt/crtlib/git_utils.py b/crt/src/crt/crtlib/git_utils.py index a3d339f..295e694 100644 --- a/crt/src/crt/crtlib/git_utils.py +++ b/crt/src/crt/crtlib/git_utils.py @@ -670,17 +670,21 @@ def git_branch_delete(repo_path: Path, branch: str) -> None: def git_push( repo_path: Path, - branch: str, + ref: str, remote_name: str, *, - branch_to: str | None = None, + ref_to: str | None = None, ) -> tuple[bool, list[str], list[str]]: - dst_branch = branch_to if branch_to else branch - - head = git_get_local_head(repo_path, branch) - if not head: - logger.error(f"unable to find branch '{branch}' to push") - raise GitHeadNotFoundError(branch) + dst_ref = ref_to if ref_to else ref + """ + pushes either a local head of branch or a local tag to the remote + """ + if _get_tag(repo_path, ref): + dst_ref = f"refs/tags/{dst_ref}" + elif not git_get_local_head(repo_path, ref): + # ref is neither a local branch nor tag + logger.error(f"unable to find ref '{ref}' to push") + raise GitHeadNotFoundError(ref) repo = git.Repo(repo_path) try: @@ -690,12 +694,12 @@ def git_push( raise GitMissingRemoteError(remote_name) from None try: - info = remote.push(f"{branch}:{dst_branch}") + info = remote.push(f"{ref}:{dst_ref}") except git.CommandError as e: - msg = f"unable to push '{branch}' to '{dst_branch}': {e}" + msg = f"unable to push '{ref}' to '{dst_ref}': {e}" logger.error(msg) logger.error(e.stderr) - raise GitPushError(branch, dst_branch, remote_name) from None + raise GitPushError(ref, dst_ref, remote_name) from None updated: list[str] = [] rejected: list[str] = [] @@ -807,6 +811,16 @@ def git_format_patch(repo_path: Path, rev: SHA, *, base_rev: SHA | None = None) return res +def git_tag_exists_in_remote(repo_path: Path, remote_name: str, tag_name: str) -> bool: + repo = git.Repo(repo_path) + raw_tags: str = repo.git.ls_remote("--tags", remote_name) + for tag_line in raw_tags.splitlines(): + _, ref = tag_line.split() + if f"refs/tags/{tag_name}^{{}}" == ref: + return True + return False + + if __name__ == "__main__": if len(sys.argv) < 2: print("error: missing repo path argument") diff --git a/crt/src/crt/crtlib/manifest.py b/crt/src/crt/crtlib/manifest.py index fbc6607..9cbc0ba 100644 --- a/crt/src/crt/crtlib/manifest.py +++ b/crt/src/crt/crtlib/manifest.py @@ -32,11 +32,13 @@ ) from crt.crtlib.errors.stages import MissingStagePatchError from crt.crtlib.git_utils import ( + GitCreateHeadExistsError, GitError, GitFetchError, GitFetchHeadNotFoundError, GitIsTagError, GitPushError, + git_branch_from, git_checkout_ref, git_cleanup_repo, git_fetch_ref, @@ -75,6 +77,8 @@ def _prepare_repo( base_remote_name: str, push_remote_name: str, token: str, + *, + run_locally: bool = False, ) -> None: try: git_cleanup_repo(repo_path) @@ -83,43 +87,50 @@ def _prepare_repo( logger.error(msg) raise ManifestError(uuid=manifest_uuid, msg=msg) from None - try: - base_remote_uri = f"github.com/{base_remote_name}" - _ = git_prepare_remote(repo_path, base_remote_uri, base_remote_name, token) - push_remote_uri = f"github.com/{push_remote_name}" - _ = git_prepare_remote(repo_path, push_remote_uri, push_remote_name, token) - except GitError as e: - raise ManifestError(uuid=manifest_uuid, msg=str(e)) from None - - # fetch from base repository, if it exists. - try: - _ = git_fetch_ref(repo_path, target_branch, target_branch, push_remote_name) - except GitIsTagError as e: - msg = f"unexpected tag for branch '{target_branch}': {e}" - logger.error(msg) - raise ManifestError(uuid=manifest_uuid, msg=msg) from None - except GitFetchHeadNotFoundError: - # does not exist in the provided remote. - logger.debug( - f"branch '{target_branch}' does not exist in remote '{push_remote_name}'" - ) - except GitFetchError as e: - msg = f"unable to fetch '{target_branch}' from '{push_remote_name}': {e}" - logger.error(msg) - raise ManifestError(uuid=manifest_uuid, msg=msg) from None - except GitError as e: - msg = ( - f"unexpected error fetching branch '{target_branch}' " - + f"from '{push_remote_name}': {e}" - ) - logger.error(msg) - raise ManifestError(uuid=manifest_uuid, msg=msg) from None + if not run_locally: + try: + base_remote_uri = f"github.com/{base_remote_name}" + _ = git_prepare_remote(repo_path, base_remote_uri, base_remote_name, token) + push_remote_uri = f"github.com/{push_remote_name}" + _ = git_prepare_remote(repo_path, push_remote_uri, push_remote_name, token) + except GitError as e: + raise ManifestError(uuid=manifest_uuid, msg=str(e)) from None + + # fetch from base repository, if it exists. + try: + _ = git_fetch_ref(repo_path, target_branch, target_branch, push_remote_name) + except GitIsTagError as e: + msg = f"unexpected tag for branch '{target_branch}': {e}" + logger.error(msg) + raise ManifestError(uuid=manifest_uuid, msg=msg) from None + except GitFetchHeadNotFoundError: + # does not exist in the provided remote. + logger.debug( + f"branch '{target_branch}' does not exist in remote '{push_remote_name}'" + ) + except GitFetchError as e: + msg = f"unable to fetch '{target_branch}' from '{push_remote_name}': {e}" + logger.error(msg) + raise ManifestError(uuid=manifest_uuid, msg=msg) from None + except GitError as e: + msg = ( + f"unexpected error fetching branch '{target_branch}' " + + f"from '{push_remote_name}': {e}" + ) + logger.error(msg) + raise ManifestError(uuid=manifest_uuid, msg=msg) from None # we either fetched and thus we have an up-to-date local branch, or we didn't find # a corresponding reference in the remote and we need to either: # 1. checkout a new copy of the base ref to the target branch # 2. use an existing local target branch try: + if run_locally: + try: + git_branch_from(repo_path, base_ref, target_branch) + except GitCreateHeadExistsError: + msg = f"branch {target_branch} exists" + logger.info(msg) _ = git_checkout_ref( repo_path, base_ref, @@ -148,6 +159,7 @@ def manifest_execute( token: str, *, no_cleanup: bool = True, + run_locally: bool = False, ) -> ManifestExecuteResult: """ Execute a manifest against its base ref. @@ -183,6 +195,7 @@ def manifest_execute( base_remote_name, manifest.dst_repo, token, + run_locally=run_locally, ) except ManifestError as e: logger.error(f"unable to prepare repository to execute manifest: {e}") @@ -197,6 +210,7 @@ def manifest_execute( target_branch, token, no_cleanup=no_cleanup, + run_locally=run_locally, ) pass except ApplyError as e: @@ -329,7 +343,7 @@ def manifest_publish_branch( repo_path, our_branch, dst_repo, - branch_to=dst_branch, + ref_to=dst_branch, ) except GitPushError as e: msg = f"unable to push '{our_branch}': {e}" @@ -490,6 +504,13 @@ def store_manifest(patches_repo_path: Path, manifest: ReleaseManifest) -> None: logger.error(msg) raise ManifestError(uuid=manifest.release_uuid, msg=msg) from None + try: + manifest_name_path.parent.mkdir(parents=True, exist_ok=True) + except Exception as e: + msg = f"error creating folder '{manifest_name_path.parent}': {e}" + logger.error(msg) + raise ManifestError(uuid=manifest.release_uuid, msg=msg) from None + if not manifest_name_path.exists(): try: manifest_name_path.symlink_to(Path("..").joinpath(manifest_uuid_path.name)) From ef439d9835ff3d2771ecf45d0012e381752ebdc2 Mon Sep 17 00:00:00 2001 From: Uwe Schwaeke Date: Tue, 27 Jan 2026 14:56:42 +0100 Subject: [PATCH 3/4] crt/cmds/patch: add --local-run option don't access remotes if the flag is set and the subcommand doesn't require remote access. affected subcommands: * add: * don't add remote url and don't fetch Signed-off-by: Uwe Schwaeke --- crt/src/crt/cmds/patch.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crt/src/crt/cmds/patch.py b/crt/src/crt/cmds/patch.py index 0930e3f..4a45818 100644 --- a/crt/src/crt/cmds/patch.py +++ b/crt/src/crt/cmds/patch.py @@ -161,17 +161,18 @@ def _check_repo(repo_path: Path, what: str) -> None: else: src_ceph_repo_path = ceph_repo_path - # update remote repo, maybe patches are not yet in the current repo state - try: - _ = git_prepare_remote( - src_ceph_repo_path, - f"github.com/{src_gh_repo}", - src_gh_repo, - ctx.github_token, - ) - except Exception as e: - perror(f"unable to update remote '{src_gh_repo}': {e}") - sys.exit(errno.ENOTRECOVERABLE) + if not ctx.run_locally: + # update remote repo, maybe patches are not yet in the current repo state + try: + _ = git_prepare_remote( + src_ceph_repo_path, + f"github.com/{src_gh_repo}", + src_gh_repo, + ctx.github_token, + ) + except Exception as e: + perror(f"unable to update remote '{src_gh_repo}': {e}") + sys.exit(errno.ENOTRECOVERABLE) try: shas = [git_revparse(src_ceph_repo_path, sha) for sha in patch_sha] @@ -230,6 +231,7 @@ def _check_repo(repo_path: Path, what: str) -> None: ceph_repo_path, patches_repo_path, ctx.github_token, + run_locally=ctx.run_locally, ) except (ApplyError, Exception) as e: perror(f"unable to apply patch sha '{sha}' to manifest: {e}") From 6ec1cfeaf75356d6ed996f8e1ff12c44c3c619d1 Mon Sep 17 00:00:00 2001 From: Uwe Schwaeke Date: Tue, 27 Jan 2026 15:01:45 +0100 Subject: [PATCH 4/4] crt/cmds/patchset: add --local-run option don't access remotes if the flag is set and the subcommand doesn't require remote access. affected subcommands: * add: * don't add remote url and don't fetch * create branch from an existing local branch * publish: * don't add remote url and don't fetch * create branch from an existing local branch other subcommands will ignore the flag Signed-off-by: Uwe Schwaeke --- crt/src/crt/cmds/patchset.py | 61 ++++++++++++++++++++++------------ crt/src/crt/crtlib/patchset.py | 38 ++++++++++++++------- 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/crt/src/crt/cmds/patchset.py b/crt/src/crt/cmds/patchset.py index 1a065c5..90e2b01 100644 --- a/crt/src/crt/cmds/patchset.py +++ b/crt/src/crt/cmds/patchset.py @@ -44,7 +44,9 @@ ) from crt.crtlib.git_utils import ( SHA, + GitError, git_branch_delete, + git_branch_from, git_get_patch_sha_title, git_patches_in_interval, git_prepare_remote, @@ -547,29 +549,40 @@ def cmd_patchset_add( progress.start() progress.new_task(f"add patches from '{gh_repo}' branch '{patches_branch}'") - # ensure we have the specified branch in the ceph repo, so we can actually obtain - # the right shas - progress.new_task("prepare remote") - try: - remote = git_prepare_remote( - ceph_repo_path, f"github.com/{gh_repo}", gh_repo, ctx.github_token - ) - except Exception as e: - progress.stop_error() - perror(f"unable to update remote '{gh_repo}': {e}") - sys.exit(errno.ENOTRECOVERABLE) - - progress.done_task() - progress.new_task("fetch patches") - seq = dt.now(datetime.UTC).strftime("%Y%m%d%H%M%S") dst_branch = f"patchset/branch/{gh_repo.replace('/', '--')}--{patches_branch}-{seq}" - try: - _ = remote.fetch(refspec=f"{patches_branch}:{dst_branch}") - except Exception as e: - progress.stop_error() - perror(f"unable to fetch branch '{patches_branch}': {e}") - sys.exit(errno.ENOTRECOVERABLE) + + if not ctx.run_locally: + # ensure we have the specified branch in the ceph repo, so we can actually + # obtain the right shas + progress.new_task("prepare remote") + try: + remote = git_prepare_remote( + ceph_repo_path, f"github.com/{gh_repo}", gh_repo, ctx.github_token + ) + except Exception as e: + progress.stop_error() + perror(f"unable to update remote '{gh_repo}': {e}") + sys.exit(errno.ENOTRECOVERABLE) + + progress.done_task() + + progress.new_task("fetch patches") + + try: + _ = remote.fetch(refspec=f"{patches_branch}:{dst_branch}") + except Exception as e: + progress.stop_error() + perror(f"unable to fetch branch '{patches_branch}': {e}") + sys.exit(errno.ENOTRECOVERABLE) + else: + progress.new_task("create branch patches") + try: + git_branch_from(ceph_repo_path, patches_branch, dst_branch) + except GitError as e: + progress.stop_error() + perror(f"unable to create branch '{dst_branch} from {patches_branch}': {e}") + sys.exit(errno.ENOTRECOVERABLE) def _cleanup() -> None: try: @@ -781,7 +794,11 @@ def cmd_patchset_publish( try: patches = fetch_custom_patchset_patches( - ceph_repo_path, patches_repo_path, patchset, ctx.github_token + ceph_repo_path, + patches_repo_path, + patchset, + ctx.github_token, + run_locally=ctx.run_locally, ) except Exception as e: progress.stop_error() diff --git a/crt/src/crt/crtlib/patchset.py b/crt/src/crt/crtlib/patchset.py index 122bf7a..e2cc358 100644 --- a/crt/src/crt/crtlib/patchset.py +++ b/crt/src/crt/crtlib/patchset.py @@ -30,6 +30,7 @@ GitError, GitPatchDiffError, git_branch_delete, + git_branch_from, git_check_patches_diff, git_format_patch, git_prepare_remote, @@ -349,6 +350,8 @@ def fetch_custom_patchset_patches( patches_repo_path: Path, patchset: CustomPatchSet, token: str, + *, + run_locally: bool = False, ) -> list[Patch]: """Fetch and store a custom patch set's patches into the patches repository.""" if patchset.is_published: @@ -378,18 +381,29 @@ def fetch_custom_patchset_patches( if dst_branch in fetched_branches: continue - try: - remote = git_prepare_remote( - ceph_repo_path, f"github.com/{meta.repo}", meta.repo, token - ) - _ = remote.fetch(refspec=f"{meta.branch}:{dst_branch}") - except Exception as e: - msg = ( - f"error fetching patchset branch '{meta.branch}' " - + f"from '{meta.repo}': {e}" - ) - logger.error(msg) - raise PatchSetError(msg=msg) from None + if run_locally: + try: + git_branch_from(ceph_repo_path, meta.branch, dst_branch) + except GitError as e: + msg = ( + f"error creating patchset branch from local branch ' " + f"'{meta.branch}' from '{meta.repo}': {e}" + ) + logger.error(msg) + raise PatchSetError(msg=msg) from None + else: + try: + remote = git_prepare_remote( + ceph_repo_path, f"github.com/{meta.repo}", meta.repo, token + ) + _ = remote.fetch(refspec=f"{meta.branch}:{dst_branch}") + except Exception as e: + msg = ( + f"error fetching patchset branch '{meta.branch}' " + + f"from '{meta.repo}': {e}" + ) + logger.error(msg) + raise PatchSetError(msg=msg) from None fetched_branches.add(dst_branch)