//! Command-line definitions and dispatch. mod format; mod identity; mod json; 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 { 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 { url: String, 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 { #[arg(default_value_t = 10)] depth: u32, #[arg(long, default_value = "origin")] remote: String, }, /// Show or set the sparse filter (paths to materialize); re-checks out the bookmark Sparse { 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 { #[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 { change: String, #[arg(long)] into: String, }, /// Re-express CHANGE against another base state Rebase { 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 { path: String, #[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, #[arg(long)] json: bool, }, /// Show what the working copy changes against its base Diff { /// A change instead of the working copy change: Option, #[arg(long)] json: bool, }, /// Print a file as it is in a state (default: the working copy's base) Cat { path: String, /// A change, bookmark, or tag #[arg(short = 'r', long)] change: Option, }, /// Print the repository root, workspace, and bookmark Root { #[arg(long)] json: bool, }, /// Print the version of oxy, its repository format, and protocol Version { #[arg(long)] json: bool, }, /// Restore paths to the working copy's base, or undo chosen hunks Discard { 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 { #[arg(long)] json: bool, }, /// Change the message of a head (default: the head the working copy sits on) Describe { #[arg(short, long)] message: String, change: Option, }, /// Fold the working copy into the head it sits on, or into CHANGE with --into Amend { #[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 { change: String, paths: Vec }, /// Drop a head from the bookmark; its content stays on disk as uncommitted work Abandon { change: String }, /// Show one change (ChangeId prefix, #hash prefix, or @) Show { change: String, #[arg(long)] json: bool, }, /// Move the working copy to the state formed by CHANGE... (default: the bookmark heads) Checkout { 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 { #[arg(long)] json: bool, }, /// Undo the last operation Undo, /// Write a checkpoint of the current bookmark's state now Checkpoint, /// 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 { path: String }, /// Release the local lock and make the file read-only again Unlock { 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 { 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: Option, #[arg(long)] json: bool, }, /// Fetch, then union the remote bookmark into the local one (no merge change) Pull { remote: Option, #[arg(long)] json: bool, }, /// Push the current bookmark to a remote Push { 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, #[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, #[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, }, /// Verify the signatures on a change (default: every change in the log) Verify { change: Option, #[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 { key: String, value: String, /// Write to the user file (~/.config/oxy/config.toml) instead of this repository #[arg(long)] user: bool, }, } #[derive(Subcommand)] enum OpCmd { Log { #[arg(long)] json: bool, }, } #[derive(Subcommand)] enum WorkspaceCmd { /// Create a workspace at PATH checked out on CHANGE... (default: the bookmark heads) Add { path: PathBuf, changes: Vec }, List { #[arg(long)] json: bool, }, /// Stop tracking a workspace (by id prefix or path); its files stay Forget { 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: Vec }, /// Mark CHANGE... (or the state under test) as bad Bad { 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 { #[arg(required = true, trailing_var_arg = true)] command: Vec, }, /// Where the search stands Status { #[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: String, changes: Vec }, /// Point an existing bookmark at CHANGE... Set { name: String, changes: Vec }, /// Remove a bookmark; its changes stay Delete { name: String }, /// List bookmarks with their heads and difference from origin List { #[arg(long)] json: bool, }, /// Make NAME this workspace's bookmark and check it out Switch { 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: String, 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, paths: Vec, }, /// List subrepos with their pins and on-disk state Status { #[arg(long)] json: bool, }, /// Unregister PATH; the directory stays, the node goes with the next commit Remove { path: String }, } #[derive(Subcommand)] enum TagCmd { /// Tag the state of CHANGE... (default: the bookmark heads) Create { name: String, changes: Vec, #[arg(short, long, default_value = "")] message: String, }, List { #[arg(long)] json: bool, }, } #[derive(Subcommand)] enum RemoteCmd { Add { name: String, url: String, }, Remove { name: String, }, List { #[arg(long)] json: bool, }, } #[derive(Subcommand)] enum IdentityCmd { /// Generate a composite Ed25519 + ML-DSA-65 key pair and make it your author identity Create { #[arg(long)] name: Option, #[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 { path: Option }, /// 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 { #[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, #[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 { identity_file: PathBuf }, /// Remove a delegate by KeyId prefix Remove { key: String }, /// List delegates List, } #[derive(Subcommand)] enum GitCmd { /// Export the current bookmark to a Git repository (created bare if absent) Export { 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 { url: String, #[arg(long, default_value = "main")] branch: String, }, /// Push the current bookmark to a branch of a Git remote (fetch first) Push { url: String, #[arg(long, default_value = "main")] branch: String, }, /// Import a Git branch into a new Oxygen repository at DEST Import { git_dir: PathBuf, dest: PathBuf, #[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 { #[arg(long)] json: bool, }, /// Allow an identity (from `oxy identity export`) to update bookmarks Grant { 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 { 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 { 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 { identity_file: PathBuf, #[arg(long)] out: Option, }, /// Remove a root key by KeyId prefix Remove { key: String, #[arg(long)] out: Option, }, /// Require this many root signatures on policy updates Threshold { n: u32, #[arg(long)] out: Option, }, } /// Open the repository containing the current directory. fn open() -> Result { Repo::open(&std::env::current_dir()?).context("opening repository") } /// 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::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::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::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::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::Verify { change, json } => identity::verify(change.as_deref(), json), Cmd::Config { key, value, user } => identity::config(&key, &value, user), } }