OXYGENHUB
# bc489b6c7f69 oxygen / src / cli / mod.rs
27.9 KiB · Rust · last changed Git import verified commit by commit; two dependency bugs fixed 1 month ago source blame history raw
1
//! Command-line definitions and dispatch.
2
3
mod format;
4
mod identity;
5
mod json;
6
mod proposal;
7
mod sync;
8
pub mod ui;
9
mod workspace;
10
11
use anyhow::{Context, Result};
12
use clap::{Parser, Subcommand};
13
use oxy_core::repo::Repo;
14
use std::path::PathBuf;
15
16
#[derive(Parser)]
17
#[command(
18
    name = "oxy",
19
    version,
20
    about = "Oxygen: breathing fresh air into version control"
21
)]
22
struct Cli {
23
    /// Plain output: no colour, no spinner (also implied by NO_COLOR or a non-terminal)
24
    #[arg(long, global = true)]
25
    plain: bool,
26
    #[command(subcommand)]
27
    cmd: Cmd,
28
}
29
30
#[derive(Subcommand)]
31
enum Cmd {
32
    /// Create a repository in PATH (default: current directory)
33
    Init {
34
        path: Option<PathBuf>,
35
        /// No working copy: an object store and references only, for hosting
36
        #[arg(long)]
37
        bare: bool,
38
    },
39
    /// Clone a repository from a local path or an http(s) URL
40
    Clone {
41
        url: String,
42
        dest: Option<PathBuf>,
43
        /// Leave blobs larger than this (e.g. 1M) to be fetched on first read
44
        #[arg(long)]
45
        max_blob: Option<String>,
46
        /// Materialize only these paths (repeatable); implies lazy blobs
47
        #[arg(long = "sparse")]
48
        sparse: Vec<String>,
49
        /// Shallow clone: fetch only this many levels of history below the heads
50
        #[arg(long)]
51
        depth: Option<u32>,
52
    },
53
    /// Fetch more history below a shallow clone's boundary
54
    Deepen {
55
        #[arg(default_value_t = 10)]
56
        depth: u32,
57
        #[arg(long, default_value = "origin")]
58
        remote: String,
59
    },
60
    /// Show or set the sparse filter (paths to materialize); re-checks out the bookmark
61
    Sparse { paths: Vec<String> },
62
    /// Show the working copy: changed paths and conflicts
63
    Status {
64
        /// Machine-readable output: paths, and every conflict with its kind and contributing changes
65
        #[arg(long)]
66
        json: bool,
67
    },
68
    /// One-line summary for shell prompts; fast and records nothing
69
    Prompt {
70
        /// 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
71
        #[arg(long, default_value = oxy_core::repo::DEFAULT_PROMPT_FORMAT)]
72
        format: String,
73
        /// Glyph format (%i %b %c %s%U%O) instead of the plain default
74
        #[arg(long)]
75
        glyphs: bool,
76
        /// Colour by state (cyan clean, yellow dirty, red conflicted): raw escapes, or guarded for a bash or zsh PS1
77
        #[arg(long, value_parser = ["raw", "bash", "zsh"])]
78
        color: Option<String>,
79
    },
80
    /// Record the working copy as a change on the current bookmark
81
    Commit {
82
        #[arg(short, long)]
83
        message: String,
84
        /// Extra explicit dependencies (ChangeId prefix, #hash prefix)
85
        #[arg(long = "depends-on")]
86
        depends_on: Vec<String>,
87
        /// Commit only these paths; the rest stays as uncommitted work
88
        paths: Vec<String>,
89
        /// Choose hunks one by one
90
        #[arg(short, long)]
91
        interactive: bool,
92
        /// Commit these hunks (as `oxy hunks` numbers them), comma separated
93
        #[arg(long, value_delimiter = ',')]
94
        hunks: Vec<usize>,
95
    },
96
    /// Fold CHANGE into one of its dependencies (keeps that change's id)
97
    Squash {
98
        change: String,
99
        #[arg(long)]
100
        into: String,
101
    },
102
    /// Re-express CHANGE against another base state
103
    Rebase {
104
        change: String,
105
        /// The changes, bookmark or tag whose state becomes the new base
106
        #[arg(long, required = true, num_args = 1..)]
107
        onto: Vec<String>,
108
    },
109
    /// Show which change introduced each line of a file
110
    Blame {
111
        path: String,
112
        #[arg(long)]
113
        json: bool,
114
    },
115
    /// Find the change that introduced a regression
116
    Bisect {
117
        #[command(subcommand)]
118
        cmd: BisectCmd,
119
    },
120
    /// List changes reachable from the bookmark and the working copy
121
    Log {
122
        /// Show at most this many changes
123
        #[arg(short = 'n', long)]
124
        limit: Option<usize>,
125
        /// Show full messages, dependencies, and signatures
126
        #[arg(short, long)]
127
        verbose: bool,
128
        /// Only changes touching this path (renames followed)
129
        #[arg(long)]
130
        path: Option<String>,
131
        #[arg(long)]
132
        json: bool,
133
    },
134
    /// Show what the working copy changes against its base
135
    Diff {
136
        /// A change instead of the working copy
137
        change: Option<String>,
138
        #[arg(long)]
139
        json: bool,
140
    },
141
    /// Print a file as it is in a state (default: the working copy's base)
142
    Cat {
143
        path: String,
144
        /// A change, bookmark, or tag
145
        #[arg(short = 'r', long)]
146
        change: Option<String>,
147
    },
148
    /// Print the repository root, workspace, and bookmark
149
    Root {
150
        #[arg(long)]
151
        json: bool,
152
    },
153
    /// Print the version of oxy, its repository format, and protocol
154
    Version {
155
        #[arg(long)]
156
        json: bool,
157
    },
158
    /// Restore paths to the working copy's base, or undo chosen hunks
159
    Discard {
160
        paths: Vec<String>,
161
        /// Hunk indices as `oxy hunks` numbers them, comma separated
162
        #[arg(long, value_delimiter = ',')]
163
        hunks: Vec<usize>,
164
    },
165
    /// List the working copy's hunks, numbered as commit --hunks expects
166
    Hunks {
167
        #[arg(long)]
168
        json: bool,
169
    },
170
    /// Change the message of a head (default: the head the working copy sits on)
171
    Describe {
172
        #[arg(short, long)]
173
        message: String,
174
        change: Option<String>,
175
    },
176
    /// Fold the working copy into the head it sits on, or into CHANGE with --into
177
    Amend {
178
        #[arg(short, long)]
179
        message: Option<String>,
180
        /// Amend an older change; the edits must touch only lines it already knows
181
        #[arg(long)]
182
        into: Option<String>,
183
    },
184
    /// Split a head: the named paths become a separate first change
185
    Split { change: String, paths: Vec<String> },
186
    /// Drop a head from the bookmark; its content stays on disk as uncommitted work
187
    Abandon { change: String },
188
    /// Show one change (ChangeId prefix, #hash prefix, or @)
189
    Show {
190
        change: String,
191
        #[arg(long)]
192
        json: bool,
193
    },
194
    /// Move the working copy to the state formed by CHANGE... (default: the bookmark heads)
195
    Checkout {
196
        changes: Vec<String>,
197
        /// Throw away uncommitted working-copy changes
198
        #[arg(long)]
199
        discard: bool,
200
    },
201
    /// Report which conflicts of the working copy's base are resolved by the current files
202
    Resolve {
203
        #[arg(long)]
204
        json: bool,
205
    },
206
    /// Undo the last operation
207
    Undo,
208
    /// Write a checkpoint of the current bookmark's state now
209
    Checkpoint,
210
    /// Check the identity, configuration and remotes, and say what to do about any problem
211
    Doctor {
212
        /// Do not contact the remotes
213
        #[arg(long)]
214
        offline: bool,
215
        #[arg(long)]
216
        json: bool,
217
    },
218
    /// Check the repository's integrity: objects, operation log, references, signatures, checkpoints
219
    Fsck {
220
        #[arg(long)]
221
        json: bool,
222
    },
223
    /// Pack loose objects and prune unreachable ones older than the grace period
224
    Gc {
225
        /// Ignore the grace period (core.gc-grace) and prune immediately
226
        #[arg(long)]
227
        now: bool,
228
    },
229
    /// List operations
230
    #[command(name = "op")]
231
    Op {
232
        #[command(subcommand)]
233
        cmd: OpCmd,
234
    },
235
    /// Take the advisory local lock on a lockable file, making it writable
236
    Lock { path: String },
237
    /// Release the local lock and make the file read-only again
238
    Unlock { path: String },
239
    /// Workspaces: extra checkout directories sharing this repository
240
    Workspace {
241
        #[command(subcommand)]
242
        cmd: WorkspaceCmd,
243
    },
244
    /// Bookmarks: named states; each workspace works on one
245
    Bookmark {
246
        #[command(subcommand)]
247
        cmd: BookmarkCmd,
248
    },
249
    /// Union another bookmark's heads into the current one (no merge change)
250
    Merge { bookmark: String },
251
    /// Subrepositories: nested repositories pinned to a state
252
    Submodule {
253
        #[command(subcommand)]
254
        cmd: SubmoduleCmd,
255
    },
256
    /// Tags: immutable named states
257
    Tag {
258
        #[command(subcommand)]
259
        cmd: TagCmd,
260
    },
261
    /// Manage remotes
262
    Remote {
263
        #[command(subcommand)]
264
        cmd: RemoteCmd,
265
    },
266
    /// Fetch every bookmark of a remote into its tracking references
267
    Fetch {
268
        remote: Option<String>,
269
        #[arg(long)]
270
        json: bool,
271
    },
272
    /// Fetch, then union the remote bookmark into the local one (no merge change)
273
    Pull {
274
        remote: Option<String>,
275
        #[arg(long)]
276
        json: bool,
277
    },
278
    /// Push the current bookmark to a remote
279
    Push {
280
        remote: Option<String>,
281
        /// Allow dropping heads the remote has
282
        #[arg(long)]
283
        force: bool,
284
        /// Push this bookmark instead of the workspace's current one
285
        #[arg(short = 'b', long)]
286
        bookmark: Option<String>,
287
        #[arg(long)]
288
        json: bool,
289
    },
290
    /// Serve repositories under ROOT over HTTPS (with --tls-cert/--tls-key) or plain HTTP
291
    Serve {
292
        /// Directory with one repository per subdirectory
293
        #[arg(long, default_value = ".")]
294
        root: PathBuf,
295
        #[arg(long, default_value = "127.0.0.1:8890")]
296
        bind: String,
297
        /// Bearer token allowed to push (repeatable); none means open
298
        #[arg(long = "token")]
299
        tokens: Vec<String>,
300
        /// PEM certificate chain; enables HTTPS together with --tls-key
301
        #[arg(long, requires = "tls_key")]
302
        tls_cert: Option<PathBuf>,
303
        /// PEM private key
304
        #[arg(long, requires = "tls_cert")]
305
        tls_key: Option<PathBuf>,
306
    },
307
    /// Manage the signing identity
308
    Identity {
309
        #[command(subcommand)]
310
        cmd: IdentityCmd,
311
    },
312
    /// Seal changes with your key: named ones, or every change on the current bookmark you can authenticate but have not
313
    Seal {
314
        changes: Vec<String>,
315
        /// Seal every change of a hub proposal and push the seal (your approval)
316
        #[arg(long, conflicts_with = "changes")]
317
        proposal: Option<String>,
318
        #[arg(long)]
319
        json: bool,
320
    },
321
    /// Hub proposals: show one, or land it from this client
322
    Proposal {
323
        #[command(subcommand)]
324
        cmd: ProposalCmd,
325
    },
326
    /// Verify the signatures on a change (default: every change in the log)
327
    Verify {
328
        change: Option<String>,
329
        #[arg(long)]
330
        json: bool,
331
    },
332
    /// Git interoperability
333
    Git {
334
        #[command(subcommand)]
335
        cmd: GitCmd,
336
    },
337
    /// Manage the trust policy (who may update which bookmarks)
338
    Policy {
339
        #[command(subcommand)]
340
        cmd: PolicyCmd,
341
    },
342
    /// 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)
343
    Config {
344
        key: String,
345
        value: String,
346
        /// Write to the user file (~/.config/oxy/config.toml) instead of this repository
347
        #[arg(long)]
348
        user: bool,
349
    },
350
}
351
352
#[derive(Subcommand)]
353
enum OpCmd {
354
    Log {
355
        #[arg(long)]
356
        json: bool,
357
    },
358
}
359
360
#[derive(Subcommand)]
361
enum WorkspaceCmd {
362
    /// Create a workspace at PATH checked out on CHANGE... (default: the bookmark heads)
363
    Add { path: PathBuf, changes: Vec<String> },
364
    List {
365
        #[arg(long)]
366
        json: bool,
367
    },
368
    /// Stop tracking a workspace (by id prefix or path); its files stay
369
    Forget { workspace: String },
370
}
371
372
#[derive(Subcommand)]
373
enum BisectCmd {
374
    /// Begin; remembers where the working copy was
375
    Start,
376
    /// Mark CHANGE... (or the state under test) as good
377
    Good { changes: Vec<String> },
378
    /// Mark CHANGE... (or the state under test) as bad
379
    Bad { changes: Vec<String> },
380
    /// The state under test cannot be tested; try another
381
    Skip,
382
    /// Run a command for each step: exit 0 good, 125 skip, else bad
383
    Run {
384
        #[arg(required = true, trailing_var_arg = true)]
385
        command: Vec<String>,
386
    },
387
    /// Where the search stands
388
    Status {
389
        #[arg(long)]
390
        json: bool,
391
    },
392
    /// Restore the working copy and forget the search
393
    Reset,
394
}
395
396
#[derive(Subcommand)]
397
enum BookmarkCmd {
398
    /// Create a bookmark at CHANGE... (default: the working copy's base)
399
    Create { name: String, changes: Vec<String> },
400
    /// Point an existing bookmark at CHANGE...
401
    Set { name: String, changes: Vec<String> },
402
    /// Remove a bookmark; its changes stay
403
    Delete { name: String },
404
    /// List bookmarks with their heads and difference from origin
405
    List {
406
        #[arg(long)]
407
        json: bool,
408
    },
409
    /// Make NAME this workspace's bookmark and check it out
410
    Switch {
411
        name: String,
412
        /// Throw away uncommitted working-copy changes
413
        #[arg(long)]
414
        discard: bool,
415
    },
416
}
417
418
#[derive(Subcommand)]
419
enum SubmoduleCmd {
420
    /// Clone URL (Oxygen or Git) into PATH and register it in .oxymodules
421
    Add {
422
        url: String,
423
        path: String,
424
        /// Bookmark (Oxygen) or branch (Git) to check out and track
425
        #[arg(long)]
426
        bookmark: Option<String>,
427
    },
428
    /// Bring nested repositories to their pins
429
    Update {
430
        /// Clone nested repositories that are absent
431
        #[arg(long)]
432
        init: bool,
433
        paths: Vec<String>,
434
    },
435
    /// List subrepos with their pins and on-disk state
436
    Status {
437
        #[arg(long)]
438
        json: bool,
439
    },
440
    /// Unregister PATH; the directory stays, the node goes with the next commit
441
    Remove { path: String },
442
}
443
444
#[derive(Subcommand)]
445
enum TagCmd {
446
    /// Tag the state of CHANGE... (default: the bookmark heads)
447
    Create {
448
        name: String,
449
        changes: Vec<String>,
450
        #[arg(short, long, default_value = "")]
451
        message: String,
452
    },
453
    List {
454
        #[arg(long)]
455
        json: bool,
456
    },
457
}
458
459
#[derive(Subcommand)]
460
enum RemoteCmd {
461
    Add {
462
        name: String,
463
        url: String,
464
    },
465
    Remove {
466
        name: String,
467
    },
468
    List {
469
        #[arg(long)]
470
        json: bool,
471
    },
472
}
473
474
#[derive(Subcommand)]
475
enum ProposalCmd {
476
    /// Show a proposal by its hub URL
477
    Show {
478
        url: String,
479
        #[arg(long)]
480
        json: bool,
481
    },
482
    /// Fetch the proposal's changes, set the target bookmark to the projected state, and push with your key
483
    Land {
484
        url: String,
485
        /// Land even when the hub reports blockers
486
        #[arg(long)]
487
        force: bool,
488
    },
489
}
490
491
#[derive(Subcommand)]
492
enum IdentityCmd {
493
    /// Generate a composite Ed25519 + ML-DSA-65 key pair and make it your author identity
494
    Create {
495
        #[arg(long)]
496
        name: Option<String>,
497
        #[arg(long)]
498
        email: Option<String>,
499
        /// Record the author in this repository only, not in the user config
500
        #[arg(long)]
501
        local: bool,
502
        /// Expire the identity this many days from now
503
        #[arg(long)]
504
        expires: Option<u64>,
505
    },
506
    /// Show the configured identity
507
    Show,
508
    /// Write your identity object to a file, to hand to a policy holder
509
    Export { path: Option<PathBuf> },
510
    /// Sign a host's challenge (hex, at least 16 bytes) to prove you hold your key; prints the signature as hex
511
    SignChallenge {
512
        challenge: String,
513
        #[arg(long)]
514
        json: bool,
515
    },
516
    /// Move to a fresh key pair; the policy follows if it names your key
517
    Rotate,
518
    /// Re-issue your identity with a new expiry (in days from now; 0 = never)
519
    Renew {
520
        #[arg(long)]
521
        expires: u64,
522
    },
523
    /// Revoke a key: yours by default, or any key if you hold a policy root
524
    Revoke {
525
        /// KeyId prefix (default: your own key)
526
        key: Option<String>,
527
        #[arg(long, default_value = "")]
528
        reason: String,
529
    },
530
    /// Keys allowed to sign or seal on your behalf
531
    Delegate {
532
        #[command(subcommand)]
533
        cmd: DelegateCmd,
534
    },
535
}
536
537
#[derive(Subcommand)]
538
enum DelegateCmd {
539
    /// Add a delegate from an exported identity file
540
    Add { identity_file: PathBuf },
541
    /// Remove a delegate by KeyId prefix
542
    Remove { key: String },
543
    /// List delegates
544
    List,
545
}
546
547
#[derive(Subcommand)]
548
enum GitCmd {
549
    /// Export the current bookmark to a Git repository (created bare if absent)
550
    Export {
551
        git_dir: PathBuf,
552
        /// Git branch to write (default: the bookmark name)
553
        #[arg(long)]
554
        branch: Option<String>,
555
    },
556
    /// Fetch a branch from a Git remote (path or URL) into the current bookmark
557
    Fetch {
558
        url: String,
559
        #[arg(long, default_value = "main")]
560
        branch: String,
561
    },
562
    /// Push the current bookmark to a branch of a Git remote (fetch first)
563
    Push {
564
        url: String,
565
        #[arg(long, default_value = "main")]
566
        branch: String,
567
    },
568
    /// Check this repository's import of a Git branch: every commit's tree must be reproduced exactly
569
    Verify {
570
        /// The Git repository the import came from
571
        git_dir: PathBuf,
572
        /// Branch that was imported
573
        #[arg(long, default_value = "main")]
574
        branch: String,
575
    },
576
    /// Import a Git branch into a new Oxygen repository at DEST
577
    Import {
578
        git_dir: PathBuf,
579
        dest: PathBuf,
580
        #[arg(long, default_value = "main")]
581
        branch: String,
582
    },
583
}
584
585
#[derive(Subcommand)]
586
enum PolicyCmd {
587
    /// Create the initial policy with your identity as the only root
588
    Init,
589
    /// Show the current policy
590
    Show {
591
        #[arg(long)]
592
        json: bool,
593
    },
594
    /// Allow an identity (from `oxy identity export`) to update bookmarks
595
    Grant {
596
        identity_file: PathBuf,
597
        /// Reference pattern the grant applies to (default: every bookmark)
598
        #[arg(long)]
599
        pattern: Option<String>,
600
        /// Where to write the proposal when more root signatures are needed
601
        #[arg(long)]
602
        out: Option<PathBuf>,
603
    },
604
    /// Revoke a key (KeyId prefix of an identity in the policy)
605
    Revoke {
606
        key: String,
607
        /// Where to write the proposal when more root signatures are needed
608
        #[arg(long)]
609
        out: Option<PathBuf>,
610
    },
611
    /// Manage root keys and the signature threshold for policy updates
612
    Root {
613
        #[command(subcommand)]
614
        cmd: RootCmd,
615
    },
616
    /// Add your root signature to a proposal file; applies it once the threshold is met
617
    Sign {
618
        proposal: PathBuf,
619
        /// Where to write the proposal if it still needs signatures (default: in place)
620
        #[arg(long)]
621
        out: Option<PathBuf>,
622
    },
623
}
624
625
#[derive(Subcommand)]
626
enum RootCmd {
627
    /// Make an exported identity a root key
628
    Add {
629
        identity_file: PathBuf,
630
        #[arg(long)]
631
        out: Option<PathBuf>,
632
    },
633
    /// Remove a root key by KeyId prefix
634
    Remove {
635
        key: String,
636
        #[arg(long)]
637
        out: Option<PathBuf>,
638
    },
639
    /// Require this many root signatures on policy updates
640
    Threshold {
641
        n: u32,
642
        #[arg(long)]
643
        out: Option<PathBuf>,
644
    },
645
}
646
647
/// Open the repository containing the current directory.
648
fn open() -> Result<Repo> {
649
    Repo::open(&std::env::current_dir()?).context("opening repository")
650
}
651
652
/// Parse arguments and run; returns the process exit code.
653
pub fn run() -> Result<i32> {
654
    let cli = Cli::parse();
655
    ui::init(cli.plain);
656
    match cli.cmd {
657
        Cmd::Init { path, bare } => workspace::init(path, bare),
658
        Cmd::Status { json } => workspace::status(json),
659
        Cmd::Prompt {
660
            format,
661
            glyphs,
662
            color,
663
        } => workspace::prompt(&format, glyphs, color.as_deref()),
664
        Cmd::Commit {
665
            message,
666
            depends_on,
667
            paths,
668
            interactive,
669
            hunks,
670
        } => workspace::commit(&message, &depends_on, &paths, interactive, &hunks),
671
        Cmd::Cat { path, change } => workspace::cat(&path, change.as_deref()),
672
        Cmd::Root { json } => workspace::root(json),
673
        Cmd::Version { json } => workspace::version(json),
674
        Cmd::Discard { paths, hunks } => workspace::discard(&paths, &hunks),
675
        Cmd::Hunks { json } => workspace::hunks(json),
676
        Cmd::Squash { change, into } => workspace::squash(&change, &into),
677
        Cmd::Rebase { change, onto } => workspace::rebase(&change, &onto),
678
        Cmd::Blame { path, json } => workspace::blame(&path, json),
679
        Cmd::Bisect { cmd } => match cmd {
680
            BisectCmd::Start => workspace::bisect_start(),
681
            BisectCmd::Good { changes } => workspace::bisect_mark(Some(true), &changes),
682
            BisectCmd::Bad { changes } => workspace::bisect_mark(Some(false), &changes),
683
            BisectCmd::Skip => workspace::bisect_mark(None, &[]),
684
            BisectCmd::Run { command } => workspace::bisect_run(&command),
685
            BisectCmd::Status { json } => workspace::bisect_status(json),
686
            BisectCmd::Reset => workspace::bisect_reset(),
687
        },
688
        Cmd::Log {
689
            limit,
690
            verbose,
691
            path,
692
            json,
693
        } => workspace::log(limit, verbose, path.as_deref(), json),
694
        Cmd::Diff { change, json } => workspace::diff(change.as_deref(), json),
695
        Cmd::Describe { message, change } => workspace::describe(&message, change.as_deref()),
696
        Cmd::Amend { message, into } => workspace::amend(message.as_deref(), into.as_deref()),
697
        Cmd::Split { change, paths } => workspace::split(&change, &paths),
698
        Cmd::Abandon { change } => workspace::abandon(&change),
699
        Cmd::Show { change, json } => workspace::show(&change, json),
700
        Cmd::Checkout { changes, discard } => workspace::checkout(&changes, discard),
701
        Cmd::Resolve { json } => workspace::resolve(json),
702
        Cmd::Undo => workspace::undo(),
703
        Cmd::Gc { now } => workspace::gc(now),
704
        Cmd::Fsck { json } => workspace::fsck(json),
705
        Cmd::Doctor { offline, json } => workspace::doctor(offline, json),
706
        Cmd::Checkpoint => workspace::checkpoint(),
707
        Cmd::Op {
708
            cmd: OpCmd::Log { json },
709
        } => workspace::op_log(json),
710
        Cmd::Clone {
711
            url,
712
            dest,
713
            max_blob,
714
            sparse,
715
            depth,
716
        } => sync::clone(&url, dest, max_blob.as_deref(), &sparse, depth),
717
        Cmd::Deepen { depth, remote } => sync::deepen(&remote, depth),
718
        Cmd::Sparse { paths } => workspace::sparse(&paths),
719
        Cmd::Lock { path } => workspace::lock(&path),
720
        Cmd::Unlock { path } => workspace::unlock(&path),
721
        Cmd::Workspace { cmd } => match cmd {
722
            WorkspaceCmd::Add { path, changes } => workspace::workspace_add(&path, &changes),
723
            WorkspaceCmd::List { json } => workspace::workspace_list(json),
724
            WorkspaceCmd::Forget { workspace } => workspace::workspace_forget(&workspace),
725
        },
726
        Cmd::Bookmark { cmd } => match cmd {
727
            BookmarkCmd::Create { name, changes } => workspace::bookmark_create(&name, &changes),
728
            BookmarkCmd::Set { name, changes } => workspace::bookmark_set(&name, &changes),
729
            BookmarkCmd::Delete { name } => workspace::bookmark_delete(&name),
730
            BookmarkCmd::List { json } => workspace::bookmark_list(json),
731
            BookmarkCmd::Switch { name, discard } => workspace::bookmark_switch(&name, discard),
732
        },
733
        Cmd::Merge { bookmark } => workspace::merge(&bookmark),
734
        Cmd::Submodule { cmd } => match cmd {
735
            SubmoduleCmd::Add {
736
                url,
737
                path,
738
                bookmark,
739
            } => sync::submodule_add(&url, &path, bookmark.as_deref()),
740
            SubmoduleCmd::Update { init, paths } => sync::submodule_update(init, &paths),
741
            SubmoduleCmd::Status { json } => sync::submodule_status(json),
742
            SubmoduleCmd::Remove { path } => sync::submodule_remove(&path),
743
        },
744
        Cmd::Tag { cmd } => match cmd {
745
            TagCmd::Create {
746
                name,
747
                changes,
748
                message,
749
            } => workspace::tag_create(&name, &changes, &message),
750
            TagCmd::List { json } => workspace::tag_list(json),
751
        },
752
        Cmd::Remote { cmd } => match cmd {
753
            RemoteCmd::Add { name, url } => sync::remote_add(&name, &url),
754
            RemoteCmd::Remove { name } => sync::remote_remove(&name),
755
            RemoteCmd::List { json } => sync::remote_list(json),
756
        },
757
        Cmd::Fetch { remote, json } => sync::fetch(remote.as_deref(), json),
758
        Cmd::Pull { remote, json } => sync::pull(remote.as_deref(), json),
759
        Cmd::Push {
760
            remote,
761
            force,
762
            bookmark,
763
            json,
764
        } => sync::push(remote.as_deref(), force, bookmark.as_deref(), json),
765
        Cmd::Serve {
766
            root,
767
            bind,
768
            tokens,
769
            tls_cert,
770
            tls_key,
771
        } => sync::serve(root, &bind, tokens, tls_cert.zip(tls_key)),
772
        Cmd::Identity { cmd } => match cmd {
773
            IdentityCmd::Create {
774
                name,
775
                email,
776
                local,
777
                expires,
778
            } => identity::create(name, email, local, expires),
779
            IdentityCmd::Show => identity::show(),
780
            IdentityCmd::Export { path } => identity::export(path),
781
            IdentityCmd::SignChallenge { challenge, json } => {
782
                identity::sign_challenge(&challenge, json)
783
            }
784
            IdentityCmd::Rotate => identity::rotate(),
785
            IdentityCmd::Renew { expires } => identity::renew(expires),
786
            IdentityCmd::Revoke { key, reason } => identity::revoke(key.as_deref(), &reason),
787
            IdentityCmd::Delegate { cmd } => match cmd {
788
                DelegateCmd::Add { identity_file } => identity::delegate_add(&identity_file),
789
                DelegateCmd::Remove { key } => identity::delegate_remove(&key),
790
                DelegateCmd::List => identity::delegate_list(),
791
            },
792
        },
793
        Cmd::Git {
794
            cmd: GitCmd::Export { git_dir, branch },
795
        } => sync::git_export(&git_dir, branch.as_deref()),
796
        Cmd::Git {
797
            cmd: GitCmd::Fetch { url, branch },
798
        } => sync::git_fetch(&url, &branch),
799
        Cmd::Git {
800
            cmd: GitCmd::Push { url, branch },
801
        } => sync::git_push(&url, &branch),
802
        Cmd::Git {
803
            cmd:
804
                GitCmd::Import {
805
                    git_dir,
806
                    dest,
807
                    branch,
808
                },
809
        } => sync::git_import(&git_dir, &dest, &branch),
810
        Cmd::Git {
811
            cmd: GitCmd::Verify { git_dir, branch },
812
        } => sync::git_verify(&git_dir, &branch),
813
        Cmd::Policy { cmd } => match cmd {
814
            PolicyCmd::Init => identity::policy_init(),
815
            PolicyCmd::Show { json } => identity::policy_show(json),
816
            PolicyCmd::Grant {
817
                identity_file,
818
                pattern,
819
                out,
820
            } => identity::policy_grant(&identity_file, pattern.as_deref(), out),
821
            PolicyCmd::Revoke { key, out } => identity::policy_revoke(&key, out),
822
            PolicyCmd::Root { cmd } => match cmd {
823
                RootCmd::Add { identity_file, out } => {
824
                    identity::policy_root_add(&identity_file, out)
825
                }
826
                RootCmd::Remove { key, out } => identity::policy_root_remove(&key, out),
827
                RootCmd::Threshold { n, out } => identity::policy_threshold(n, out),
828
            },
829
            PolicyCmd::Sign { proposal, out } => identity::policy_sign(&proposal, out),
830
        },
831
        Cmd::Seal {
832
            changes,
833
            proposal: Some(url),
834
            json,
835
        } if changes.is_empty() => proposal::seal(&url, json),
836
        Cmd::Seal { changes, json, .. } => identity::seal(&changes, json),
837
        Cmd::Proposal { cmd } => match cmd {
838
            ProposalCmd::Show { url, json } => proposal::show(&url, json),
839
            ProposalCmd::Land { url, force } => proposal::land(&url, force),
840
        },
841
        Cmd::Verify { change, json } => identity::verify(change.as_deref(), json),
842
        Cmd::Config { key, value, user } => identity::config(&key, &value, user),
843
    }
844
}