diff --git a/src/stack_pr/cli.py b/src/stack_pr/cli.py index d068998..8a60089 100755 --- a/src/stack_pr/cli.py +++ b/src/stack_pr/cli.py @@ -51,6 +51,7 @@ import argparse import configparser +import contextlib import json import logging import os @@ -526,6 +527,24 @@ def draft_bitmask_type(value: str) -> list[bool]: return [bool(int(bit)) for bit in value] +@contextlib.contextmanager +def maybe_stash_interactive_rebase() -> Iterator[None]: + """ + If the user is in the middle of an interactive rebase, we stash the + rebase state so that we can restore it later. This is useful when + the user is trying to submit only part of their commit history. + """ + if os.path.exists(".git/rebase-merge"): + try: + assert not os.path.exists(".git/rebase-merge-stashed") + os.rename(".git/rebase-merge", ".git/rebase-merge-stashed") + yield + finally: + os.rename(".git/rebase-merge-stashed", ".git/rebase-merge") + else: + yield + + # ===----------------------------------------------------------------------=== # # SUBMIT # ===----------------------------------------------------------------------=== # @@ -1500,13 +1519,14 @@ def main() -> None: # noqa: PLR0912 common_args = deduce_base(common_args) if args.command in ["submit", "export"]: - command_submit( - common_args, - draft=args.draft, - reviewer=args.reviewer, - keep_body=args.keep_body, - draft_bitmask=args.draft_bitmask, - ) + with maybe_stash_interactive_rebase(): + command_submit( + common_args, + draft=args.draft, + reviewer=args.reviewer, + keep_body=args.keep_body, + draft_bitmask=args.draft_bitmask, + ) elif args.command == "land": command_land(common_args) elif args.command == "abandon": diff --git a/src/stack_pr/git.py b/src/stack_pr/git.py index 3cb6ce3..43eaf22 100644 --- a/src/stack_pr/git.py +++ b/src/stack_pr/git.py @@ -83,7 +83,7 @@ def get_current_branch_name(repo_dir: Path | None = None) -> str: repo_dir: path to the repo. Defaults to the current working directory. Returns: - The name of the branch currently checked out, or "HEAD" if the repo is + The name of the branch currently checked out, or a hash if the repo is in a 'detached HEAD' state Raises: @@ -92,9 +92,17 @@ def get_current_branch_name(repo_dir: Path | None = None) -> str: """ try: - return get_command_output( + result = get_command_output( ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_dir ).strip() + if result == "HEAD": + # Detached HEAD state, return a hash so we can cleanup after ourselves properly, since + # git checkout HEAD + # is a no-op. + result = get_command_output( + ["git", "rev-parse", "HEAD"], cwd=repo_dir + ).strip() + return result except subprocess.CalledProcessError as e: if e.returncode == GIT_NOT_A_REPO_ERROR: raise GitError("Not inside a valid git repository.") from e diff --git a/src/stack_pr/shell_commands.py b/src/stack_pr/shell_commands.py index ac12e30..6f7385e 100644 --- a/src/stack_pr/shell_commands.py +++ b/src/stack_pr/shell_commands.py @@ -45,7 +45,8 @@ def run_shell_command( raise ValueError("shell support has been removed") _ = subprocess.list2cmdline(cmd) if quiet: - kwargs.update({"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL}) + # Use pipes so errors result in usable error messages + kwargs.update({"stdout": subprocess.PIPE, "stderr": subprocess.PIPE}) logger.debug("Running: %s", cmd) return subprocess.run(list(map(str, cmd)), **kwargs, check=check)