//! 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, /// 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 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 Amend { #[arg(short, long)] message: Option, }, /// 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, }, /// 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, }, /// 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 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, }, /// Show the configured identity Show, /// Write your identity object to a file, to hand to a policy holder Export { path: Option }, } #[derive(Subcommand)] enum GitCmd { /// Export the current bookmark to a Git repository (created bare if absent) Export { git_dir: PathBuf }, /// 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, }, /// Revoke a key (KeyId prefix of an identity in the policy) Revoke { key: String }, } /// 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 => workspace::status(), Cmd::Prompt { format } => workspace::prompt(&format), Cmd::Commit { message, depends_on } => workspace::commit(&message, &depends_on), Cmd::Log => workspace::log(), Cmd::Diff => workspace::diff(), Cmd::Describe { message, change } => workspace::describe(&message, change.as_deref()), Cmd::Amend { message } => workspace::amend(message.as_deref()), 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::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 } => sync::push(remote.as_deref(), force), 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 } => identity::create(name, email, local), IdentityCmd::Show => identity::show(), IdentityCmd::Export { path } => identity::export(path), }, Cmd::Git { cmd: GitCmd::Export { git_dir } } => sync::git_export(&git_dir), 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 } => identity::policy_grant(&identity_file, pattern.as_deref()), PolicyCmd::Revoke { key } => identity::policy_revoke(&key), }, Cmd::Verify { change } => identity::verify(change.as_deref()), Cmd::Config { key, value, user } => identity::config(&key, &value, user), } }