OXYGENHUB
# 6d3eefbce2f0 oxygen / src / cli / mod.rs
11.8 KiB · Rust · last changed Lazy blobs: clone --max-blob / filter.max-blob, absent-blob registry fetched on first read, objects endpoint (spec 0.6e) 1 month ago source blame history raw
1
//! Command-line definitions and dispatch.
2
3
mod format;
4
mod identity;
5
mod sync;
6
pub mod ui;
7
mod workspace;
8
9
use anyhow::{Context, Result};
10
use clap::{Parser, Subcommand};
11
use oxy_core::repo::Repo;
12
use std::path::PathBuf;
13
14
#[derive(Parser)]
15
#[command(name = "oxy", version, about = "Oxygen: breathing fresh air into version control")]
16
struct Cli {
17
    /// Plain output: no colour, no spinner (also implied by NO_COLOR or a non-terminal)
18
    #[arg(long, global = true)]
19
    plain: bool,
20
    #[command(subcommand)]
21
    cmd: Cmd,
22
}
23
24
#[derive(Subcommand)]
25
enum Cmd {
26
    /// Create a repository in PATH (default: current directory)
27
    Init { path: Option<PathBuf> },
28
    /// Clone a repository from a local path or an http(s) URL
29
    Clone {
30
        url: String,
31
        dest: Option<PathBuf>,
32
        /// Leave blobs larger than this (e.g. 1M) to be fetched on first read
33
        #[arg(long)]
34
        max_blob: Option<String>,
35
        /// Materialize only these paths (repeatable); implies lazy blobs
36
        #[arg(long = "sparse")]
37
        sparse: Vec<String>,
38
        /// Shallow clone: fetch only this many levels of history below the heads
39
        #[arg(long)]
40
        depth: Option<u32>,
41
    },
42
    /// Fetch more history below a shallow clone's boundary
43
    Deepen {
44
        #[arg(default_value_t = 10)]
45
        depth: u32,
46
        #[arg(long, default_value = "origin")]
47
        remote: String,
48
    },
49
    /// Show or set the sparse filter (paths to materialize); re-checks out the bookmark
50
    Sparse { paths: Vec<String> },
51
    /// Show the working copy: changed paths and conflicts
52
    Status,
53
    /// One-line summary for shell prompts; fast and records nothing
54
    Prompt {
55
        /// Placeholders: %b bookmark, %c change id, %h heads, %d dirty (*), %x conflict (!), %A/%D ahead/behind (+n/-n), %a/%B raw counts
56
        #[arg(long, default_value = oxy_core::repo::DEFAULT_PROMPT_FORMAT)]
57
        format: String,
58
    },
59
    /// Record the working copy as a change on the current bookmark
60
    Commit {
61
        #[arg(short, long)]
62
        message: String,
63
        /// Extra explicit dependencies (ChangeId prefix, #hash prefix)
64
        #[arg(long = "depends-on")]
65
        depends_on: Vec<String>,
66
    },
67
    /// List changes reachable from the bookmark and the working copy
68
    Log,
69
    /// Show what the working copy changes against its base
70
    Diff,
71
    /// Change the message of a head (default: the head the working copy sits on)
72
    Describe {
73
        #[arg(short, long)]
74
        message: String,
75
        change: Option<String>,
76
    },
77
    /// Fold the working copy into the head it sits on
78
    Amend {
79
        #[arg(short, long)]
80
        message: Option<String>,
81
    },
82
    /// Drop a head from the bookmark; its content stays on disk as uncommitted work
83
    Abandon { change: String },
84
    /// Show one change (ChangeId prefix, #hash prefix, or @)
85
    Show { change: String },
86
    /// Move the working copy to the state formed by CHANGE... (default: the bookmark heads)
87
    Checkout {
88
        changes: Vec<String>,
89
        /// Throw away uncommitted working-copy changes
90
        #[arg(long)]
91
        discard: bool,
92
    },
93
    /// Report which conflicts of the working copy's base are resolved by the current files
94
    Resolve,
95
    /// Undo the last operation
96
    Undo,
97
    /// Write a checkpoint of the current bookmark's state now
98
    Checkpoint,
99
    /// Pack loose objects and prune unreachable ones older than the grace period
100
    Gc {
101
        /// Ignore the grace period (core.gc-grace) and prune immediately
102
        #[arg(long)]
103
        now: bool,
104
    },
105
    /// List operations
106
    #[command(name = "op")]
107
    Op {
108
        #[command(subcommand)]
109
        cmd: OpCmd,
110
    },
111
    /// Take the advisory local lock on a lockable file, making it writable
112
    Lock { path: String },
113
    /// Release the local lock and make the file read-only again
114
    Unlock { path: String },
115
    /// Workspaces: extra checkout directories sharing this repository
116
    Workspace {
117
        #[command(subcommand)]
118
        cmd: WorkspaceCmd,
119
    },
120
    /// Tags: immutable named states
121
    Tag {
122
        #[command(subcommand)]
123
        cmd: TagCmd,
124
    },
125
    /// Manage remotes
126
    Remote {
127
        #[command(subcommand)]
128
        cmd: RemoteCmd,
129
    },
130
    /// Fetch every bookmark of a remote into its tracking references
131
    Fetch { remote: Option<String> },
132
    /// Fetch, then union the remote bookmark into the local one (no merge change)
133
    Pull { remote: Option<String> },
134
    /// Push the current bookmark to a remote
135
    Push {
136
        remote: Option<String>,
137
        /// Allow dropping heads the remote has
138
        #[arg(long)]
139
        force: bool,
140
    },
141
    /// Serve repositories under ROOT over HTTPS (with --tls-cert/--tls-key) or plain HTTP
142
    Serve {
143
        /// Directory with one repository per subdirectory
144
        #[arg(long, default_value = ".")]
145
        root: PathBuf,
146
        #[arg(long, default_value = "127.0.0.1:8890")]
147
        bind: String,
148
        /// Bearer token allowed to push (repeatable); none means open
149
        #[arg(long = "token")]
150
        tokens: Vec<String>,
151
        /// PEM certificate chain; enables HTTPS together with --tls-key
152
        #[arg(long, requires = "tls_key")]
153
        tls_cert: Option<PathBuf>,
154
        /// PEM private key
155
        #[arg(long, requires = "tls_cert")]
156
        tls_key: Option<PathBuf>,
157
    },
158
    /// Manage the signing identity
159
    Identity {
160
        #[command(subcommand)]
161
        cmd: IdentityCmd,
162
    },
163
    /// Verify the signatures on a change (default: every change in the log)
164
    Verify { change: Option<String> },
165
    /// Git interoperability
166
    Git {
167
        #[command(subcommand)]
168
        cmd: GitCmd,
169
    },
170
    /// Manage the trust policy (who may update which bookmarks)
171
    Policy {
172
        #[command(subcommand)]
173
        cmd: PolicyCmd,
174
    },
175
    /// Set a configuration key (author.*, core.sign, core.op-retention, core.gc-grace, core.checkpoint-interval, filter.max-blob, filter.paths, workspace.bookmark, remote.<name>.url|token|ca)
176
    Config {
177
        key: String,
178
        value: String,
179
        /// Write to the user file (~/.config/oxy/config.toml) instead of this repository
180
        #[arg(long)]
181
        user: bool,
182
    },
183
}
184
185
#[derive(Subcommand)]
186
enum OpCmd {
187
    Log,
188
}
189
190
#[derive(Subcommand)]
191
enum WorkspaceCmd {
192
    /// Create a workspace at PATH checked out on CHANGE... (default: the bookmark heads)
193
    Add { path: PathBuf, changes: Vec<String> },
194
    List,
195
    /// Stop tracking a workspace (by id prefix or path); its files stay
196
    Forget { workspace: String },
197
}
198
199
#[derive(Subcommand)]
200
enum TagCmd {
201
    /// Tag the state of CHANGE... (default: the bookmark heads)
202
    Create {
203
        name: String,
204
        changes: Vec<String>,
205
        #[arg(short, long, default_value = "")]
206
        message: String,
207
    },
208
    List,
209
}
210
211
#[derive(Subcommand)]
212
enum RemoteCmd {
213
    Add { name: String, url: String },
214
    Remove { name: String },
215
    List,
216
}
217
218
#[derive(Subcommand)]
219
enum IdentityCmd {
220
    /// Generate a composite Ed25519 + ML-DSA-65 key pair and make it your author identity
221
    Create {
222
        #[arg(long)]
223
        name: Option<String>,
224
        #[arg(long)]
225
        email: Option<String>,
226
        /// Record the author in this repository only, not in the user config
227
        #[arg(long)]
228
        local: bool,
229
    },
230
    /// Show the configured identity
231
    Show,
232
    /// Write your identity object to a file, to hand to a policy holder
233
    Export { path: Option<PathBuf> },
234
}
235
236
#[derive(Subcommand)]
237
enum GitCmd {
238
    /// Export the current bookmark to a Git repository (created bare if absent)
239
    Export { git_dir: PathBuf },
240
    /// Import a Git branch into a new Oxygen repository at DEST
241
    Import {
242
        git_dir: PathBuf,
243
        dest: PathBuf,
244
        #[arg(long, default_value = "main")]
245
        branch: String,
246
    },
247
}
248
249
#[derive(Subcommand)]
250
enum PolicyCmd {
251
    /// Create the initial policy with your identity as the only root
252
    Init,
253
    /// Show the current policy
254
    Show,
255
    /// Allow an identity (from `oxy identity export`) to update bookmarks
256
    Grant {
257
        identity_file: PathBuf,
258
        /// Reference pattern the grant applies to (default: every bookmark)
259
        #[arg(long)]
260
        pattern: Option<String>,
261
    },
262
    /// Revoke a key (KeyId prefix of an identity in the policy)
263
    Revoke { key: String },
264
}
265
266
/// Open the repository containing the current directory.
267
fn open() -> Result<Repo> {
268
    Repo::open(&std::env::current_dir()?).context("opening repository")
269
}
270
271
/// Parse arguments and run; returns the process exit code.
272
pub fn run() -> Result<i32> {
273
    let cli = Cli::parse();
274
    ui::init(cli.plain);
275
    match cli.cmd {
276
        Cmd::Init { path } => workspace::init(path),
277
        Cmd::Status => workspace::status(),
278
        Cmd::Prompt { format } => workspace::prompt(&format),
279
        Cmd::Commit { message, depends_on } => workspace::commit(&message, &depends_on),
280
        Cmd::Log => workspace::log(),
281
        Cmd::Diff => workspace::diff(),
282
        Cmd::Describe { message, change } => workspace::describe(&message, change.as_deref()),
283
        Cmd::Amend { message } => workspace::amend(message.as_deref()),
284
        Cmd::Abandon { change } => workspace::abandon(&change),
285
        Cmd::Show { change } => workspace::show(&change),
286
        Cmd::Checkout { changes, discard } => workspace::checkout(&changes, discard),
287
        Cmd::Resolve => workspace::resolve(),
288
        Cmd::Undo => workspace::undo(),
289
        Cmd::Gc { now } => workspace::gc(now),
290
        Cmd::Checkpoint => workspace::checkpoint(),
291
        Cmd::Op { cmd: OpCmd::Log } => workspace::op_log(),
292
        Cmd::Clone { url, dest, max_blob, sparse, depth } => sync::clone(&url, dest, max_blob.as_deref(), &sparse, depth),
293
        Cmd::Deepen { depth, remote } => sync::deepen(&remote, depth),
294
        Cmd::Sparse { paths } => workspace::sparse(&paths),
295
        Cmd::Lock { path } => workspace::lock(&path),
296
        Cmd::Unlock { path } => workspace::unlock(&path),
297
        Cmd::Workspace { cmd } => match cmd {
298
            WorkspaceCmd::Add { path, changes } => workspace::workspace_add(&path, &changes),
299
            WorkspaceCmd::List => workspace::workspace_list(),
300
            WorkspaceCmd::Forget { workspace } => workspace::workspace_forget(&workspace),
301
        },
302
        Cmd::Tag { cmd } => match cmd {
303
            TagCmd::Create { name, changes, message } => workspace::tag_create(&name, &changes, &message),
304
            TagCmd::List => workspace::tag_list(),
305
        },
306
        Cmd::Remote { cmd } => match cmd {
307
            RemoteCmd::Add { name, url } => sync::remote_add(&name, &url),
308
            RemoteCmd::Remove { name } => sync::remote_remove(&name),
309
            RemoteCmd::List => sync::remote_list(),
310
        },
311
        Cmd::Fetch { remote } => sync::fetch(remote.as_deref()),
312
        Cmd::Pull { remote } => sync::pull(remote.as_deref()),
313
        Cmd::Push { remote, force } => sync::push(remote.as_deref(), force),
314
        Cmd::Serve { root, bind, tokens, tls_cert, tls_key } => sync::serve(root, &bind, tokens, tls_cert.zip(tls_key)),
315
        Cmd::Identity { cmd } => match cmd {
316
            IdentityCmd::Create { name, email, local } => identity::create(name, email, local),
317
            IdentityCmd::Show => identity::show(),
318
            IdentityCmd::Export { path } => identity::export(path),
319
        },
320
        Cmd::Git { cmd: GitCmd::Export { git_dir } } => sync::git_export(&git_dir),
321
        Cmd::Git { cmd: GitCmd::Import { git_dir, dest, branch } } => sync::git_import(&git_dir, &dest, &branch),
322
        Cmd::Policy { cmd } => match cmd {
323
            PolicyCmd::Init => identity::policy_init(),
324
            PolicyCmd::Show => identity::policy_show(),
325
            PolicyCmd::Grant { identity_file, pattern } => identity::policy_grant(&identity_file, pattern.as_deref()),
326
            PolicyCmd::Revoke { key } => identity::policy_revoke(&key),
327
        },
328
        Cmd::Verify { change } => identity::verify(change.as_deref()),
329
        Cmd::Config { key, value, user } => identity::config(&key, &value, user),
330
    }
331
}