diff --git a/README.md b/README.md index 6d7e3ff..dcb3fd4 100644 --- a/README.md +++ b/README.md @@ -239,9 +239,56 @@ stack-pr land -B HEAD~5 -H HEAD~2 ``` ## Command Line Options Reference -The section is not added yet, contributions are welcome! + +### Common Arguments + +These arguments can be used with any subcommand: + +- `-R, --remote`: Remote name (default: "origin") +- `-B, --base`: Local base branch +- `-H, --head`: Local head branch (default: "HEAD") +- `-T, --target`: Remote target branch (default: "main") +- `--hyperlinks/--no-hyperlinks`: Enable/disable hyperlink support (default: enabled) +- `-V, --verbose`: Enable verbose output from Git subcommands (default: false) +- `--branch-name-template`: Template for generated branch names (default: "$USERNAME/stack") +- `--merge-status-mode`: Mode for checking merge status when verifying PRs in GitHub (default: clean) + - `clean`: Only allow clean merge state + - `unstable`: Allow unstable and clean merge states. See [GitHub's documentation on MergeStateStatus](https://docs.github.com/en/enterprise-cloud@latest/graphql/reference/enums#mergestatestatus) for details + - `bypass`: Skip merge status checks completely. This is risky + +### Subcommands + +#### submit (alias: export) + +Submit a stack of PRs + +Options: + +- `--keep-body`: Keep current PR body, only update cross-links (default: false) +- `-d, --draft`: Submit PRs in draft mode (default: false) +- `--draft-bitmask`: Bitmask for setting draft status per PR +- `--reviewer`: List of reviewers for the PRs (default: from $STACK_PR_DEFAULT_REVIEWER or config) + +#### land + +Land the current stack + +Takes no additional arguments beyond common ones. + +#### abandon + +Abandon the current stack + +Takes no additional arguments beyond common ones. + +#### view + +Inspect the current stack + +Takes no additional arguments beyond common ones. ### Config files + Default values for command line options can be specified via a config file. Path to the config file can be specified via `STACKPR_CONFIG` envvar, and by default it's assumed to be `.stack-pr.cfg` in the current folder. diff --git a/src/stack_pr/cli.py b/src/stack_pr/cli.py index 764ce39..36f1886 100755 --- a/src/stack_pr/cli.py +++ b/src/stack_pr/cli.py @@ -180,6 +180,13 @@ If you use the default commit message filled by the web UI, links to other PRs from the stack will be included in the commit message. """ +from enum import Enum, auto + +class MergeStatusMode(str, Enum): + CLEAN = "clean" + UNSTABLE = "unstable" + BYPASS = "bypass" + # ===----------------------------------------------------------------------=== # # Class to work with git commit contents @@ -458,7 +465,7 @@ def set_base_branches(st: List[StackEntry], target: str): e.base, prev_branch = prev_branch, e._head -def verify(st: List[StackEntry], check_base: bool = False): +def verify(st: List[StackEntry], merge_status_mode: MergeStatusMode, check_base: bool = False): log(h("Verifying stack info"), level=1) for index, e in enumerate(st): if e.has_missing_info(): @@ -506,9 +513,20 @@ def verify(st: List[StackEntry], check_base: bool = False): raise RuntimeError # The first entry on the stack needs to be actually mergeable on GitHub. - if check_base and index == 0 and d["mergeStateStatus"] != "CLEAN" and d["mergeStateStatus"] != "UNKNOWN": - error(ERROR_STACKINFO_PR_NOT_MERGEABLE.format(**locals())) - raise RuntimeError + match merge_status_mode: + case MergeStatusMode.CLEAN: + if check_base and index == 0 and d["mergeStateStatus"] != "CLEAN" and d["mergeStateStatus"] != "UNKNOWN": + error(ERROR_STACKINFO_PR_NOT_MERGEABLE.format(**locals())) + raise RuntimeError + case MergeStatusMode.UNSTABLE: + log("Checking merge status and allowing UNSTABLE status", level=1) + if check_base and index == 0 and d["mergeStateStatus"] != "CLEAN" and d["mergeStateStatus"] != "UNKNOWN" and d["mergeStateStatus"] != "UNSTABLE": + error(ERROR_STACKINFO_PR_NOT_MERGEABLE.format(**locals())) + raise RuntimeError + case MergeStatusMode.BYPASS | _: + log("Bypassing merge status check", level=1) + raise NotImplementedError("Not implemented yet") # I don't want to test this locally, so not implementing for now. + def print_stack(st: List[StackEntry], links: bool, level=1): @@ -799,6 +817,7 @@ def update_local_base(base: str, remote: str, target: str, verbose: bool): ) + class CommonArgs(NamedTuple): """Class to help type checkers and separate implementation for CLI args.""" @@ -809,6 +828,7 @@ class CommonArgs(NamedTuple): hyperlinks: bool verbose: bool branch_name_template: str + merge_status_mode: MergeStatusMode @classmethod def from_args(cls, args: argparse.Namespace) -> "CommonArgs": @@ -820,6 +840,7 @@ def from_args(cls, args: argparse.Namespace) -> "CommonArgs": args.hyperlinks, args.verbose, args.branch_name_template, + args.merge_status_mode, ) @@ -848,6 +869,7 @@ def deduce_base(args: CommonArgs) -> CommonArgs: args.hyperlinks, args.verbose, args.branch_name_template, + args.merge_status_mode, ) @@ -929,7 +951,7 @@ def command_submit( create_pr(e, is_pr_draft, reviewer) # Verify consistency in everything we have so far - verify(st) + verify(st, args.merge_status_mode) # Embed stack-info into commit messages log(h("Updating commit messages with stack metadata"), level=1) @@ -1095,7 +1117,7 @@ def command_land(args: CommonArgs): print_stack(st, args.hyperlinks) # Verify that the stack is correct before trying to land it. - verify(st, check_base=True) + verify(st, args.merge_status_mode, check_base=True) # All good, land the bottommost PR! land_pr(st[0], args.remote, args.target, args.verbose) @@ -1308,6 +1330,13 @@ def create_argparser( ), help="A template for names of the branches stack-pr would use.", ) + common_parser.add_argument( + "--merge-status-mode", + type=MergeStatusMode, + choices=[x.value for x in MergeStatusMode], + default=MergeStatusMode.CLEAN, + help="Mode for checking merge status (clean, unstable, bypass). Default: clean. Ustable allows landing stacks where PRs still have failing checks, but are mergable (use with caution!). Bypass allows landing stacks where PRs are not mergable (do not use if you're not certain that's what you want).", + ) parser_submit = subparsers.add_parser( "submit",