//! Command-line definitions and dispatch. mod auth; mod format; mod identity; mod json; mod proposal; mod receipt; 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, 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, /// Read the repository as it was after operation OP (an ID from oxy op log, @ or @-); commands that would change it are refused #[arg(long, global = true, value_name = "OP")] at_op: Option, #[command(subcommand)] cmd: Cmd, } /// `--at-op`, for `open` to apply. static AT_OP: std::sync::OnceLock> = std::sync::OnceLock::new(); #[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, /// Clone the subrepos too (as oxy submodule update --init) #[arg(long)] recursive: bool, }, /// 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, required_unless_present = "file", conflicts_with = "file")] message: Option, /// Read the message from FILE (`-` for standard input) #[arg(short = 'F', long, value_name = "FILE")] file: Option, /// 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, /// Where the rebase would be conflicted, merge the target into the /// current bookmark instead, leaving the conflict in the working /// copy to fix and commit #[arg(long)] into_working_copy: bool, }, /// 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, /// Only changes that wrote or deleted lines START to END of PATH /// (numbered as in the working copy's base), or deleted lines from /// between them: the history of a function #[arg(short = 'L', value_name = "START,END:PATH", conflicts_with = "path")] lines: 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, }, /// With -m, change the message of a head; without, name the working /// copy for a build: its tag, or the nearest tag plus distance and /// change, or the bookmark and change, and -dirty Describe { /// The new message #[arg(short, long)] message: Option, /// The head to describe (default: the one the working copy sits on) change: Option, /// Machine-readable output (naming only) #[arg(long, conflicts_with = "message")] json: bool, }, /// 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, /// Take uncommitted changes along; refused when the target has /// other versions of the files they touch #[arg(long, conflicts_with = "discard")] carry: 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, }, /// Prune unreachable objects older than the grace period and repack everything into one pack 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, }, /// Sign in to a hub once, for every repository on it Auth { #[command(subcommand)] cmd: AuthCmd, }, /// Subrepositories: nested repositories pinned to a state Submodule { #[command(subcommand)] cmd: SubmoduleCmd, }, /// Execution receipts: signed records of what a runner ran on a state Receipt { #[command(subcommand)] cmd: ReceiptCmd, }, /// 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, /// Delete this bookmark on the remote (never its default bookmark); /// the local bookmark is kept #[arg(long, value_name = "NAME", conflicts_with_all = ["bookmark", "force"])] delete: 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, auth.helper with --user) 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, with what each changed Log { /// Show only the latest N (snapshots folded together count as one) #[arg(short = 'n', long)] limit: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Go back to the bookmarks and working copy as they were after OP (recorded as a new operation, so it can be undone) Restore { /// The operation: an ID prefix from oxy op log, @ or @- op: String, /// Throw away uncommitted working-copy changes #[arg(long)] discard: bool, }, /// Undo OP alone, keeping every operation after it: the bookmarks it moved go back Revert { /// The operation: an ID prefix from oxy op log op: String, }, } #[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). A state is its /// heads and what they depend on: a change alone may leave out /// earlier changes of the current state, which is refused unless /// --upto or --only says which is meant changes: Vec, /// Everything of the current state up to CHANGE..., as a Git /// branch at a commit would hold #[arg(long, conflicts_with = "only")] upto: bool, /// Just CHANGE... and what they depend on #[arg(long)] only: bool, }, /// Point an existing bookmark at CHANGE... Set { /// The bookmark to move name: String, /// Its new heads. A change alone may leave out changes the /// bookmark now holds that were recorded before it, which is /// refused unless --upto or --only says which is meant changes: Vec, /// Everything the bookmark holds up to CHANGE..., as a Git branch /// moved to a commit would hold #[arg(long, conflicts_with = "only")] upto: bool, /// Just CHANGE... and what they depend on #[arg(long)] only: bool, }, /// Remove a bookmark; its changes stay Delete { /// The bookmark to remove name: String, }, /// List bookmarks with their heads and difference from origin List { /// Also list remotes' bookmarks that have no local bookmark #[arg(long, short)] all: bool, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Make NAME this workspace's bookmark and check it out. Uncommitted work stops it; --carry takes the work along, --discard drops it Switch { /// The bookmark to work on; one only a remote has (NAME or REMOTE/NAME) is created from it name: String, /// Throw away uncommitted working-copy changes #[arg(long)] discard: bool, /// Take uncommitted changes along; refused when NAME has other /// versions of the files they touch #[arg(long, conflicts_with = "discard")] carry: bool, }, } #[derive(Subcommand)] enum AuthCmd { /// Sign in to HUB in the browser (device sign-in) and keep the token for /// every repository there Login { /// The hub, as https://oxygenhub.io or oxygenhub.io (default: this checkout's origin) hub: Option, /// Read a token from standard input instead (for CI and scripts) #[arg(long)] with_token: bool, /// PEM file of extra roots to trust (a hub with a self-signed certificate) #[arg(long)] ca: Option, }, /// List the hubs you are signed in to, checking each token with its hub Status, /// Forget HUB's token and revoke it on the hub Logout { /// The hub (default: this checkout's origin, or the only one) hub: Option, }, } #[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 ReceiptCmd { /// The receipts kept for a state (default: the bookmark heads) List { /// Change, bookmark or tag whose state to look at rev: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// One receipt in full, and whether its signature verifies Show { /// The receipt (a prefix of its hash, as `list` shows it) receipt: String, /// Change, bookmark or tag whose state holds it #[arg(long)] rev: Option, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Write a signed receipt to FILE Export { /// The receipt (a prefix of its hash, as `list` shows it) receipt: String, /// Where to write it file: String, /// Change, bookmark or tag whose state holds it #[arg(long)] rev: Option, }, /// Check a signed receipt in FILE without keeping it Verify { /// A signed receipt, as a runner or `export` writes it file: String, /// Machine-readable output: one JSON document on stdout #[arg(long)] json: bool, }, /// Verify a signed receipt in FILE and keep it with its state Import { /// A signed receipt, as a runner or `export` writes it file: 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: .identity in the current directory) 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 { let repo = Repo::open(&std::env::current_dir()?).context("opening repository")?; match AT_OP.get().and_then(|o| o.as_deref()) { Some(op) => Ok(repo.at_op(op)?), None => Ok(repo), } } /// `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); AT_OP.set(cli.at_op.clone()).ok(); 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, file, depends_on, paths, interactive, hunks, } => { let message = match (message, file) { (Some(m), _) => m, (None, Some(f)) if f.as_os_str() == "-" => { let mut s = String::new(); std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?; s } (None, Some(f)) => std::fs::read_to_string(&f).map_err(|e| { anyhow::anyhow!("reading the message from {}: {e}", f.display()) })?, (None, None) => anyhow::bail!("a commit message is required (-m or -F)"), }; 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, into_working_copy, } => workspace::rebase(&change, &onto, into_working_copy), 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, lines, json, } => workspace::log(limit, verbose, path.as_deref(), lines.as_deref(), json), Cmd::Diff { change, json } => workspace::diff(change.as_deref(), json), Cmd::Describe { message, change, json, } => match message { Some(m) => workspace::describe(&m, change.as_deref()), None if change.is_some() => { anyhow::bail!( "naming works on the working copy; to change a head's message pass -m" ) } None => workspace::describe_name(json), }, 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, carry, } => workspace::checkout(&changes, discard, carry), 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 } => match cmd { OpCmd::Log { limit, json } => workspace::op_log(limit, json), OpCmd::Restore { op, discard } => workspace::op_restore(&op, discard), OpCmd::Revert { op } => workspace::op_revert(&op), }, Cmd::Clone { url, dest, max_blob, sparse, depth, recursive, } => sync::clone(&url, dest, max_blob.as_deref(), &sparse, depth, recursive), 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, upto, only, } => workspace::bookmark_create(&name, &changes, upto, only), BookmarkCmd::Set { name, changes, upto, only, } => workspace::bookmark_set(&name, &changes, upto, only), BookmarkCmd::Delete { name } => workspace::bookmark_delete(&name), BookmarkCmd::List { all, json } => workspace::bookmark_list(all, json), BookmarkCmd::Switch { name, discard, carry, } => workspace::bookmark_switch(&name, discard, carry), }, Cmd::Merge { bookmark } => workspace::merge(&bookmark), Cmd::Auth { cmd } => match cmd { AuthCmd::Login { hub, with_token, ca, } => auth::login(hub.as_deref(), with_token, ca), AuthCmd::Status => auth::status(), AuthCmd::Logout { hub } => auth::logout(hub.as_deref()), }, 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::Receipt { cmd } => match cmd { ReceiptCmd::List { rev, json } => receipt::list(rev.as_deref(), json), ReceiptCmd::Show { receipt: r, rev, json, } => receipt::show(&r, rev.as_deref(), json), ReceiptCmd::Export { receipt: r, file, rev, } => receipt::export(&r, &file, rev.as_deref()), ReceiptCmd::Verify { file, json } => receipt::verify(&file, json), ReceiptCmd::Import { file } => receipt::import(&file), }, 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, delete, json, } => match delete { Some(name) => sync::push_delete(remote.as_deref(), &name, json), None => 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") ); } }