quinjet 0.0.69

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

use super::github::PullRequestMergeMethod;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StackModifyAction {
    Abort,
    Continue,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StackRebaseAction {
    Start {
        branch: Option<String>,
        downstack: bool,
        upstack: bool,
        no_trunk: bool,
        preserve_dates: bool,
        remote: Option<String>,
    },
    Abort,
    Continue,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StackOperation {
    Init {
        branches: Vec<String>,
        base: Option<String>,
    },
    Add {
        branch: String,
        all: bool,
        update: bool,
        message: Option<String>,
    },
    Checkout(String),
    Modify(StackModifyAction),
    Unstack {
        stack: Option<String>,
        local: bool,
    },
    Link {
        members: Vec<String>,
        base: Option<String>,
        open: bool,
        remote: Option<String>,
    },
    Merge {
        target: Option<String>,
        method: PullRequestMergeMethod,
    },
    Push {
        remote: Option<String>,
    },
    Rebase(StackRebaseAction),
    Submit {
        open: bool,
        remote: Option<String>,
    },
    Sync {
        prune: bool,
        remote: Option<String>,
    },
    Bottom,
    Down(u64),
    Top,
    Trunk,
    Up(u64),
}

impl StackOperation {
    pub(crate) fn arguments(&self) -> Vec<OsString> {
        let mut arguments = vec![OsString::from("stack")];
        match self {
            Self::Init { branches, base } => {
                arguments.push(OsString::from("init"));
                push_option(&mut arguments, "--base", base.as_ref());
                if !branches.is_empty() {
                    arguments.push(OsString::from("--"));
                }
                arguments.extend(branches.iter().map(OsString::from));
            }
            Self::Add {
                branch,
                all,
                update,
                message,
            } => {
                arguments.push(OsString::from("add"));
                if *all {
                    arguments.push(OsString::from("--all"));
                }
                if *update {
                    arguments.push(OsString::from("--update"));
                }
                push_option(&mut arguments, "--message", message.as_ref());
                arguments.push(OsString::from("--"));
                arguments.push(OsString::from(branch));
            }
            Self::Checkout(target) => {
                arguments.extend([OsString::from("checkout"), OsString::from("--")]);
                arguments.push(OsString::from(target));
            }
            Self::Modify(action) => {
                arguments.push(OsString::from("modify"));
                arguments.push(OsString::from(match action {
                    StackModifyAction::Abort => "--abort",
                    StackModifyAction::Continue => "--continue",
                }));
            }
            Self::Unstack { stack, local } => {
                arguments.push(OsString::from("unstack"));
                if *local {
                    arguments.push(OsString::from("--local"));
                }
                push_positional(&mut arguments, stack.as_ref());
            }
            Self::Link {
                members,
                base,
                open,
                remote,
            } => {
                arguments.push(OsString::from("link"));
                push_option(&mut arguments, "--base", base.as_ref());
                if *open {
                    arguments.push(OsString::from("--open"));
                }
                push_option(&mut arguments, "--remote", remote.as_ref());
                arguments.push(OsString::from("--"));
                arguments.extend(members.iter().map(OsString::from));
            }
            Self::Merge { target, method } => {
                arguments.extend([
                    OsString::from("merge"),
                    OsString::from(method.flag()),
                    OsString::from("--yes"),
                ]);
                push_positional(&mut arguments, target.as_ref());
            }
            Self::Push { remote } => {
                arguments.push(OsString::from("push"));
                push_option(&mut arguments, "--remote", remote.as_ref());
            }
            Self::Rebase(action) => {
                arguments.push(OsString::from("rebase"));
                push_rebase_arguments(&mut arguments, action);
            }
            Self::Submit { open, remote } => {
                arguments.extend([OsString::from("submit"), OsString::from("--auto")]);
                if *open {
                    arguments.push(OsString::from("--open"));
                }
                push_option(&mut arguments, "--remote", remote.as_ref());
            }
            Self::Sync { prune, remote } => {
                arguments.push(OsString::from("sync"));
                if *prune {
                    arguments.push(OsString::from("--prune"));
                }
                push_option(&mut arguments, "--remote", remote.as_ref());
            }
            Self::Bottom => arguments.push(OsString::from("bottom")),
            Self::Down(steps) => {
                arguments.extend([OsString::from("down"), OsString::from(steps.to_string())]);
            }
            Self::Top => arguments.push(OsString::from("top")),
            Self::Trunk => arguments.push(OsString::from("trunk")),
            Self::Up(steps) => {
                arguments.extend([OsString::from("up"), OsString::from(steps.to_string())]);
            }
        }
        arguments
    }

    pub(crate) const fn progress_label(&self) -> &'static str {
        match self {
            Self::Init { .. } => "Initializing stack",
            Self::Add { .. } => "Adding stack branch",
            Self::Checkout(_) => "Checking out stack",
            Self::Modify(_) => "Recovering stack modification",
            Self::Unstack { .. } => "Removing stack tracking",
            Self::Link { .. } => "Linking pull request stack",
            Self::Merge { .. } => "Merging pull request stack",
            Self::Push { .. } => "Pushing stack branches",
            Self::Rebase(_) => "Rebasing stack branches",
            Self::Submit { .. } => "Submitting pull request stack",
            Self::Sync { .. } => "Synchronizing pull request stack",
            Self::Bottom | Self::Down(_) | Self::Top | Self::Trunk | Self::Up(_) => {
                "Navigating stack branches"
            }
        }
    }

    pub(crate) fn preview_message(&self) -> String {
        format!(
            "Would {}. Pass --yes to continue.",
            self.preview_description()
        )
    }

    pub(crate) const fn success_message(&self) -> &'static str {
        match self {
            Self::Init { .. } => "Stack initialized",
            Self::Add { .. } => "Branch added to stack",
            Self::Checkout(_) => "Stack checked out",
            Self::Modify(StackModifyAction::Abort) => "Stack modification aborted",
            Self::Modify(StackModifyAction::Continue) => "Stack modification continued",
            Self::Unstack { .. } => "Stack tracking removed",
            Self::Link { .. } => "Pull requests linked into a stack",
            Self::Merge { .. } => "Stack merged",
            Self::Push { .. } => "Stack branches pushed",
            Self::Rebase(StackRebaseAction::Abort) => "Stack rebase aborted",
            Self::Rebase(StackRebaseAction::Continue) => "Stack rebase continued",
            Self::Rebase(StackRebaseAction::Start { .. }) => "Stack rebased",
            Self::Submit { .. } => "Stack submitted",
            Self::Sync { .. } => "Stack synchronized",
            Self::Bottom => "Switched to the bottom stack branch",
            Self::Down(_) => "Moved down the stack",
            Self::Top => "Switched to the top stack branch",
            Self::Trunk => "Switched to the stack trunk",
            Self::Up(_) => "Moved up the stack",
        }
    }

    fn preview_description(&self) -> String {
        match self {
            Self::Init { .. } => "initialize a stack".to_owned(),
            Self::Add {
                branch,
                all,
                update,
                message,
            } => message.as_ref().map_or_else(
                || format!("add branch {branch} to the stack"),
                |message| {
                    if *all {
                        format!(
                            "add branch {branch}, stage all changes, and commit them as `{message}`"
                        )
                    } else if *update {
                        format!(
                            "add branch {branch}, stage tracked changes, and commit them as `{message}`"
                        )
                    } else {
                        format!("add branch {branch} and commit staged changes as `{message}`")
                    }
                },
            ),
            Self::Checkout(target) => format!("check out stack {target}"),
            Self::Modify(StackModifyAction::Abort) => {
                "abort the active stack modification".to_owned()
            }
            Self::Modify(StackModifyAction::Continue) => {
                "continue the active stack modification".to_owned()
            }
            Self::Unstack { stack, local } => stack.as_ref().map_or_else(
                || {
                    if *local {
                        "remove local tracking for the active stack".to_owned()
                    } else {
                        "remove local and GitHub tracking for the active stack".to_owned()
                    }
                },
                |stack| {
                    if *local {
                        format!("remove local tracking for stack {stack}")
                    } else {
                        format!("remove local and GitHub tracking for stack {stack}")
                    }
                },
            ),
            Self::Link { members, .. } => {
                format!("link {} members into a pull request stack", members.len())
            }
            Self::Merge { target, method } => target.as_ref().map_or_else(
                || format!("atomically {} the active stack", method.preview_verb()),
                |target| format!("atomically {} stack {target}", method.preview_verb()),
            ),
            Self::Push { .. } => "push the active stack".to_owned(),
            Self::Rebase(StackRebaseAction::Abort) => "abort the active stack rebase".to_owned(),
            Self::Rebase(StackRebaseAction::Continue) => {
                "continue the active stack rebase".to_owned()
            }
            Self::Rebase(StackRebaseAction::Start { .. }) => "rebase the active stack".to_owned(),
            Self::Submit { .. } => "submit the active stack".to_owned(),
            Self::Sync { prune, .. } => {
                if *prune {
                    "synchronize the active stack and prune merged local branches".to_owned()
                } else {
                    "synchronize the active stack".to_owned()
                }
            }
            Self::Bottom => "switch to the bottom stack branch".to_owned(),
            Self::Down(steps) => format!("move {steps} branches down the stack"),
            Self::Top => "switch to the top stack branch".to_owned(),
            Self::Trunk => "switch to the stack trunk".to_owned(),
            Self::Up(steps) => format!("move {steps} branches up the stack"),
        }
    }
}

fn push_option(arguments: &mut Vec<OsString>, flag: &str, value: Option<&String>) {
    if let Some(value) = value {
        arguments.extend([OsString::from(flag), OsString::from(value)]);
    }
}

fn push_positional(arguments: &mut Vec<OsString>, value: Option<&String>) {
    if let Some(value) = value {
        arguments.extend([OsString::from("--"), OsString::from(value)]);
    }
}

fn push_rebase_arguments(arguments: &mut Vec<OsString>, action: &StackRebaseAction) {
    match action {
        StackRebaseAction::Start {
            branch,
            downstack,
            upstack,
            no_trunk,
            preserve_dates,
            remote,
        } => {
            if *downstack {
                arguments.push(OsString::from("--downstack"));
            }
            if *upstack {
                arguments.push(OsString::from("--upstack"));
            }
            if *no_trunk {
                arguments.push(OsString::from("--no-trunk"));
            }
            if *preserve_dates {
                arguments.push(OsString::from("--committer-date-is-author-date"));
            }
            push_option(arguments, "--remote", remote.as_ref());
            push_positional(arguments, branch.as_ref());
        }
        StackRebaseAction::Abort => arguments.push(OsString::from("--abort")),
        StackRebaseAction::Continue => arguments.push(OsString::from("--continue")),
    }
}