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,
}