//! Command-line definitions and dispatch. mod format; mod identity; 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, #[command(subcommand)] cmd: Cmd, } #[derive(Subcommand)] enum Cmd { /// Create a repository in PATH (default: current directory) Init { path: Option }, /// 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 #[arg(long, default_value = oxy_core::repo::DEFAULT_PROMPT_FORMAT)] format: String, }, /// 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, }, /// 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, }, /// Show what the working copy changes against its base Diff, /// 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 }, /// 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, /// 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 }, /// 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 }, /// Fetch, then union the remote bookmark into the local one (no merge change) Pull { remote: Option }, /// 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, }, /// 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 }, /// 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, } #[derive(Subcommand)] enum WorkspaceCmd { /// Create a workspace at PATH checked out on CHANGE... (default: the bookmark heads) Add { path: PathBuf, changes: Vec }, List, /// Stop tracking a workspace (by id prefix or path); its files stay Forget { workspace: String }, } #[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, /// 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 TagCmd { /// Tag the state of CHANGE... (default: the bookmark heads) Create { name: String, changes: Vec, #[arg(short, long, default_value = "")] message: String, }, List, } #[derive(Subcommand)] enum RemoteCmd { Add { name: String, url: String }, Remove { name: String }, List, } #[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, /// 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 } => workspace::init(path), Cmd::Status { json } => workspace::status(json), Cmd::Prompt { format } => workspace::prompt(&format), Cmd::Commit { message, depends_on } => workspace::commit(&message, &depends_on), Cmd::Log { limit, verbose } => workspace::log(limit, verbose), Cmd::Diff => workspace::diff(), 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 } => workspace::show(&change), Cmd::Checkout { changes, discard } => workspace::checkout(&changes, discard), Cmd::Resolve => workspace::resolve(), Cmd::Undo => workspace::undo(), Cmd::Gc { now } => workspace::gc(now), Cmd::Checkpoint => workspace::checkpoint(), Cmd::Op { cmd: OpCmd::Log } => workspace::op_log(), 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 => workspace::workspace_list(), 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 => workspace::bookmark_list(), BookmarkCmd::Switch { name, discard } => workspace::bookmark_switch(&name, discard), }, Cmd::Merge { bookmark } => workspace::merge(&bookmark), Cmd::Tag { cmd } => match cmd { TagCmd::Create { name, changes, message } => workspace::tag_create(&name, &changes, &message), TagCmd::List => workspace::tag_list(), }, Cmd::Remote { cmd } => match cmd { RemoteCmd::Add { name, url } => sync::remote_add(&name, &url), RemoteCmd::Remove { name } => sync::remote_remove(&name), RemoteCmd::List => sync::remote_list(), }, Cmd::Fetch { remote } => sync::fetch(remote.as_deref()), Cmd::Pull { remote } => sync::pull(remote.as_deref()), Cmd::Push { remote, force, bookmark } => sync::push(remote.as_deref(), force, bookmark.as_deref()), 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 => identity::policy_show(), 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 } => identity::verify(change.as_deref()), Cmd::Config { key, value, user } => identity::config(&key, &value, user), } }