quinjet 0.0.56

A fast, live, keyboard-first Git source-control interface for the terminal
use std::path::PathBuf;

use clap::{Args, Subcommand};

use super::{PrArgs, PrMergeMethodArgs};
use crate::git::{StackModifyAction, StackOperation, StackRebaseAction};

#[derive(Debug, Subcommand)]
pub(super) enum StackVerb {
    #[doc = "Print the ordered pull requests in a stack"]
    View(PrArgs),
    #[doc = "List files across a contiguous stack range"]
    Files(StackRangeArgs),
    #[doc = "Print the patch across a contiguous stack range"]
    Diff(StackDiffArgs),
    #[doc = "Initialize a local branch stack"]
    Init(StackInitArgs),
    #[doc = "Add a branch to the active stack"]
    Add(StackAddArgs),
    #[doc = "Check out a stack"]
    Checkout(StackCheckoutArgs),
    #[doc = "Abort or continue a stack modification"]
    Modify(StackModifyArgs),
    #[doc = "Remove local or GitHub stack tracking"]
    Unstack(StackUnstackArgs),
    #[doc = "Link branches or pull requests into a GitHub stack"]
    Link(StackLinkArgs),
    #[doc = "Atomically merge a stack"]
    Merge(StackMergeArgs),
    #[doc = "Push active branches in the current stack"]
    Push(StackRemoteArgs),
    #[doc = "Cascade-rebase branches in the current stack"]
    Rebase(StackRebaseArgs),
    #[doc = "Push branches and create or update stacked pull requests"]
    Submit(StackSubmitArgs),
    #[doc = "Fetch, rebase, push, and synchronize the current stack"]
    Sync(StackSyncArgs),
    #[doc = "Check out the bottom branch of the current stack"]
    Bottom(StackConfirmArgs),
    #[doc = "Move toward the trunk in the current stack"]
    Down(StackStepArgs),
    #[doc = "Check out the top branch of the current stack"]
    Top(StackConfirmArgs),
    #[doc = "Check out the trunk of the current stack"]
    Trunk(StackConfirmArgs),
    #[doc = "Move toward the top of the current stack"]
    Up(StackStepArgs),
}

impl StackVerb {
    pub(super) fn into_operation(self) -> Option<(StackOperation, bool)> {
        match self {
            Self::View(_) | Self::Files(_) | Self::Diff(_) => None,
            Self::Init(args) => Some((
                StackOperation::Init {
                    branches: args.branches,
                    base: args.base,
                },
                args.yes,
            )),
            Self::Add(args) => Some((
                StackOperation::Add {
                    branch: args.branch,
                    all: args.all,
                    update: args.update,
                    message: args.message,
                },
                args.yes,
            )),
            Self::Checkout(args) => Some((StackOperation::Checkout(args.target), args.yes)),
            Self::Modify(args) => Some((
                StackOperation::Modify(if args.abort {
                    StackModifyAction::Abort
                } else {
                    StackModifyAction::Continue
                }),
                args.yes,
            )),
            Self::Unstack(args) => Some((
                StackOperation::Unstack {
                    stack: args.stack.map(|stack| stack.to_string()),
                    local: args.local,
                },
                args.yes,
            )),
            Self::Link(args) => Some((
                StackOperation::Link {
                    members: args.members,
                    base: args.base,
                    open: args.open,
                    remote: args.remote,
                },
                args.yes,
            )),
            Self::Merge(args) => Some((
                StackOperation::Merge {
                    target: args.target.map(|target| target.to_string()),
                    method: args.method.method(),
                },
                args.yes,
            )),
            Self::Push(args) => Some((
                StackOperation::Push {
                    remote: args.remote,
                },
                args.yes,
            )),
            Self::Rebase(args) => {
                let yes = args.yes;
                Some((StackOperation::Rebase(args.action()), yes))
            }
            Self::Submit(args) => Some((
                StackOperation::Submit {
                    open: args.open,
                    remote: args.remote,
                },
                args.yes,
            )),
            Self::Sync(args) => Some((
                StackOperation::Sync {
                    prune: args.prune,
                    remote: args.remote,
                },
                args.yes,
            )),
            Self::Bottom(args) => Some((StackOperation::Bottom, args.yes)),
            Self::Down(args) => Some((StackOperation::Down(args.steps), args.yes)),
            Self::Top(args) => Some((StackOperation::Top, args.yes)),
            Self::Trunk(args) => Some((StackOperation::Trunk, args.yes)),
            Self::Up(args) => Some((StackOperation::Up(args.steps), args.yes)),
        }
    }
}

#[derive(Debug, Args)]
pub(super) struct StackRangeArgs {
    #[command(flatten)]
    pub(super) pull_request: PrArgs,
    #[doc = "First one-based stack position to include"]
    #[arg(long, value_name = "POSITION")]
    pub(super) from: Option<usize>,
    #[doc = "Last one-based stack position to include"]
    #[arg(long, value_name = "POSITION")]
    pub(super) to: Option<usize>,
}

#[derive(Debug, Args)]
pub(super) struct StackDiffArgs {
    #[command(flatten)]
    pub(super) range: StackRangeArgs,
    #[doc = "Show only one changed path"]
    #[arg(value_name = "PATH")]
    pub(super) path: Option<PathBuf>,
}

#[derive(Debug, Args)]
pub(super) struct StackInitArgs {
    #[doc = "Use this trunk branch instead of the repository default"]
    #[arg(long, short = 'b', value_name = "BRANCH", value_parser = non_empty)]
    pub(super) base: Option<String>,
    #[doc = "Branches to adopt or create, ordered from bottom to top"]
    #[arg(value_name = "BRANCH", num_args = 1.., required = true, value_parser = non_empty)]
    pub(super) branches: Vec<String>,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackAddArgs {
    #[doc = "Branch to create on top of the active stack"]
    #[arg(value_name = "BRANCH", value_parser = non_empty)]
    pub(super) branch: String,
    #[doc = "Stage tracked and untracked changes before committing"]
    #[arg(long, short = 'A', conflicts_with = "update", requires = "message")]
    pub(super) all: bool,
    #[doc = "Stage tracked changes before committing"]
    #[arg(long, short = 'u', conflicts_with = "all", requires = "message")]
    pub(super) update: bool,
    #[doc = "Commit staged changes with this message"]
    #[arg(long, short = 'm', value_name = "MESSAGE", value_parser = non_empty)]
    pub(super) message: Option<String>,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackCheckoutArgs {
    #[doc = "Stack number, pull request, URL, or locally tracked branch"]
    #[arg(value_name = "STACK|PR|URL|BRANCH", value_parser = non_empty)]
    pub(super) target: String,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackModifyArgs {
    #[doc = "Abort the active modification and restore the stack"]
    #[arg(
        long,
        conflicts_with = "continue",
        required_unless_present = "continue"
    )]
    pub(super) abort: bool,
    #[doc = "Continue the active modification after resolving conflicts"]
    #[arg(long, conflicts_with = "abort", required_unless_present = "abort")]
    pub(super) r#continue: bool,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackUnstackArgs {
    #[doc = "Stack number; defaults to the active stack"]
    #[arg(value_name = "STACK", value_parser = clap::value_parser!(u64).range(1..))]
    pub(super) stack: Option<u64>,
    #[doc = "Remove only local tracking and keep the GitHub stack"]
    #[arg(long)]
    pub(super) local: bool,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackLinkArgs {
    #[doc = "Stack number, branches, pull requests, or URLs in stack order"]
    #[arg(
        value_name = "STACK|BRANCH|PR",
        num_args = 2..,
        required = true,
        value_parser = non_empty
    )]
    pub(super) members: Vec<String>,
    #[doc = "Base branch for the bottom pull request"]
    #[arg(long, value_name = "BRANCH", value_parser = non_empty)]
    pub(super) base: Option<String>,
    #[doc = "Mark linked pull requests ready for review"]
    #[arg(long)]
    pub(super) open: bool,
    #[doc = "Git remote used to push branches"]
    #[arg(id = "git_remote", long = "git-remote", value_name = "REMOTE", value_parser = non_empty)]
    pub(super) remote: Option<String>,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackMergeArgs {
    #[doc = "Stack or pull request number; defaults to the active stack"]
    #[arg(value_name = "STACK|PR", value_parser = clap::value_parser!(u64).range(1..))]
    pub(super) target: Option<u64>,
    #[command(flatten)]
    pub(super) method: PrMergeMethodArgs,
    #[doc = "Confirm the atomic merge"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackRemoteArgs {
    #[doc = "Git remote used to push branches"]
    #[arg(id = "git_remote", long = "git-remote", value_name = "REMOTE", value_parser = non_empty)]
    pub(super) remote: Option<String>,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
#[expect(
    clippy::struct_excessive_bools,
    reason = "these booleans model independent command-line flags"
)]
pub(super) struct StackRebaseArgs {
    #[doc = "Branch that bounds a directional rebase"]
    #[arg(value_name = "BRANCH", conflicts_with_all = ["abort", "continue"], value_parser = non_empty)]
    pub(super) branch: Option<String>,
    #[doc = "Abort the active rebase and restore every branch"]
    #[arg(long, conflicts_with_all = ["continue", "branch", "downstack", "upstack", "no_trunk", "preserve_dates", "git_remote"])]
    pub(super) abort: bool,
    #[doc = "Continue the active rebase after resolving conflicts"]
    #[arg(long, conflicts_with_all = ["abort", "branch", "downstack", "upstack", "no_trunk", "preserve_dates", "git_remote"])]
    pub(super) r#continue: bool,
    #[doc = "Rebase from the trunk through the selected branch"]
    #[arg(long, conflicts_with = "upstack")]
    pub(super) downstack: bool,
    #[doc = "Rebase from the selected branch through the stack top"]
    #[arg(long, conflicts_with = "downstack")]
    pub(super) upstack: bool,
    #[doc = "Rebase stack branches without updating the trunk"]
    #[arg(long)]
    pub(super) no_trunk: bool,
    #[doc = "Preserve author dates as committer dates"]
    #[arg(
        long = "committer-date-is-author-date",
        visible_alias = "preserve-dates"
    )]
    pub(super) preserve_dates: bool,
    #[doc = "Git remote used to fetch branches"]
    #[arg(id = "git_remote", long = "git-remote", value_name = "REMOTE", value_parser = non_empty)]
    pub(super) remote: Option<String>,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

impl StackRebaseArgs {
    fn action(self) -> StackRebaseAction {
        if self.abort {
            StackRebaseAction::Abort
        } else if self.r#continue {
            StackRebaseAction::Continue
        } else {
            StackRebaseAction::Start {
                branch: self.branch,
                downstack: self.downstack,
                upstack: self.upstack,
                no_trunk: self.no_trunk,
                preserve_dates: self.preserve_dates,
                remote: self.remote,
            }
        }
    }
}

#[derive(Debug, Args)]
pub(super) struct StackSubmitArgs {
    #[doc = "Mark new and existing pull requests ready for review"]
    #[arg(long)]
    pub(super) open: bool,
    #[doc = "Git remote used to push branches"]
    #[arg(id = "git_remote", long = "git-remote", value_name = "REMOTE", value_parser = non_empty)]
    pub(super) remote: Option<String>,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackSyncArgs {
    #[doc = "Delete local branches for merged pull requests"]
    #[arg(long)]
    pub(super) prune: bool,
    #[doc = "Git remote used to fetch and push branches"]
    #[arg(id = "git_remote", long = "git-remote", value_name = "REMOTE", value_parser = non_empty)]
    pub(super) remote: Option<String>,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackConfirmArgs {
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

#[derive(Debug, Args)]
pub(super) struct StackStepArgs {
    #[doc = "Number of branches to move"]
    #[arg(default_value_t = 1, value_parser = clap::value_parser!(u64).range(1..))]
    pub(super) steps: u64,
    #[doc = "Confirm; without it the command reports what it would do"]
    #[arg(long)]
    pub(super) yes: bool,
}

fn non_empty(value: &str) -> Result<String, String> {
    if value.trim().is_empty() {
        Err("value cannot be empty".to_owned())
    } else {
        Ok(value.to_owned())
    }
}