OXYGENHUB
# 4213c41dfc64 oxygen / src / cli / mod.rs
26.9 KiB · Rust · last changed Roadmap: phase D done 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
    /// Pack loose objects and prune unreachable ones older than the grace period
211
    Gc {
212
        /// Ignore the grace period (core.gc-grace) and prune immediately
213
        #[arg(long)]
214
        now: bool,
215
    },
216
    /// List operations
217
    #[command(name = "op")]
218
    Op {
219
        #[command(subcommand)]
220
        cmd: OpCmd,
221
    },
222
    /// Take the advisory local lock on a lockable file, making it writable
223
    Lock { path: String },
224
    /// Release the local lock and make the file read-only again
225
    Unlock { path: String },
226
    /// Workspaces: extra checkout directories sharing this repository
227
    Workspace {
228
        #[command(subcommand)]
229
        cmd: WorkspaceCmd,
230
    },
231
    /// Bookmarks: named states; each workspace works on one
232
    Bookmark {
233
        #[command(subcommand)]
234
        cmd: BookmarkCmd,
235
    },
236
    /// Union another bookmark's heads into the current one (no merge change)
237
    Merge { bookmark: String },
238
    /// Subrepositories: nested repositories pinned to a state
239
    Submodule {
240
        #[command(subcommand)]
241
        cmd: SubmoduleCmd,
242
    },
243
    /// Tags: immutable named states
244
    Tag {
245
        #[command(subcommand)]
246
        cmd: TagCmd,
247
    },
248
    /// Manage remotes
249
    Remote {
250
        #[command(subcommand)]
251
        cmd: RemoteCmd,
252
    },
253
    /// Fetch every bookmark of a remote into its tracking references
254
    Fetch {
255
        remote: Option<String>,
256
        #[arg(long)]
257
        json: bool,
258
    },
259
    /// Fetch, then union the remote bookmark into the local one (no merge change)
260
    Pull {
261
        remote: Option<String>,
262
        #[arg(long)]
263
        json: bool,
264
    },
265
    /// Push the current bookmark to a remote
266
    Push {
267
        remote: Option<String>,
268
        /// Allow dropping heads the remote has
269
        #[arg(long)]
270
        force: bool,
271
        /// Push this bookmark instead of the workspace's current one
272
        #[arg(short = 'b', long)]
273
        bookmark: Option<String>,
274
        #[arg(long)]
275
        json: bool,
276
    },
277
    /// Serve repositories under ROOT over HTTPS (with --tls-cert/--tls-key) or plain HTTP
278
    Serve {
279
        /// Directory with one repository per subdirectory
280
        #[arg(long, default_value = ".")]
281
        root: PathBuf,
282
        #[arg(long, default_value = "127.0.0.1:8890")]
283
        bind: String,
284
        /// Bearer token allowed to push (repeatable); none means open
285
        #[arg(long = "token")]
286
        tokens: Vec<String>,
287
        /// PEM certificate chain; enables HTTPS together with --tls-key
288
        #[arg(long, requires = "tls_key")]
289
        tls_cert: Option<PathBuf>,
290
        /// PEM private key
291
        #[arg(long, requires = "tls_cert")]
292
        tls_key: Option<PathBuf>,
293
    },
294
    /// Manage the signing identity
295
    Identity {
296
        #[command(subcommand)]
297
        cmd: IdentityCmd,
298
    },
299
    /// Seal changes with your key: named ones, or every change on the current bookmark you can authenticate but have not
300
    Seal {
301
        changes: Vec<String>,
302
        /// Seal every change of a hub proposal and push the seal (your approval)
303
        #[arg(long, conflicts_with = "changes")]
304
        proposal: Option<String>,
305
        #[arg(long)]
306
        json: bool,
307
    },
308
    /// Hub proposals: show one, or land it from this client
309
    Proposal {
310
        #[command(subcommand)]
311
        cmd: ProposalCmd,
312
    },
313
    /// Verify the signatures on a change (default: every change in the log)
314
    Verify {
315
        change: Option<String>,
316
        #[arg(long)]
317
        json: bool,
318
    },
319
    /// Git interoperability
320
    Git {
321
        #[command(subcommand)]
322
        cmd: GitCmd,
323
    },
324
    /// Manage the trust policy (who may update which bookmarks)
325
    Policy {
326
        #[command(subcommand)]
327
        cmd: PolicyCmd,
328
    },
329
    /// 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)
330
    Config {
331
        key: String,
332
        value: String,
333
        /// Write to the user file (~/.config/oxy/config.toml) instead of this repository
334
        #[arg(long)]
335
        user: bool,
336
    },
337
}
338
339
#[derive(Subcommand)]
340
enum OpCmd {
341
    Log {
342
        #[arg(long)]
343
        json: bool,
344
    },
345
}
346
347
#[derive(Subcommand)]
348
enum WorkspaceCmd {
349
    /// Create a workspace at PATH checked out on CHANGE... (default: the bookmark heads)
350
    Add { path: PathBuf, changes: Vec<String> },
351
    List {
352
        #[arg(long)]
353
        json: bool,
354
    },
355
    /// Stop tracking a workspace (by id prefix or path); its files stay
356
    Forget { workspace: String },
357
}
358
359
#[derive(Subcommand)]
360
enum BisectCmd {
361
    /// Begin; remembers where the working copy was
362
    Start,
363
    /// Mark CHANGE... (or the state under test) as good
364
    Good { changes: Vec<String> },
365
    /// Mark CHANGE... (or the state under test) as bad
366
    Bad { changes: Vec<String> },
367
    /// The state under test cannot be tested; try another
368
    Skip,
369
    /// Run a command for each step: exit 0 good, 125 skip, else bad
370
    Run {
371
        #[arg(required = true, trailing_var_arg = true)]
372
        command: Vec<String>,
373
    },
374
    /// Where the search stands
375
    Status {
376
        #[arg(long)]
377
        json: bool,
378
    },
379
    /// Restore the working copy and forget the search
380
    Reset,
381
}
382
383
#[derive(Subcommand)]
384
enum BookmarkCmd {
385
    /// Create a bookmark at CHANGE... (default: the working copy's base)
386
    Create { name: String, changes: Vec<String> },
387
    /// Point an existing bookmark at CHANGE...
388
    Set { name: String, changes: Vec<String> },
389
    /// Remove a bookmark; its changes stay
390
    Delete { name: String },
391
    /// List bookmarks with their heads and difference from origin
392
    List {
393
        #[arg(long)]
394
        json: bool,
395
    },
396
    /// Make NAME this workspace's bookmark and check it out
397
    Switch {
398
        name: String,
399
        /// Throw away uncommitted working-copy changes
400
        #[arg(long)]
401
        discard: bool,
402
    },
403
}
404
405
#[derive(Subcommand)]
406
enum SubmoduleCmd {
407
    /// Clone URL (Oxygen or Git) into PATH and register it in .oxymodules
408
    Add {
409
        url: String,
410
        path: String,
411
        /// Bookmark (Oxygen) or branch (Git) to check out and track
412
        #[arg(long)]
413
        bookmark: Option<String>,
414
    },
415
    /// Bring nested repositories to their pins
416
    Update {
417
        /// Clone nested repositories that are absent
418
        #[arg(long)]
419
        init: bool,
420
        paths: Vec<String>,
421
    },
422
    /// List subrepos with their pins and on-disk state
423
    Status {
424
        #[arg(long)]
425
        json: bool,
426
    },
427
    /// Unregister PATH; the directory stays, the node goes with the next commit
428
    Remove { path: String },
429
}
430
431
#[derive(Subcommand)]
432
enum TagCmd {
433
    /// Tag the state of CHANGE... (default: the bookmark heads)
434
    Create {
435
        name: String,
436
        changes: Vec<String>,
437
        #[arg(short, long, default_value = "")]
438
        message: String,
439
    },
440
    List {
441
        #[arg(long)]
442
        json: bool,
443
    },
444
}
445
446
#[derive(Subcommand)]
447
enum RemoteCmd {
448
    Add {
449
        name: String,
450
        url: String,
451
    },
452
    Remove {
453
        name: String,
454
    },
455
    List {
456
        #[arg(long)]
457
        json: bool,
458
    },
459
}
460
461
#[derive(Subcommand)]
462
enum ProposalCmd {
463
    /// Show a proposal by its hub URL
464
    Show {
465
        url: String,
466
        #[arg(long)]
467
        json: bool,
468
    },
469
    /// Fetch the proposal's changes, set the target bookmark to the projected state, and push with your key
470
    Land {
471
        url: String,
472
        /// Land even when the hub reports blockers
473
        #[arg(long)]
474
        force: bool,
475
    },
476
}
477
478
#[derive(Subcommand)]
479
enum IdentityCmd {
480
    /// Generate a composite Ed25519 + ML-DSA-65 key pair and make it your author identity
481
    Create {
482
        #[arg(long)]
483
        name: Option<String>,
484
        #[arg(long)]
485
        email: Option<String>,
486
        /// Record the author in this repository only, not in the user config
487
        #[arg(long)]
488
        local: bool,
489
        /// Expire the identity this many days from now
490
        #[arg(long)]
491
        expires: Option<u64>,
492
    },
493
    /// Show the configured identity
494
    Show,
495
    /// Write your identity object to a file, to hand to a policy holder
496
    Export { path: Option<PathBuf> },
497
    /// Sign a host's challenge (hex, at least 16 bytes) to prove you hold your key; prints the signature as hex
498
    SignChallenge {
499
        challenge: String,
500
        #[arg(long)]
501
        json: bool,
502
    },
503
    /// Move to a fresh key pair; the policy follows if it names your key
504
    Rotate,
505
    /// Re-issue your identity with a new expiry (in days from now; 0 = never)
506
    Renew {
507
        #[arg(long)]
508
        expires: u64,
509
    },
510
    /// Revoke a key: yours by default, or any key if you hold a policy root
511
    Revoke {
512
        /// KeyId prefix (default: your own key)
513
        key: Option<String>,
514
        #[arg(long, default_value = "")]
515
        reason: String,
516
    },
517
    /// Keys allowed to sign or seal on your behalf
518
    Delegate {
519
        #[command(subcommand)]
520
        cmd: DelegateCmd,
521
    },
522
}
523
524
#[derive(Subcommand)]
525
enum DelegateCmd {
526
    /// Add a delegate from an exported identity file
527
    Add { identity_file: PathBuf },
528
    /// Remove a delegate by KeyId prefix
529
    Remove { key: String },
530
    /// List delegates
531
    List,
532
}
533
534
#[derive(Subcommand)]
535
enum GitCmd {
536
    /// Export the current bookmark to a Git repository (created bare if absent)
537
    Export {
538
        git_dir: PathBuf,
539
        /// Git branch to write (default: the bookmark name)
540
        #[arg(long)]
541
        branch: Option<String>,
542
    },
543
    /// Fetch a branch from a Git remote (path or URL) into the current bookmark
544
    Fetch {
545
        url: String,
546
        #[arg(long, default_value = "main")]
547
        branch: String,
548
    },
549
    /// Push the current bookmark to a branch of a Git remote (fetch first)
550
    Push {
551
        url: String,
552
        #[arg(long, default_value = "main")]
553
        branch: String,
554
    },
555
    /// Import a Git branch into a new Oxygen repository at DEST
556
    Import {
557
        git_dir: PathBuf,
558
        dest: PathBuf,
559
        #[arg(long, default_value = "main")]
560
        branch: String,
561
    },
562
}
563
564
#[derive(Subcommand)]
565
enum PolicyCmd {
566
    /// Create the initial policy with your identity as the only root
567
    Init,
568
    /// Show the current policy
569
    Show {
570
        #[arg(long)]
571
        json: bool,
572
    },
573
    /// Allow an identity (from `oxy identity export`) to update bookmarks
574
    Grant {
575
        identity_file: PathBuf,
576
        /// Reference pattern the grant applies to (default: every bookmark)
577
        #[arg(long)]
578
        pattern: Option<String>,
579
        /// Where to write the proposal when more root signatures are needed
580
        #[arg(long)]
581
        out: Option<PathBuf>,
582
    },
583
    /// Revoke a key (KeyId prefix of an identity in the policy)
584
    Revoke {
585
        key: String,
586
        /// Where to write the proposal when more root signatures are needed
587
        #[arg(long)]
588
        out: Option<PathBuf>,
589
    },
590
    /// Manage root keys and the signature threshold for policy updates
591
    Root {
592
        #[command(subcommand)]
593
        cmd: RootCmd,
594
    },
595
    /// Add your root signature to a proposal file; applies it once the threshold is met
596
    Sign {
597
        proposal: PathBuf,
598
        /// Where to write the proposal if it still needs signatures (default: in place)
599
        #[arg(long)]
600
        out: Option<PathBuf>,
601
    },
602
}
603
604
#[derive(Subcommand)]
605
enum RootCmd {
606
    /// Make an exported identity a root key
607
    Add {
608
        identity_file: PathBuf,
609
        #[arg(long)]
610
        out: Option<PathBuf>,
611
    },
612
    /// Remove a root key by KeyId prefix
613
    Remove {
614
        key: String,
615
        #[arg(long)]
616
        out: Option<PathBuf>,
617
    },
618
    /// Require this many root signatures on policy updates
619
    Threshold {
620
        n: u32,
621
        #[arg(long)]
622
        out: Option<PathBuf>,
623
    },
624
}
625
626
/// Open the repository containing the current directory.
627
fn open() -> Result<Repo> {
628
    Repo::open(&std::env::current_dir()?).context("opening repository")
629
}
630
631
/// Parse arguments and run; returns the process exit code.
632
pub fn run() -> Result<i32> {
633
    let cli = Cli::parse();
634
    ui::init(cli.plain);
635
    match cli.cmd {
636
        Cmd::Init { path, bare } => workspace::init(path, bare),
637
        Cmd::Status { json } => workspace::status(json),
638
        Cmd::Prompt {
639
            format,
640
            glyphs,
641
            color,
642
        } => workspace::prompt(&format, glyphs, color.as_deref()),
643
        Cmd::Commit {
644
            message,
645
            depends_on,
646
            paths,
647
            interactive,
648
            hunks,
649
        } => workspace::commit(&message, &depends_on, &paths, interactive, &hunks),
650
        Cmd::Cat { path, change } => workspace::cat(&path, change.as_deref()),
651
        Cmd::Root { json } => workspace::root(json),
652
        Cmd::Version { json } => workspace::version(json),
653
        Cmd::Discard { paths, hunks } => workspace::discard(&paths, &hunks),
654
        Cmd::Hunks { json } => workspace::hunks(json),
655
        Cmd::Squash { change, into } => workspace::squash(&change, &into),
656
        Cmd::Rebase { change, onto } => workspace::rebase(&change, &onto),
657
        Cmd::Blame { path, json } => workspace::blame(&path, json),
658
        Cmd::Bisect { cmd } => match cmd {
659
            BisectCmd::Start => workspace::bisect_start(),
660
            BisectCmd::Good { changes } => workspace::bisect_mark(Some(true), &changes),
661
            BisectCmd::Bad { changes } => workspace::bisect_mark(Some(false), &changes),
662
            BisectCmd::Skip => workspace::bisect_mark(None, &[]),
663
            BisectCmd::Run { command } => workspace::bisect_run(&command),
664
            BisectCmd::Status { json } => workspace::bisect_status(json),
665
            BisectCmd::Reset => workspace::bisect_reset(),
666
        },
667
        Cmd::Log {
668
            limit,
669
            verbose,
670
            path,
671
            json,
672
        } => workspace::log(limit, verbose, path.as_deref(), json),
673
        Cmd::Diff { change, json } => workspace::diff(change.as_deref(), json),
674
        Cmd::Describe { message, change } => workspace::describe(&message, change.as_deref()),
675
        Cmd::Amend { message, into } => workspace::amend(message.as_deref(), into.as_deref()),
676
        Cmd::Split { change, paths } => workspace::split(&change, &paths),
677
        Cmd::Abandon { change } => workspace::abandon(&change),
678
        Cmd::Show { change, json } => workspace::show(&change, json),
679
        Cmd::Checkout { changes, discard } => workspace::checkout(&changes, discard),
680
        Cmd::Resolve { json } => workspace::resolve(json),
681
        Cmd::Undo => workspace::undo(),
682
        Cmd::Gc { now } => workspace::gc(now),
683
        Cmd::Checkpoint => workspace::checkpoint(),
684
        Cmd::Op {
685
            cmd: OpCmd::Log { json },
686
        } => workspace::op_log(json),
687
        Cmd::Clone {
688
            url,
689
            dest,
690
            max_blob,
691
            sparse,
692
            depth,
693
        } => sync::clone(&url, dest, max_blob.as_deref(), &sparse, depth),
694
        Cmd::Deepen { depth, remote } => sync::deepen(&remote, depth),
695
        Cmd::Sparse { paths } => workspace::sparse(&paths),
696
        Cmd::Lock { path } => workspace::lock(&path),
697
        Cmd::Unlock { path } => workspace::unlock(&path),
698
        Cmd::Workspace { cmd } => match cmd {
699
            WorkspaceCmd::Add { path, changes } => workspace::workspace_add(&path, &changes),
700
            WorkspaceCmd::List { json } => workspace::workspace_list(json),
701
            WorkspaceCmd::Forget { workspace } => workspace::workspace_forget(&workspace),
702
        },
703
        Cmd::Bookmark { cmd } => match cmd {
704
            BookmarkCmd::Create { name, changes } => workspace::bookmark_create(&name, &changes),
705
            BookmarkCmd::Set { name, changes } => workspace::bookmark_set(&name, &changes),
706
            BookmarkCmd::Delete { name } => workspace::bookmark_delete(&name),
707
            BookmarkCmd::List { json } => workspace::bookmark_list(json),
708
            BookmarkCmd::Switch { name, discard } => workspace::bookmark_switch(&name, discard),
709
        },
710
        Cmd::Merge { bookmark } => workspace::merge(&bookmark),
711
        Cmd::Submodule { cmd } => match cmd {
712
            SubmoduleCmd::Add {
713
                url,
714
                path,
715
                bookmark,
716
            } => sync::submodule_add(&url, &path, bookmark.as_deref()),
717
            SubmoduleCmd::Update { init, paths } => sync::submodule_update(init, &paths),
718
            SubmoduleCmd::Status { json } => sync::submodule_status(json),
719
            SubmoduleCmd::Remove { path } => sync::submodule_remove(&path),
720
        },
721
        Cmd::Tag { cmd } => match cmd {
722
            TagCmd::Create {
723
                name,
724
                changes,
725
                message,
726
            } => workspace::tag_create(&name, &changes, &message),
727
            TagCmd::List { json } => workspace::tag_list(json),
728
        },
729
        Cmd::Remote { cmd } => match cmd {
730
            RemoteCmd::Add { name, url } => sync::remote_add(&name, &url),
731
            RemoteCmd::Remove { name } => sync::remote_remove(&name),
732
            RemoteCmd::List { json } => sync::remote_list(json),
733
        },
734
        Cmd::Fetch { remote, json } => sync::fetch(remote.as_deref(), json),
735
        Cmd::Pull { remote, json } => sync::pull(remote.as_deref(), json),
736
        Cmd::Push {
737
            remote,
738
            force,
739
            bookmark,
740
            json,
741
        } => sync::push(remote.as_deref(), force, bookmark.as_deref(), json),
742
        Cmd::Serve {
743
            root,
744
            bind,
745
            tokens,
746
            tls_cert,
747
            tls_key,
748
        } => sync::serve(root, &bind, tokens, tls_cert.zip(tls_key)),
749
        Cmd::Identity { cmd } => match cmd {
750
            IdentityCmd::Create {
751
                name,
752
                email,
753
                local,
754
                expires,
755
            } => identity::create(name, email, local, expires),
756
            IdentityCmd::Show => identity::show(),
757
            IdentityCmd::Export { path } => identity::export(path),
758
            IdentityCmd::SignChallenge { challenge, json } => {
759
                identity::sign_challenge(&challenge, json)
760
            }
761
            IdentityCmd::Rotate => identity::rotate(),
762
            IdentityCmd::Renew { expires } => identity::renew(expires),
763
            IdentityCmd::Revoke { key, reason } => identity::revoke(key.as_deref(), &reason),
764
            IdentityCmd::Delegate { cmd } => match cmd {
765
                DelegateCmd::Add { identity_file } => identity::delegate_add(&identity_file),
766
                DelegateCmd::Remove { key } => identity::delegate_remove(&key),
767
                DelegateCmd::List => identity::delegate_list(),
768
            },
769
        },
770
        Cmd::Git {
771
            cmd: GitCmd::Export { git_dir, branch },
772
        } => sync::git_export(&git_dir, branch.as_deref()),
773
        Cmd::Git {
774
            cmd: GitCmd::Fetch { url, branch },
775
        } => sync::git_fetch(&url, &branch),
776
        Cmd::Git {
777
            cmd: GitCmd::Push { url, branch },
778
        } => sync::git_push(&url, &branch),
779
        Cmd::Git {
780
            cmd:
781
                GitCmd::Import {
782
                    git_dir,
783
                    dest,
784
                    branch,
785
                },
786
        } => sync::git_import(&git_dir, &dest, &branch),
787
        Cmd::Policy { cmd } => match cmd {
788
            PolicyCmd::Init => identity::policy_init(),
789
            PolicyCmd::Show { json } => identity::policy_show(json),
790
            PolicyCmd::Grant {
791
                identity_file,
792
                pattern,
793
                out,
794
            } => identity::policy_grant(&identity_file, pattern.as_deref(), out),
795
            PolicyCmd::Revoke { key, out } => identity::policy_revoke(&key, out),
796
            PolicyCmd::Root { cmd } => match cmd {
797
                RootCmd::Add { identity_file, out } => {
798
                    identity::policy_root_add(&identity_file, out)
799
                }
800
                RootCmd::Remove { key, out } => identity::policy_root_remove(&key, out),
801
                RootCmd::Threshold { n, out } => identity::policy_threshold(n, out),
802
            },
803
            PolicyCmd::Sign { proposal, out } => identity::policy_sign(&proposal, out),
804
        },
805
        Cmd::Seal {
806
            changes,
807
            proposal: Some(url),
808
            json,
809
        } if changes.is_empty() => proposal::seal(&url, json),
810
        Cmd::Seal { changes, json, .. } => identity::seal(&changes, json),
811
        Cmd::Proposal { cmd } => match cmd {
812
            ProposalCmd::Show { url, json } => proposal::show(&url, json),
813
            ProposalCmd::Land { url, force } => proposal::land(&url, force),
814
        },
815
        Cmd::Verify { change, json } => identity::verify(change.as_deref(), json),
816
        Cmd::Config { key, value, user } => identity::config(&key, &value, user),
817
    }
818
}