a73x

src/cli.rs

Ref:   Size: 11.0 KiB

use clap::{Parser, Subcommand, ValueEnum};
use clap_complete::Shell;

#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum SortMode {
    /// Sort by last updated (most recent first)
    #[default]
    Recent,
    /// Sort by creation time (newest first)
    Created,
    /// Sort alphabetically by title
    Alpha,
}

/// Trait for items that support list filtering, sorting, and pagination.
pub trait Listable {
    fn is_open(&self) -> bool;
    fn last_updated(&self) -> &str;
    fn created_at(&self) -> &str;
    fn title(&self) -> &str;
}

/// Filter by open status, sort by mode, then apply offset/limit pagination.
pub fn filter_sort_paginate<T: Listable>(
    items: Vec<T>,
    show_closed: bool,
    sort: SortMode,
    offset: Option<usize>,
    limit: Option<usize>,
) -> Vec<T> {
    let mut filtered: Vec<T> = items
        .into_iter()
        .filter(|item| show_closed || item.is_open())
        .collect();
    match sort {
        SortMode::Recent => filtered.sort_by(|a, b| b.last_updated().cmp(a.last_updated())),
        SortMode::Created => filtered.sort_by(|a, b| b.created_at().cmp(a.created_at())),
        SortMode::Alpha => filtered.sort_by(|a, b| a.title().cmp(b.title())),
    }
    filtered
        .into_iter()
        .skip(offset.unwrap_or(0))
        .take(limit.unwrap_or(usize::MAX))
        .collect()
}

#[derive(Parser)]
#[command(
    name = "git-collab",
    about = "Distributed issues and code review over Git"
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Initialize collab refspecs on all remotes
    Init,

    /// Manage issues
    #[command(subcommand)]
    Issue(IssueCmd),

    /// Manage patches (code review)
    #[command(subcommand)]
    Patch(PatchCmd),

    /// Show project status overview
    Status,

    /// Interactive patch review dashboard
    Dashboard,

    /// Show raw collab event stream in chronological order
    Log {
        /// Maximum number of events to show
        #[arg(short = 'n', long)]
        limit: Option<usize>,
    },

    /// Generate shell completions
    Completions {
        /// Shell to generate completions for
        shell: Shell,
    },

    /// Sync with a remote (fetch, reconcile, push)
    Sync {
        /// Remote name (default: origin)
        #[arg(default_value = "origin")]
        remote: String,
    },

    /// Generate an Ed25519 signing keypair
    #[clap(name = "init-key")]
    InitKey {
        /// Overwrite existing key files
        #[arg(long)]
        force: bool,
    },

    /// Manage trusted keys
    #[command(subcommand)]
    Key(KeyCmd),

    /// Show current user identity
    Whoami,

    /// Manage identity aliases
    #[command(subcommand)]
    Identity(IdentityCmd),

    /// Full-text search across all issues and patches
    Search {
        /// Search query (case-insensitive substring match)
        query: String,
    },
}

#[derive(Subcommand)]
pub enum IssueCmd {
    /// Open a new issue
    Open {
        /// Issue title
        #[arg(short, long)]
        title: String,
        /// Issue body
        #[arg(short, long, default_value = "")]
        body: String,
        /// Related issue ID
        #[arg(long)]
        relates_to: Option<String>,
    },
    /// List issues
    List {
        /// Show closed issues too
        #[arg(short = 'a', long)]
        all: bool,
        /// Include archived issues
        #[arg(long)]
        archived: bool,
        /// Maximum number of issues to display
        #[arg(short = 'n', long)]
        limit: Option<usize>,
        /// Number of issues to skip before displaying
        #[arg(long)]
        offset: Option<usize>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Sort order: recent (default), created, alpha
        #[arg(long, default_value = "recent")]
        sort: SortMode,
    },
    /// Show issue details
    Show {
        /// Issue ID (prefix match)
        id: String,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Comment on an issue
    Comment {
        /// Issue ID (prefix match)
        id: String,
        /// Comment body
        #[arg(short, long)]
        body: String,
    },
    /// Edit an issue's title or body
    Edit {
        /// Issue ID (prefix match)
        id: String,
        /// New title
        #[arg(short, long)]
        title: Option<String>,
        /// New body
        #[arg(short, long)]
        body: Option<String>,
    },
    /// Add a label to an issue
    Label {
        /// Issue ID (prefix match)
        id: String,
        /// Label to add
        label: String,
    },
    /// Remove a label from an issue
    Unlabel {
        /// Issue ID (prefix match)
        id: String,
        /// Label to remove
        label: String,
    },
    /// Assign an issue to someone
    Assign {
        /// Issue ID (prefix match)
        id: String,
        /// Name to assign
        name: String,
    },
    /// Unassign someone from an issue
    Unassign {
        /// Issue ID (prefix match)
        id: String,
        /// Name to unassign
        name: String,
    },
    /// Close an issue
    Close {
        /// Issue ID (prefix match)
        id: String,
        /// Reason for closing
        #[arg(short, long)]
        reason: Option<String>,
    },
    /// Delete an issue (removes the local collab ref)
    Delete {
        /// Issue ID (prefix match)
        id: String,
    },
}

#[derive(Subcommand)]
pub enum KeyCmd {
    /// Add a trusted public key
    Add {
        /// Base64-encoded Ed25519 public key
        pubkey: Option<String>,
        /// Read public key from your own signing key
        #[arg(long = "self")]
        self_key: bool,
        /// Human-readable label for the key
        #[arg(long)]
        label: Option<String>,
        /// Store in global trust store (~/.config/git-collab/trusted-keys)
        #[arg(long)]
        global: bool,
    },
    /// List trusted public keys
    List {
        /// Show only global trusted keys
        #[arg(long)]
        global: bool,
    },
    /// Remove a trusted public key
    Remove {
        /// Base64-encoded public key to remove
        pubkey: String,
        /// Remove from global trust store (~/.config/git-collab/trusted-keys)
        #[arg(long)]
        global: bool,
    },
}

#[derive(Subcommand)]
pub enum PatchCmd {
    /// Create a new patch for review
    Create {
        /// Patch title
        #[arg(short, long)]
        title: String,
        /// Patch description
        #[arg(short, long, default_value = "")]
        body: String,
        /// Base branch ref
        #[arg(long, default_value = "main")]
        base: String,
        /// Source branch (defaults to current branch)
        #[arg(short = 'B', long)]
        branch: Option<String>,
        /// Issue ID this patch fixes (auto-closes on merge)
        #[arg(long)]
        fixes: Option<String>,
    },
    /// List patches
    List {
        /// Show closed/merged patches too
        #[arg(short = 'a', long)]
        all: bool,
        /// Include archived patches
        #[arg(long)]
        archived: bool,
        /// Maximum number of patches to display
        #[arg(short = 'n', long)]
        limit: Option<usize>,
        /// Number of patches to skip before displaying
        #[arg(long)]
        offset: Option<usize>,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Sort order: recent (default), created, alpha
        #[arg(long, default_value = "recent")]
        sort: SortMode,
    },
    /// Show patch details
    Show {
        /// Patch ID (prefix match)
        id: String,
        /// Output as JSON
        #[arg(long)]
        json: bool,
        /// Show only comments/reviews from this revision
        #[arg(long)]
        revision: Option<u32>,
    },
    /// Show diff between base and head
    Diff {
        /// Patch ID (prefix match)
        id: String,
        /// Show historical diff for a specific revision against the base
        #[arg(long)]
        revision: Option<u32>,
        /// Show interdiff between two revisions (N M or just N for N..latest)
        #[arg(long, num_args = 1..=2)]
        between: Option<Vec<u32>>,
    },
    /// Comment on a patch (use --file and --line for inline comments)
    Comment {
        /// Patch ID (prefix match)
        id: String,
        /// Comment body
        #[arg(short, long)]
        body: String,
        /// File path for inline comment
        #[arg(short, long)]
        file: Option<String>,
        /// Line number for inline comment
        #[arg(short, long)]
        line: Option<u32>,
        /// Target revision for inline comment
        #[arg(long)]
        revision: Option<u32>,
    },
    /// Review a patch
    Review {
        /// Patch ID (prefix match)
        id: String,
        /// Verdict: approve, request-changes, comment
        #[arg(short, long)]
        verdict: String,
        /// Review body
        #[arg(short, long)]
        body: String,
        /// Target revision for review
        #[arg(long)]
        revision: Option<u32>,
    },
    /// Revise a patch (record a new revision snapshot)
    Revise {
        /// Patch ID (prefix match)
        id: String,
        /// Revision description
        #[arg(short, long)]
        body: Option<String>,
    },
    /// Show revision log for a patch
    Log {
        /// Patch ID (prefix match)
        id: String,
        /// Output as JSON
        #[arg(long)]
        json: bool,
    },
    /// Close a patch
    Close {
        /// Patch ID (prefix match)
        id: String,
        /// Reason for closing
        #[arg(short, long)]
        reason: Option<String>,
    },
    /// Delete a patch (removes the local collab ref)
    Delete {
        /// Patch ID (prefix match)
        id: String,
    },
    /// Check out a patch's latest revision as a local branch
    Checkout {
        /// Patch ID (prefix match)
        id: String,
    },
}

impl Commands {
    pub fn is_write(&self) -> bool {
        match self {
            Commands::Issue(cmd) => matches!(
                cmd,
                IssueCmd::Open { .. }
                    | IssueCmd::Comment { .. }
                    | IssueCmd::Close { .. }
                    | IssueCmd::Edit { .. }
                    | IssueCmd::Label { .. }
                    | IssueCmd::Unlabel { .. }
                    | IssueCmd::Assign { .. }
                    | IssueCmd::Unassign { .. }
            ),
            Commands::Patch(cmd) => matches!(
                cmd,
                PatchCmd::Create { .. }
                    | PatchCmd::Comment { .. }
                    | PatchCmd::Review { .. }
                    | PatchCmd::Revise { .. }
                    | PatchCmd::Close { .. }
            ),
            _ => false,
        }
    }
}

#[derive(Subcommand)]
pub enum IdentityCmd {
    /// Link another email to your current identity
    Alias {
        /// Email address to add as alias
        email: String,
    },
    /// Remove an email alias
    Unalias {
        /// Email address to remove
        email: String,
    },
    /// Show current identity and aliases
    List,
}