//! Command-line definitions and dispatch. mod format; mod identity; mod json; mod proposal; mod sync; pub mod ui; mod workspace; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use oxy_core::repo::Repo; use std::path::PathBuf; #[derive(Parser)] #[command( name = "oxy", version = env!("OXY_VERSION_LONG"), about = "Oxygen: breathing fresh air into version control" )] struct Cli { /// Plain output: no colour, no spinner (also implied by NO_COLOR or a non-terminal) #[arg(long, global = true)] plain: bool, #[command(subcommand)] cmd: Cmd, } #[derive(Subcommand)] enum Cmd { /// Create a repository in PATH (default: current directory) Init { /// Directory to create the repository in (default: the current directory) path: Option, /// No working copy: an object store and references only, for hosting #[arg(long)] bare: bool, }, /// Clone a repository from a local path or an http(s) URL Clone { /// Local path or http(s) URL of the repository url: String, /// Directory to clone into (default: the last part of the URL) dest: Option, /// Leave blobs larger than this (e.g. 1M) to be fetched on first read #[arg(long)] max_blob: Option, /// Materialize only these paths (repeatable); implies lazy blobs #[arg(long = "sparse")] sparse: Vec, /// Shallow clone: fetch only this many levels of history below the heads #[arg(long)] depth: Option, }, /// Fetch more history below a shallow clone's boundary Deepen { /// Levels of history to add below the boundary #[arg(default_value_t = 10)] depth: u32, /// Remote to fetch the history from #[arg(long, default_value = "origin")] remote: String, }, /// Show or set the sparse filter (paths to materialize); re-checks out the bookmark Sparse { /// Paths to materialize; none prints the current filter paths: Vec, }, /// Show the working copy: changed paths and conflicts Status { /// Machine-readable output: paths, and every conflict with its kind and contributing changes #[arg(long)] json: bool, }, /// One-line summary for shell prompts; fast and records nothing Prompt { /// Placeholders: %b bookmark, %c change id, %h heads, %d dirty (*), %x conflict (!), %A/%D ahead/behind (+n/-n), %a/%B raw counts, %s state glyph, %U/%O ahead/behind arrows, %i icon #[arg(long, default_value = oxy_core::repo::DEFAULT_PROMPT_FORMAT)] format: String, /// Glyph format (%i %b %c %s%U%O) instead of the plain default #[arg(long)] glyphs: bool, /// Colour by state (cyan clean, yellow dirty, red conflicted): raw escapes, or guarded for a bash or zsh PS1 #[arg(long, value_parser = ["raw", "bash", "zsh"])] color: Option, }, /// Record the working copy as a change on the current bookmark Commit { /// The change's message: a subject line, then optionally a blank line and a body #[arg(short, long)] message: String, /// Extra explicit dependencies (ChangeId prefix, #hash prefix) #[arg(long = "depends-on")] depends_on: Vec, /// Commit only these paths; the rest stays as uncommitted work paths: Vec, /// Choose hunks one by one #[arg(short, long)] interactive: bool, /// Commit these hunks (as `oxy hunks` numbers them), comma separated #[arg(long, value_delimiter = ',')] hunks: Vec, }, /// Fold CHANGE into one of its dependencies (keeps that change's id) Squash { /// The change to fold (ChangeId prefix, #hash prefix) change: String, /// The dependency that absorbs it #[arg(long)] into: String, }, /// Re-express CHANGE against another base state Rebase { /// The change to re-express (ChangeId prefix, #hash prefix) change: String, /// The changes, bookmark or tag whose state becomes the new base #[arg(long, required = true, num_args = 1..)] onto: Vec, }, /// Show which change introduced each line of a file Blame { /// File to annotate, relative to the repository root path: String, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Find the change that introduced a regression Bisect { #[command(subcommand)] cmd: BisectCmd, }, /// List changes reachable from the bookmark and the working copy Log { /// Show at most this many changes #[arg(short = 'n', long)] limit: Option, /// Show full messages, dependencies, and signatures #[arg(short, long)] verbose: bool, /// Only changes touching this path (renames followed) #[arg(long)] path: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Show what the working copy changes against its base Diff { /// A change instead of the working copy change: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Print a file as it is in a state (default: the working copy's base) Cat { /// File to print, relative to the repository root path: String, /// A change, bookmark, or tag #[arg(short = 'r', long)] change: Option, }, /// Print the repository root, workspace, and bookmark Root { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Print the version of oxy, its repository format, and protocol Version { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Restore paths to the working copy's base, or undo chosen hunks Discard { /// Paths to restore; none with --hunks, or everything with neither paths: Vec, /// Hunk indices as `oxy hunks` numbers them, comma separated #[arg(long, value_delimiter = ',')] hunks: Vec, }, /// List the working copy's hunks, numbered as commit --hunks expects Hunks { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Change the message of a head (default: the head the working copy sits on) Describe { /// The new message #[arg(short, long)] message: String, /// The head to describe (default: the one the working copy sits on) change: Option, }, /// Fold the working copy into the head it sits on, or into CHANGE with --into Amend { /// Replace the message too #[arg(short, long)] message: Option, /// Amend an older change; the edits must touch only lines it already knows #[arg(long)] into: Option, }, /// Split a head: the named paths become a separate first change Split { /// The head to split change: String, /// Paths whose edits become the first change paths: Vec, }, /// Drop a head from the bookmark; its content stays on disk as uncommitted work Abandon { /// The head to drop (ChangeId prefix, #hash prefix) change: String, }, /// Show one change (ChangeId prefix, #hash prefix, or @) Show { /// ChangeId prefix, #hash prefix, or @ for the working copy change: String, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Move the working copy to the state formed by CHANGE... (default: the bookmark heads) Checkout { /// Changes whose state to check out (default: the bookmark heads) changes: Vec, /// Throw away uncommitted working-copy changes #[arg(long)] discard: bool, }, /// Report which conflicts of the working copy's base are resolved by the current files Resolve { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Undo the last operation Undo, /// Write a checkpoint of the current bookmark's state now Checkpoint, /// Print a shell completion script: `oxy completions bash > ~/.local/share/bash-completion/completions/oxy` Completions { /// bash, zsh, fish, elvish or powershell shell: clap_complete::Shell, }, /// Write man pages (oxy.1 and one per command) into DIR, or print oxy.1 Man { /// Directory to write every page into, e.g. ~/.local/share/man/man1 #[arg(long)] dir: Option, }, /// Check the identity, configuration and remotes, and say what to do about any problem Doctor { /// Do not contact the remotes #[arg(long)] offline: bool, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Check the repository's integrity: objects, operation log, references, signatures, checkpoints Fsck { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Pack loose objects and prune unreachable ones older than the grace period Gc { /// Ignore the grace period (core.gc-grace) and prune immediately #[arg(long)] now: bool, }, /// List operations #[command(name = "op")] Op { #[command(subcommand)] cmd: OpCmd, }, /// Take the advisory local lock on a lockable file, making it writable Lock { /// The lockable file path: String, }, /// Release the local lock and make the file read-only again Unlock { /// The locked file path: String, }, /// Workspaces: extra checkout directories sharing this repository Workspace { #[command(subcommand)] cmd: WorkspaceCmd, }, /// Bookmarks: named states; each workspace works on one Bookmark { #[command(subcommand)] cmd: BookmarkCmd, }, /// Union another bookmark's heads into the current one (no merge change) Merge { /// The bookmark whose heads to add bookmark: String, }, /// Subrepositories: nested repositories pinned to a state Submodule { #[command(subcommand)] cmd: SubmoduleCmd, }, /// Tags: immutable named states Tag { #[command(subcommand)] cmd: TagCmd, }, /// Manage remotes Remote { #[command(subcommand)] cmd: RemoteCmd, }, /// Fetch every bookmark of a remote into its tracking references Fetch { /// Remote name (default: origin) remote: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Fetch, then union the remote bookmark into the local one (no merge change) Pull { /// Remote name (default: origin) remote: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Push the current bookmark to a remote Push { /// Remote name (default: origin) remote: Option, /// Allow dropping heads the remote has #[arg(long)] force: bool, /// Push this bookmark instead of the workspace's current one #[arg(short = 'b', long)] bookmark: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Serve repositories under ROOT over HTTPS (with --tls-cert/--tls-key) or plain HTTP Serve { /// Directory with one repository per subdirectory #[arg(long, default_value = ".")] root: PathBuf, /// Address and port to listen on #[arg(long, default_value = "127.0.0.1:8890")] bind: String, /// Bearer token allowed to push (repeatable); none means open #[arg(long = "token")] tokens: Vec, /// PEM certificate chain; enables HTTPS together with --tls-key #[arg(long, requires = "tls_key")] tls_cert: Option, /// PEM private key #[arg(long, requires = "tls_cert")] tls_key: Option, }, /// Manage the signing identity Identity { #[command(subcommand)] cmd: IdentityCmd, }, /// Seal changes with your key: named ones, or every change on the current bookmark you can authenticate but have not Seal { /// Changes to seal (default: every change on the bookmark you can authenticate and have not sealed) changes: Vec, /// Seal every change of a hub proposal and push the seal (your approval) #[arg(long, conflicts_with = "changes")] proposal: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Hub proposals: show one, or land it from this client Proposal { #[command(subcommand)] cmd: ProposalCmd, }, /// Verify the signatures on a change (default: every change in the log) Verify { /// The change to verify (default: every change in the log) change: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Git interoperability Git { #[command(subcommand)] cmd: GitCmd, }, /// Manage the trust policy (who may update which bookmarks) Policy { #[command(subcommand)] cmd: PolicyCmd, }, /// Set a configuration key (author.*, core.sign, core.op-retention, core.gc-grace, core.checkpoint-interval, filter.max-blob, filter.paths, workspace.bookmark, remote..url|token|ca) Config { /// The key, such as author.name or remote.origin.token key: String, /// The value to set value: String, /// Write to the user file (~/.config/oxy/config.toml) instead of this repository #[arg(long)] user: bool, }, } #[derive(Subcommand)] enum OpCmd { /// List operations, newest first Log { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, } #[derive(Subcommand)] enum WorkspaceCmd { /// Create a workspace at PATH checked out on CHANGE... (default: the bookmark heads) Add { /// Directory for the new workspace (must be empty or absent) path: PathBuf, /// Changes whose state to check out there changes: Vec, }, /// List workspaces and their working-copy changes List { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Stop tracking a workspace (by id prefix or path); its files stay Forget { /// Workspace id prefix or path workspace: String, }, } #[derive(Subcommand)] enum BisectCmd { /// Begin; remembers where the working copy was Start, /// Mark CHANGE... (or the state under test) as good Good { /// Changes known to be good (default: the state under test) changes: Vec, }, /// Mark CHANGE... (or the state under test) as bad Bad { /// Changes known to be bad (default: the state under test) changes: Vec, }, /// The state under test cannot be tested; try another Skip, /// Run a command for each step: exit 0 good, 125 skip, else bad Run { /// Command and arguments to run at each step #[arg(required = true, trailing_var_arg = true)] command: Vec, }, /// Where the search stands Status { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Restore the working copy and forget the search Reset, } #[derive(Subcommand)] enum BookmarkCmd { /// Create a bookmark at CHANGE... (default: the working copy's base) Create { /// Name of the new bookmark name: String, /// Its heads (default: the working copy's base) changes: Vec, }, /// Point an existing bookmark at CHANGE... Set { /// The bookmark to move name: String, /// Its new heads changes: Vec, }, /// Remove a bookmark; its changes stay Delete { /// The bookmark to remove name: String, }, /// List bookmarks with their heads and difference from origin List { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Make NAME this workspace's bookmark and check it out Switch { /// The bookmark to work on name: String, /// Throw away uncommitted working-copy changes #[arg(long)] discard: bool, }, } #[derive(Subcommand)] enum SubmoduleCmd { /// Clone URL (Oxygen or Git) into PATH and register it in .oxymodules Add { /// URL or path of the Oxygen or Git repository url: String, /// Where to put it, relative to the repository root path: String, /// Bookmark (Oxygen) or branch (Git) to check out and track #[arg(long)] bookmark: Option, }, /// Bring nested repositories to their pins Update { /// Clone nested repositories that are absent #[arg(long)] init: bool, /// Subrepos to update (default: all) paths: Vec, }, /// List subrepos with their pins and on-disk state Status { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Unregister PATH; the directory stays, the node goes with the next commit Remove { /// The subrepo to unregister path: String, }, } #[derive(Subcommand)] enum TagCmd { /// Tag the state of CHANGE... (default: the bookmark heads) Create { /// Name of the tag; tags never move name: String, /// Changes whose state to tag (default: the bookmark heads) changes: Vec, /// A message recorded with the tag #[arg(short, long, default_value = "")] message: String, }, /// List tags and the states they name List { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, } #[derive(Subcommand)] enum RemoteCmd { /// Add a remote Add { /// Name to refer to the remote by, such as origin name: String, /// Local path or http(s) URL of the repository url: String, }, /// Remove a remote Remove { /// The remote to remove name: String, }, /// List remotes and their URLs List { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, } #[derive(Subcommand)] enum ProposalCmd { /// Show a proposal by its hub URL Show { /// The proposal's URL on the hub url: String, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Fetch the proposal's changes, set the target bookmark to the projected state, and push with your key Land { /// The proposal's URL on the hub url: String, /// Land even when the hub reports blockers #[arg(long)] force: bool, }, } #[derive(Subcommand)] enum IdentityCmd { /// Generate a composite Ed25519 + ML-DSA-65 key pair and make it your author identity Create { /// Your name (default: author.name) #[arg(long)] name: Option, /// Your email (default: author.email) #[arg(long)] email: Option, /// Record the author in this repository only, not in the user config #[arg(long)] local: bool, /// Expire the identity this many days from now #[arg(long)] expires: Option, }, /// Show the configured identity Show, /// Write your identity object to a file, to hand to a policy holder Export { /// File to write (default: stdout) path: Option, }, /// Sign a host's challenge (hex, at least 16 bytes) to prove you hold your key; prints the signature as hex SignChallenge { /// The host's challenge, as hex challenge: String, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Move to a fresh key pair; the policy follows if it names your key Rotate, /// Re-issue your identity with a new expiry (in days from now; 0 = never) Renew { /// Days from now until it expires; 0 for never #[arg(long)] expires: u64, }, /// Revoke a key: yours by default, or any key if you hold a policy root Revoke { /// KeyId prefix (default: your own key) key: Option, /// Why, recorded in the revocation #[arg(long, default_value = "")] reason: String, }, /// Keys allowed to sign or seal on your behalf Delegate { #[command(subcommand)] cmd: DelegateCmd, }, } #[derive(Subcommand)] enum DelegateCmd { /// Add a delegate from an exported identity file Add { /// The delegate's exported identity file identity_file: PathBuf, }, /// Remove a delegate by KeyId prefix Remove { /// KeyId prefix of the delegate key: String, }, /// List delegates List, } #[derive(Subcommand)] enum GitCmd { /// Export the current bookmark to a Git repository (created bare if absent) Export { /// Git repository to write to (created bare if absent) git_dir: PathBuf, /// Git branch to write (default: the bookmark name) #[arg(long)] branch: Option, }, /// Fetch a branch from a Git remote (path or URL) into the current bookmark Fetch { /// Git remote: a path or URL url: String, /// Branch to fetch #[arg(long, default_value = "main")] branch: String, }, /// Push the current bookmark to a branch of a Git remote (fetch first) Push { /// Git remote: a path or URL url: String, /// Branch to push to #[arg(long, default_value = "main")] branch: String, }, /// Check this repository's import of a Git branch: every commit's tree must be reproduced exactly Verify { /// The Git repository the import came from git_dir: PathBuf, /// Branch that was imported #[arg(long, default_value = "main")] branch: String, }, /// Import a Git branch into a new Oxygen repository at DEST Import { /// Git repository to read git_dir: PathBuf, /// Directory for the new Oxygen repository dest: PathBuf, /// Branch to import #[arg(long, default_value = "main")] branch: String, }, } #[derive(Subcommand)] enum PolicyCmd { /// Create the initial policy with your identity as the only root Init, /// Show the current policy Show { /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Allow an identity (from `oxy identity export`) to update bookmarks Grant { /// The identity to grant, exported with `oxy identity export` identity_file: PathBuf, /// Reference pattern the grant applies to (default: every bookmark) #[arg(long)] pattern: Option, /// Where to write the proposal when more root signatures are needed #[arg(long)] out: Option, }, /// Revoke a key (KeyId prefix of an identity in the policy) Revoke { /// KeyId prefix of the identity key: String, /// Where to write the proposal when more root signatures are needed #[arg(long)] out: Option, }, /// Manage root keys and the signature threshold for policy updates Root { #[command(subcommand)] cmd: RootCmd, }, /// Add your root signature to a proposal file; applies it once the threshold is met Sign { /// The proposal file to sign proposal: PathBuf, /// Where to write the proposal if it still needs signatures (default: in place) #[arg(long)] out: Option, }, } #[derive(Subcommand)] enum RootCmd { /// Make an exported identity a root key Add { /// The identity to make a root, exported with `oxy identity export` identity_file: PathBuf, /// Where to write the proposal when more root signatures are needed #[arg(long)] out: Option, }, /// Remove a root key by KeyId prefix Remove { /// KeyId prefix of the root key key: String, /// Where to write the proposal when more root signatures are needed #[arg(long)] out: Option, }, /// Require this many root signatures on policy updates Threshold { /// Number of root signatures n: u32, /// Where to write the proposal when more root signatures are needed #[arg(long)] out: Option, }, } /// Open the repository containing the current directory. fn open() -> Result { Repo::open(&std::env::current_dir()?).context("opening repository") } /// `oxy man`: every page into `dir`, or the top-level page on stdout. fn man(dir: Option) -> Result { use clap::CommandFactory; let cmd = Cli::command(); match dir { Some(dir) => { std::fs::create_dir_all(&dir)?; clap_mangen::generate_to(cmd, &dir)?; let pages = std::fs::read_dir(&dir)? .filter(|e| { e.as_ref() .is_ok_and(|e| e.path().extension().is_some_and(|x| x == "1")) }) .count(); println!( "{}", ui::done(format!("Wrote {pages} man pages to {}", dir.display())) ); } None => clap_mangen::Man::new(cmd).render(&mut std::io::stdout())?, } Ok(0) } /// Parse arguments and run; returns the process exit code. pub fn run() -> Result { let cli = Cli::parse(); ui::init(cli.plain); match cli.cmd { Cmd::Completions { shell } => { use clap::CommandFactory; clap_complete::generate(shell, &mut Cli::command(), "oxy", &mut std::io::stdout()); Ok(0) } Cmd::Man { dir } => man(dir), Cmd::Init { path, bare } => workspace::init(path, bare), Cmd::Status { json } => workspace::status(json), Cmd::Prompt { format, glyphs, color, } => workspace::prompt(&format, glyphs, color.as_deref()), Cmd::Commit { message, depends_on, paths, interactive, hunks, } => workspace::commit(&message, &depends_on, &paths, interactive, &hunks), Cmd::Cat { path, change } => workspace::cat(&path, change.as_deref()), Cmd::Root { json } => workspace::root(json), Cmd::Version { json } => workspace::version(json), Cmd::Discard { paths, hunks } => workspace::discard(&paths, &hunks), Cmd::Hunks { json } => workspace::hunks(json), Cmd::Squash { change, into } => workspace::squash(&change, &into), Cmd::Rebase { change, onto } => workspace::rebase(&change, &onto), Cmd::Blame { path, json } => workspace::blame(&path, json), Cmd::Bisect { cmd } => match cmd { BisectCmd::Start => workspace::bisect_start(), BisectCmd::Good { changes } => workspace::bisect_mark(Some(true), &changes), BisectCmd::Bad { changes } => workspace::bisect_mark(Some(false), &changes), BisectCmd::Skip => workspace::bisect_mark(None, &[]), BisectCmd::Run { command } => workspace::bisect_run(&command), BisectCmd::Status { json } => workspace::bisect_status(json), BisectCmd::Reset => workspace::bisect_reset(), }, Cmd::Log { limit, verbose, path, json, } => workspace::log(limit, verbose, path.as_deref(), json), Cmd::Diff { change, json } => workspace::diff(change.as_deref(), json), Cmd::Describe { message, change } => workspace::describe(&message, change.as_deref()), Cmd::Amend { message, into } => workspace::amend(message.as_deref(), into.as_deref()), Cmd::Split { change, paths } => workspace::split(&change, &paths), Cmd::Abandon { change } => workspace::abandon(&change), Cmd::Show { change, json } => workspace::show(&change, json), Cmd::Checkout { changes, discard } => workspace::checkout(&changes, discard), Cmd::Resolve { json } => workspace::resolve(json), Cmd::Undo => workspace::undo(), Cmd::Gc { now } => workspace::gc(now), Cmd::Fsck { json } => workspace::fsck(json), Cmd::Doctor { offline, json } => workspace::doctor(offline, json), Cmd::Checkpoint => workspace::checkpoint(), Cmd::Op { cmd: OpCmd::Log { json }, } => workspace::op_log(json), Cmd::Clone { url, dest, max_blob, sparse, depth, } => sync::clone(&url, dest, max_blob.as_deref(), &sparse, depth), Cmd::Deepen { depth, remote } => sync::deepen(&remote, depth), Cmd::Sparse { paths } => workspace::sparse(&paths), Cmd::Lock { path } => workspace::lock(&path), Cmd::Unlock { path } => workspace::unlock(&path), Cmd::Workspace { cmd } => match cmd { WorkspaceCmd::Add { path, changes } => workspace::workspace_add(&path, &changes), WorkspaceCmd::List { json } => workspace::workspace_list(json), WorkspaceCmd::Forget { workspace } => workspace::workspace_forget(&workspace), }, Cmd::Bookmark { cmd } => match cmd { BookmarkCmd::Create { name, changes } => workspace::bookmark_create(&name, &changes), BookmarkCmd::Set { name, changes } => workspace::bookmark_set(&name, &changes), BookmarkCmd::Delete { name } => workspace::bookmark_delete(&name), BookmarkCmd::List { json } => workspace::bookmark_list(json), BookmarkCmd::Switch { name, discard } => workspace::bookmark_switch(&name, discard), }, Cmd::Merge { bookmark } => workspace::merge(&bookmark), Cmd::Submodule { cmd } => match cmd { SubmoduleCmd::Add { url, path, bookmark, } => sync::submodule_add(&url, &path, bookmark.as_deref()), SubmoduleCmd::Update { init, paths } => sync::submodule_update(init, &paths), SubmoduleCmd::Status { json } => sync::submodule_status(json), SubmoduleCmd::Remove { path } => sync::submodule_remove(&path), }, Cmd::Tag { cmd } => match cmd { TagCmd::Create { name, changes, message, } => workspace::tag_create(&name, &changes, &message), TagCmd::List { json } => workspace::tag_list(json), }, Cmd::Remote { cmd } => match cmd { RemoteCmd::Add { name, url } => sync::remote_add(&name, &url), RemoteCmd::Remove { name } => sync::remote_remove(&name), RemoteCmd::List { json } => sync::remote_list(json), }, Cmd::Fetch { remote, json } => sync::fetch(remote.as_deref(), json), Cmd::Pull { remote, json } => sync::pull(remote.as_deref(), json), Cmd::Push { remote, force, bookmark, json, } => sync::push(remote.as_deref(), force, bookmark.as_deref(), json), Cmd::Serve { root, bind, tokens, tls_cert, tls_key, } => sync::serve(root, &bind, tokens, tls_cert.zip(tls_key)), Cmd::Identity { cmd } => match cmd { IdentityCmd::Create { name, email, local, expires, } => identity::create(name, email, local, expires), IdentityCmd::Show => identity::show(), IdentityCmd::Export { path } => identity::export(path), IdentityCmd::SignChallenge { challenge, json } => { identity::sign_challenge(&challenge, json) } IdentityCmd::Rotate => identity::rotate(), IdentityCmd::Renew { expires } => identity::renew(expires), IdentityCmd::Revoke { key, reason } => identity::revoke(key.as_deref(), &reason), IdentityCmd::Delegate { cmd } => match cmd { DelegateCmd::Add { identity_file } => identity::delegate_add(&identity_file), DelegateCmd::Remove { key } => identity::delegate_remove(&key), DelegateCmd::List => identity::delegate_list(), }, }, Cmd::Git { cmd: GitCmd::Export { git_dir, branch }, } => sync::git_export(&git_dir, branch.as_deref()), Cmd::Git { cmd: GitCmd::Fetch { url, branch }, } => sync::git_fetch(&url, &branch), Cmd::Git { cmd: GitCmd::Push { url, branch }, } => sync::git_push(&url, &branch), Cmd::Git { cmd: GitCmd::Import { git_dir, dest, branch, }, } => sync::git_import(&git_dir, &dest, &branch), Cmd::Git { cmd: GitCmd::Verify { git_dir, branch }, } => sync::git_verify(&git_dir, &branch), Cmd::Policy { cmd } => match cmd { PolicyCmd::Init => identity::policy_init(), PolicyCmd::Show { json } => identity::policy_show(json), PolicyCmd::Grant { identity_file, pattern, out, } => identity::policy_grant(&identity_file, pattern.as_deref(), out), PolicyCmd::Revoke { key, out } => identity::policy_revoke(&key, out), PolicyCmd::Root { cmd } => match cmd { RootCmd::Add { identity_file, out } => { identity::policy_root_add(&identity_file, out) } RootCmd::Remove { key, out } => identity::policy_root_remove(&key, out), RootCmd::Threshold { n, out } => identity::policy_threshold(n, out), }, PolicyCmd::Sign { proposal, out } => identity::policy_sign(&proposal, out), }, Cmd::Seal { changes, proposal: Some(url), json, } if changes.is_empty() => proposal::seal(&url, json), Cmd::Seal { changes, json, .. } => identity::seal(&changes, json), Cmd::Proposal { cmd } => match cmd { ProposalCmd::Show { url, json } => proposal::show(&url, json), ProposalCmd::Land { url, force } => proposal::land(&url, force), }, Cmd::Verify { change, json } => identity::verify(change.as_deref(), json), Cmd::Config { key, value, user } => identity::config(&key, &value, user), } } #[cfg(test)] mod help_tests { use super::Cli; use clap::CommandFactory; /// Every command and every argument explains itself, so `--help`, /// completions and the man pages are complete. #[test] fn every_command_and_argument_has_help() { fn walk(cmd: &clap::Command, path: &str, missing: &mut Vec) { let here = format!("{path} {}", cmd.get_name()).trim().to_string(); if cmd.get_about().is_none() && path != "" { missing.push(format!("{here} (command)")); } for a in cmd.get_arguments() { let id = a.get_id().as_str(); if id == "help" || id == "version" || a.is_global_set() && path != "" { continue; } if a.get_help().is_none() && a.get_long_help().is_none() { missing.push(format!("{here} {id}")); } } for sub in cmd.get_subcommands() { walk(sub, &here, missing); } } let mut missing = Vec::new(); walk(&Cli::command(), "", &mut missing); assert!( missing.is_empty(), "{} without help:\n{}", missing.len(), missing.join("\n") ); } }