Coverage / src/cli/mod.rs 61.6%
249 of 404 lines run, 11 of 22 functions · run #427 on main · run missed
| 1 | //! Command-line definitions and dispatch. | |
| 2 | | |
| 3 | mod approve; | |
| 4 | mod artifact; | |
| 5 | mod auth; | |
| 6 | mod format; | |
| 7 | mod identity; | |
| 8 | mod json; | |
| 9 | mod mcp; | |
| 10 | mod proposal; | |
| 11 | mod receipt; | |
| 12 | mod sync; | |
| 13 | mod tlog; | |
| 14 | pub mod ui; | |
| 15 | mod workspace; | |
| 16 | ||
| 17 | use anyhow::{Context, Result}; | |
| 18 | use clap::{Parser, Subcommand}; | |
| 19 | use oxy_core::repo::Repo; | |
| 20 | use std::path::PathBuf; | |
| 21 | ||
| 22 | #[derive(Parser)] | |
| 23 | #[command( | |
| 24 | name = "oxy", | |
| 25 | version = env!("OXY_VERSION_LONG"), | |
| 26 | about = "Oxygen: breathing fresh air into version control" | |
| 27 | )] | |
| 28 | struct Cli { | |
| 29 | /// Plain output: no colour, no spinner (also implied by NO_COLOR or a non-terminal) | |
| 30 | #[arg(long, global = true)] | |
| 31 | plain: bool, | |
| 32 | /// Read the repository as it was after operation OP (an ID from oxy op log, @ or @-); commands that would change it are refused | |
| 33 | #[arg(long, global = true, value_name = "OP")] | |
| 34 | at_op: Option<String>, | |
| 35 | #[command(subcommand)] | |
| 36 | cmd: Cmd, | |
| 37 | } | |
| 38 | ||
| 39 | /// `--at-op`, for `open` to apply. | |
| 40 | static AT_OP: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new(); | |
| 41 | ||
| 42 | #[derive(Subcommand)] | |
| 43 | enum Cmd { | |
| 44 | /// Create a repository in PATH (default: current directory) | |
| 45 | Init { | |
| 46 | /// Directory to create the repository in (default: the current directory) | |
| 47 | path: Option<PathBuf>, | |
| 48 | /// No working copy: an object store and references only, for hosting | |
| 49 | #[arg(long)] | |
| 50 | bare: bool, | |
| 51 | /// Keep the objects and the operation log on this store | |
| 52 | /// (dir://PATH, s3://BUCKET/PREFIX, gs://…, az://…) instead of | |
| 53 | /// in .oxy, with a local cache in front (storage.cache, 2 GiB) | |
| 54 | #[arg(long, value_name = "URL")] | |
| 55 | store: Option<String>, | |
| 56 | }, | |
| 57 | /// Clone a repository from a local path or an http(s) URL | |
| 58 | Clone { | |
| 59 | /// Local path or http(s) URL of the repository | |
| 60 | url: String, | |
| 61 | /// Directory to clone into (default: the last part of the URL) | |
| 62 | dest: Option<PathBuf>, | |
| 63 | /// Leave blobs larger than this (e.g. 1M) to be fetched on first read | |
| 64 | #[arg(long)] | |
| 65 | max_blob: Option<String>, | |
| 66 | /// Materialize only these paths (repeatable); implies lazy blobs | |
| 67 | #[arg(long = "sparse")] | |
| 68 | sparse: Vec<String>, | |
| 69 | /// Shallow clone: fetch only this many levels of history below the heads | |
| 70 | #[arg(long)] | |
| 71 | depth: Option<u32>, | |
| 72 | /// Clone the subrepos too (as oxy submodule update --init) | |
| 73 | #[arg(long)] | |
| 74 | recursive: bool, | |
| 75 | }, | |
| 76 | /// Fetch more history below a shallow clone's boundary | |
| 77 | Deepen { | |
| 78 | /// Levels of history to add below the boundary | |
| 79 | #[arg(default_value_t = 10)] | |
| 80 | depth: u32, | |
| 81 | /// Remote to fetch the history from | |
| 82 | #[arg(long, default_value = "origin")] | |
| 83 | remote: String, | |
| 84 | }, | |
| 85 | /// Show or set the sparse filter (paths to materialize); re-checks out the bookmark | |
| 86 | Sparse { | |
| 87 | /// Paths to materialize; none prints the current filter | |
| 88 | paths: Vec<String>, | |
| 89 | }, | |
| 90 | /// Show the working copy: changed paths and conflicts | |
| 91 | Status { | |
| 92 | /// Machine-readable output: paths, and every conflict with its kind and contributing changes | |
| 93 | #[arg(long)] | |
| 94 | json: bool, | |
| 95 | }, | |
| 96 | /// One-line summary for shell prompts; fast and records nothing | |
| 97 | Prompt { | |
| 98 | /// 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 | |
| 99 | #[arg(long, default_value = oxy_core::repo::DEFAULT_PROMPT_FORMAT)] | |
| 100 | format: String, | |
| 101 | /// Glyph format (%i %b %c %s%U%O) instead of the plain default | |
| 102 | #[arg(long)] | |
| 103 | glyphs: bool, | |
| 104 | /// Colour by state (cyan clean, yellow dirty, red conflicted): raw escapes, or guarded for a bash or zsh PS1 | |
| 105 | #[arg(long, value_parser = ["raw", "bash", "zsh"])] | |
| 106 | color: Option<String>, | |
| 107 | }, | |
| 108 | /// Record the working copy as a change on the current bookmark | |
| 109 | Commit { | |
| 110 | /// The change's message: a subject line, then optionally a blank line and a body | |
| 111 | #[arg(short, long, required_unless_present = "file", conflicts_with = "file")] | |
| 112 | message: Option<String>, | |
| 113 | /// Read the message from FILE (`-` for standard input) | |
| 114 | #[arg(short = 'F', long, value_name = "FILE")] | |
| 115 | file: Option<PathBuf>, | |
| 116 | /// Extra explicit dependencies (ChangeId prefix, #hash prefix) | |
| 117 | #[arg(long = "depends-on")] | |
| 118 | depends_on: Vec<String>, | |
| 119 | /// Commit only these paths; the rest stays as uncommitted work | |
| 120 | paths: Vec<String>, | |
| 121 | /// Choose hunks one by one | |
| 122 | #[arg(short, long)] | |
| 123 | interactive: bool, | |
| 124 | /// Commit these hunks (as `oxy hunks` numbers them), comma separated | |
| 125 | #[arg(long, value_delimiter = ',')] | |
| 126 | hunks: Vec<usize>, | |
| 127 | }, | |
| 128 | /// Fold CHANGE into one of its dependencies (keeps that change's id) | |
| 129 | Squash { | |
| 130 | /// The change to fold (ChangeId prefix, #hash prefix) | |
| 131 | change: String, | |
| 132 | /// The dependency that absorbs it | |
| 133 | #[arg(long)] | |
| 134 | into: String, | |
| 135 | }, | |
| 136 | /// Re-express CHANGE against another base state | |
| 137 | Rebase { | |
| 138 | /// The change to re-express (ChangeId prefix, #hash prefix) | |
| 139 | change: String, | |
| 140 | /// The changes, bookmark or tag whose state becomes the new base | |
| 141 | #[arg(long, required = true, num_args = 1..)] | |
| 142 | onto: Vec<String>, | |
| 143 | /// Where the rebase would be conflicted, merge the target into the | |
| 144 | /// current bookmark instead, leaving the conflict in the working | |
| 145 | /// copy to fix and commit | |
| 146 | #[arg(long)] | |
| 147 | into_working_copy: bool, | |
| 148 | }, | |
| 149 | /// Show which change introduced each line of a file | |
| 150 | Blame { | |
| 151 | /// File to annotate, relative to the repository root | |
| 152 | path: String, | |
| 153 | /// Machine-readable output: one JSON document on stdout | |
| 154 | #[arg(long)] | |
| 155 | json: bool, | |
| 156 | }, | |
| 157 | /// Find the change that introduced a regression | |
| 158 | Bisect { | |
| 159 | #[command(subcommand)] | |
| 160 | cmd: BisectCmd, | |
| 161 | }, | |
| 162 | /// List changes reachable from the bookmark and the working copy | |
| 163 | Log { | |
| 164 | /// Show the changes a revset selects: `main` (a bookmark name means its | |
| 165 | /// heads; `bookmark(main)` only ever the bookmark), `main..feature & | |
| 166 | /// author(andy)`, `touches("src/**") & date(-7d..)` (see | |
| 167 | /// docs/revset_query_language.md) | |
| 168 | #[arg(short = 'r', long, value_name = "REVSET", conflicts_with_all = ["path", "lines"])] | |
| 169 | revset: Option<String>, | |
| 170 | /// Show at most this many changes | |
| 171 | #[arg(short = 'n', long)] | |
| 172 | limit: Option<usize>, | |
| 173 | /// Show full messages, dependencies, and signatures | |
| 174 | #[arg(short, long)] | |
| 175 | verbose: bool, | |
| 176 | /// Only changes touching this path (renames followed) | |
| 177 | #[arg(long)] | |
| 178 | path: Option<String>, | |
| 179 | /// Only changes that wrote or deleted lines START to END of PATH | |
| 180 | /// (numbered as in the working copy's base), or deleted lines from | |
| 181 | /// between them: the history of a function | |
| 182 | #[arg(short = 'L', value_name = "START,END:PATH", conflicts_with = "path")] | |
| 183 | lines: Option<String>, | |
| 184 | /// Machine-readable output: one JSON document on stdout | |
| 185 | #[arg(long)] | |
| 186 | json: bool, | |
| 187 | }, | |
| 188 | /// Show what the working copy changes against its base | |
| 189 | Diff { | |
| 190 | /// A change instead of the working copy | |
| 191 | change: Option<String>, | |
| 192 | /// Machine-readable output: one JSON document on stdout | |
| 193 | #[arg(long)] | |
| 194 | json: bool, | |
| 195 | }, | |
| 196 | /// Print a file as it is in a state (default: the working copy's base) | |
| 197 | Cat { | |
| 198 | /// File to print, relative to the repository root | |
| 199 | path: String, | |
| 200 | /// A bookmark or tag (its state), or a change: a change means its | |
| 201 | /// dependency closure, the change and what it depends on, so files | |
| 202 | /// only later changes touched are absent or at older versions | |
| 203 | #[arg(short = 'r', long)] | |
| 204 | change: Option<String>, | |
| 205 | }, | |
| 206 | /// Print the repository root, workspace, and bookmark | |
| 207 | Root { | |
| 208 | /// Machine-readable output: one JSON document on stdout | |
| 209 | #[arg(long)] | |
| 210 | json: bool, | |
| 211 | }, | |
| 212 | /// Serve this repository to an AI agent over the Model Context Protocol (stdio), signing as this checkout's identity | |
| 213 | Mcp { | |
| 214 | /// The repository's root (default: the current directory) | |
| 215 | dir: Option<PathBuf>, | |
| 216 | /// Also offer tools that can lose work: undo, and a forced push | |
| 217 | #[arg(long)] | |
| 218 | allow_destructive: bool, | |
| 219 | }, | |
| 220 | /// Print the version of oxy, its repository format, and protocol | |
| 221 | Version { | |
| 222 | /// Machine-readable output: one JSON document on stdout | |
| 223 | #[arg(long)] | |
| 224 | json: bool, | |
| 225 | }, | |
| 226 | /// Restore paths to the working copy's base, or undo chosen hunks | |
| 227 | Discard { | |
| 228 | /// Paths to restore; none with --hunks, or everything with neither | |
| 229 | paths: Vec<String>, | |
| 230 | /// Hunk indices as `oxy hunks` numbers them, comma separated | |
| 231 | #[arg(long, value_delimiter = ',')] | |
| 232 | hunks: Vec<usize>, | |
| 233 | }, | |
| 234 | /// List the working copy's hunks, numbered as commit --hunks expects | |
| 235 | Hunks { | |
| 236 | /// Machine-readable output: one JSON document on stdout | |
| 237 | #[arg(long)] | |
| 238 | json: bool, | |
| 239 | }, | |
| 240 | /// With -m, change the message of a head; without, name the working | |
| 241 | /// copy for a build: its tag, or the nearest tag plus distance and | |
| 242 | /// change, or the bookmark and change, and -dirty | |
| 243 | Describe { | |
| 244 | /// The new message | |
| 245 | #[arg(short, long)] | |
| 246 | message: Option<String>, | |
| 247 | /// The head to describe (default: the one the working copy sits on) | |
| 248 | change: Option<String>, | |
| 249 | /// Machine-readable output (naming only) | |
| 250 | #[arg(long, conflicts_with = "message")] | |
| 251 | json: bool, | |
| 252 | }, | |
| 253 | /// Fold the working copy into the head it sits on, or into CHANGE with --into | |
| 254 | Amend { | |
| 255 | /// Replace the message too | |
| 256 | #[arg(short, long)] | |
| 257 | message: Option<String>, | |
| 258 | /// Amend an older change; the edits must touch only lines it already knows | |
| 259 | #[arg(long, conflicts_with = "paths")] | |
| 260 | into: Option<String>, | |
| 261 | /// Fold in only these paths; the rest stays as uncommitted work | |
| 262 | paths: Vec<String>, | |
| 263 | }, | |
| 264 | /// Split a head: the named paths become a separate first change | |
| 265 | Split { | |
| 266 | /// The head to split | |
| 267 | change: String, | |
| 268 | /// Paths whose edits become the first change | |
| 269 | paths: Vec<String>, | |
| 270 | }, | |
| 271 | /// Drop a change from the bookmark; its content stays on disk as uncommitted work | |
| 272 | Abandon { | |
| 273 | /// The change to drop (ChangeId prefix, #hash prefix) | |
| 274 | #[arg(required_unless_present = "revset")] | |
| 275 | change: Option<String>, | |
| 276 | /// Drop every change a revset selects, newest first | |
| 277 | #[arg(short = 'r', long, value_name = "REVSET", conflicts_with = "change")] | |
| 278 | revset: Option<String>, | |
| 279 | /// Drop more than five changes without asking | |
| 280 | #[arg(long, short = 'y')] | |
| 281 | yes: bool, | |
| 282 | }, | |
| 283 | /// Show one change (ChangeId prefix, #hash prefix, or @), or those a revset selects | |
| 284 | Show { | |
| 285 | /// ChangeId prefix, #hash prefix, or @ for the working copy | |
| 286 | #[arg(required_unless_present = "revset")] | |
| 287 | change: Option<String>, | |
| 288 | /// Show every change a revset selects | |
| 289 | #[arg(short = 'r', long, value_name = "REVSET", conflicts_with = "change")] | |
| 290 | revset: Option<String>, | |
| 291 | /// The files it touched with lines added and removed, instead of the diff | |
| 292 | #[arg(long)] | |
| 293 | stat: bool, | |
| 294 | /// Machine-readable output: one JSON document on stdout | |
| 295 | #[arg(long)] | |
| 296 | json: bool, | |
| 297 | }, | |
| 298 | /// Move the working copy to the state formed by CHANGE... (default: the bookmark heads) | |
| 299 | Checkout { | |
| 300 | /// Changes whose state to check out (default: the bookmark heads) | |
| 301 | changes: Vec<String>, | |
| 302 | /// Throw away uncommitted working-copy changes | |
| 303 | #[arg(long)] | |
| 304 | discard: bool, | |
| 305 | /// Take uncommitted changes along; refused when the target has | |
| 306 | /// other versions of the files they touch | |
| 307 | #[arg(long, conflicts_with = "discard")] | |
| 308 | carry: bool, | |
| 309 | }, | |
| 310 | /// Report which conflicts of the working copy's base are resolved by the current files; | |
| 311 | /// with paths, settle their replace conflicts (binary files, subrepo pins) with what the | |
| 312 | /// working copy holds there | |
| 313 | Resolve { | |
| 314 | /// Opaque files or subrepos whose replace conflict to settle with their current content; | |
| 315 | /// with --both, the text files whose conflicts to settle (all of them when none) | |
| 316 | paths: Vec<String>, | |
| 317 | /// Settle each adjacent insertion (two changes adding lines at the same place) by keeping | |
| 318 | /// every side, earliest change first; refused where the sides changed the same lines | |
| 319 | #[arg(long)] | |
| 320 | both: bool, | |
| 321 | /// With --both, the latest change first | |
| 322 | #[arg(long, requires = "both")] | |
| 323 | reverse: bool, | |
| 324 | /// Machine-readable output: one JSON document on stdout | |
| 325 | #[arg(long)] | |
| 326 | json: bool, | |
| 327 | }, | |
| 328 | /// Undo the last operation | |
| 329 | Undo, | |
| 330 | /// Write a checkpoint of the current bookmark's state now | |
| 331 | Checkpoint, | |
| 332 | /// Print a shell completion script: `oxy completions bash > ~/.local/share/bash-completion/completions/oxy` | |
| 333 | Completions { | |
| 334 | /// bash, zsh, fish, elvish or powershell | |
| 335 | shell: clap_complete::Shell, | |
| 336 | }, | |
| 337 | /// Write man pages (oxy.1 and one per command) into DIR, or print oxy.1 | |
| 338 | Man { | |
| 339 | /// Directory to write every page into, e.g. ~/.local/share/man/man1 | |
| 340 | #[arg(long)] | |
| 341 | dir: Option<PathBuf>, | |
| 342 | }, | |
| 343 | /// Check the identity, configuration and remotes, and say what to do about any problem | |
| 344 | Doctor { | |
| 345 | /// Do not contact the remotes | |
| 346 | #[arg(long)] | |
| 347 | offline: bool, | |
| 348 | /// Machine-readable output: one JSON document on stdout | |
| 349 | #[arg(long)] | |
| 350 | json: bool, | |
| 351 | }, | |
| 352 | /// Check the repository's integrity: objects, operation log, references, signatures, checkpoints | |
| 353 | Fsck { | |
| 354 | /// Machine-readable output: one JSON document on stdout | |
| 355 | #[arg(long)] | |
| 356 | json: bool, | |
| 357 | }, | |
| 358 | /// Prune unreachable objects older than the grace period and repack everything into one pack | |
| 359 | Gc { | |
| 360 | /// Ignore the grace period (core.gc-grace) and prune immediately | |
| 361 | #[arg(long)] | |
| 362 | now: bool, | |
| 363 | /// Keep no past working-copy snapshots, only each workspace's current one, so files only they held go (core.snapshot-retention, default 7 days) | |
| 364 | #[arg(long)] | |
| 365 | expire_snapshots: bool, | |
| 366 | /// Instead: fold the bundles of a remote kept on a store into one, and remove the ones a gc superseded 7 days ago (or now, with --now) | |
| 367 | #[arg(long, value_name = "NAME")] | |
| 368 | remote: Option<String>, | |
| 369 | }, | |
| 370 | /// List operations | |
| 371 | #[command(name = "op")] | |
| 372 | Op { | |
| 373 | #[command(subcommand)] | |
| 374 | cmd: OpCmd, | |
| 375 | }, | |
| 376 | /// Take the advisory local lock on a lockable file, making it writable | |
| 377 | Lock { | |
| 378 | /// The lockable file | |
| 379 | path: String, | |
| 380 | }, | |
| 381 | /// Release the local lock and make the file read-only again | |
| 382 | Unlock { | |
| 383 | /// The locked file | |
| 384 | path: String, | |
| 385 | }, | |
| 386 | /// Workspaces: extra checkout directories sharing this repository | |
| 387 | Workspace { | |
| 388 | #[command(subcommand)] | |
| 389 | cmd: WorkspaceCmd, | |
| 390 | }, | |
| 391 | /// Bookmarks: named states; each workspace works on one | |
| 392 | Bookmark { | |
| 393 | #[command(subcommand)] | |
| 394 | cmd: BookmarkCmd, | |
| 395 | }, | |
| 396 | /// Union another bookmark's heads into the current one (no merge change): | |
| 397 | /// a bookmark here, or a remote's (`origin/develop`) as last fetched | |
| 398 | Merge { | |
| 399 | /// The bookmark whose heads to add | |
| 400 | bookmark: String, | |
| 401 | }, | |
| 402 | /// Sign in to a hub once, for every repository on it | |
| 403 | Auth { | |
| 404 | #[command(subcommand)] | |
| 405 | cmd: AuthCmd, | |
| 406 | }, | |
| 407 | /// Subrepositories: nested repositories pinned to a state | |
| 408 | Submodule { | |
| 409 | #[command(subcommand)] | |
| 410 | cmd: SubmoduleCmd, | |
| 411 | }, | |
| 412 | /// Witness hubs' transparency logs: cosign each checkpoint that extends | |
| 413 | /// the last one cosigned (spec §14.8) | |
| 414 | Witness { | |
| 415 | #[command(subcommand)] | |
| 416 | cmd: WitnessCmd, | |
| 417 | }, | |
| 418 | /// Hubs' transparency logs: check that a hub shows everyone the same | |
| 419 | /// history (spec §14.8) | |
| 420 | Tlog { | |
| 421 | #[command(subcommand)] | |
| 422 | cmd: TlogCmd, | |
| 423 | }, | |
| 424 | /// Artifacts: a directory packed into a tree of this repository's chunks (Cylinder, docs/cylinder.md) | |
| 425 | Artifact { | |
| 426 | #[command(subcommand)] | |
| 427 | cmd: ArtifactCmd, | |
| 428 | }, | |
| 429 | /// Execution receipts: signed records of what a runner ran on a state | |
| 430 | Receipt { | |
| 431 | #[command(subcommand)] | |
| 432 | cmd: ReceiptCmd, | |
| 433 | }, | |
| 434 | /// Tags: immutable named states | |
| 435 | Tag { | |
| 436 | #[command(subcommand)] | |
| 437 | cmd: TagCmd, | |
| 438 | }, | |
| 439 | /// Manage remotes | |
| 440 | Remote { | |
| 441 | #[command(subcommand)] | |
| 442 | cmd: RemoteCmd, | |
| 443 | }, | |
| 444 | /// Fetch every bookmark of a remote into its tracking references | |
| 445 | Fetch { | |
| 446 | /// Remote name (default: origin) | |
| 447 | remote: Option<String>, | |
| 448 | /// Machine-readable output: one JSON document on stdout | |
| 449 | #[arg(long)] | |
| 450 | json: bool, | |
| 451 | }, | |
| 452 | /// Fetch, then union the remote bookmark into the local one (no merge change) | |
| 453 | Pull { | |
| 454 | /// Remote name (default: origin) | |
| 455 | remote: Option<String>, | |
| 456 | /// Machine-readable output: one JSON document on stdout | |
| 457 | #[arg(long)] | |
| 458 | json: bool, | |
| 459 | }, | |
| 460 | /// Push the current bookmark to a remote | |
| 461 | Push { | |
| 462 | /// Remote name (default: origin) | |
| 463 | remote: Option<String>, | |
| 464 | /// Allow dropping heads the remote has | |
| 465 | #[arg(long)] | |
| 466 | force: bool, | |
| 467 | /// Push this bookmark instead of the workspace's current one | |
| 468 | #[arg(short = 'b', long)] | |
| 469 | bookmark: Option<String>, | |
| 470 | /// Delete this bookmark on the remote (never its default bookmark); | |
| 471 | /// the local bookmark is kept. `tags/NAME` withdraws a tag there | |
| 472 | /// (and here, as `oxy tag delete` does) | |
| 473 | #[arg(long, value_name = "NAME", conflicts_with_all = ["bookmark", "force"])] | |
| 474 | delete: Option<String>, | |
| 475 | /// Machine-readable output: one JSON document on stdout | |
| 476 | #[arg(long)] | |
| 477 | json: bool, | |
| 478 | }, | |
| 479 | /// Serve repositories under ROOT over HTTPS (with --tls-cert/--tls-key) or plain HTTP | |
| 480 | Serve { | |
| 481 | /// Directory with one repository per subdirectory | |
| 482 | #[arg(long, default_value = ".")] | |
| 483 | root: PathBuf, | |
| 484 | /// Address and port to listen on | |
| 485 | #[arg(long, default_value = "127.0.0.1:8890")] | |
| 486 | bind: String, | |
| 487 | /// Bearer token allowed to push (repeatable); none means open | |
| 488 | #[arg(long = "token")] | |
| 489 | tokens: Vec<String>, | |
| 490 | /// PEM certificate chain; enables HTTPS together with --tls-key | |
| 491 | #[arg(long, requires = "tls_key")] | |
| 492 | tls_cert: Option<PathBuf>, | |
| 493 | /// PEM private key | |
| 494 | #[arg(long, requires = "tls_cert")] | |
| 495 | tls_key: Option<PathBuf>, | |
| 496 | }, | |
| 497 | /// Manage the signing identity | |
| 498 | Identity { | |
| 499 | #[command(subcommand)] | |
| 500 | cmd: IdentityCmd, | |
| 501 | }, | |
| 502 | /// Seal changes with your key: named ones, or every change on the current bookmark you can authenticate but have not | |
| 503 | Seal { | |
| 504 | /// Changes to seal (default: every change on the bookmark you can authenticate and have not sealed) | |
| 505 | changes: Vec<String>, | |
| 506 | /// Seal every change of a hub proposal and push the seal (your approval) | |
| 507 | #[arg(long, conflicts_with = "changes")] | |
| 508 | proposal: Option<String>, | |
| 509 | /// Approve a job of a hub run that waits for a sealed approval: `--flow https://hub/OWNER/NAME/flow/N --job apply` | |
| 510 | #[arg(long, conflicts_with_all = ["changes", "proposal"], requires = "job")] | |
| 511 | flow: Option<String>, | |
| 512 | /// The job to approve, with --flow | |
| 513 | #[arg(long, requires = "flow")] | |
| 514 | job: Option<String>, | |
| 515 | /// Reject the job instead of approving it, with --flow | |
| 516 | #[arg(long, requires = "flow")] | |
| 517 | reject: bool, | |
| 518 | /// Machine-readable output: one JSON document on stdout | |
| 519 | #[arg(long)] | |
| 520 | json: bool, | |
| 521 | }, | |
| 522 | /// Hub proposals: show one, or land it from this client | |
| 523 | Proposal { | |
| 524 | #[command(subcommand)] | |
| 525 | cmd: ProposalCmd, | |
| 526 | }, | |
| 527 | /// Verify the signatures on a change (default: every change in the log) | |
| 528 | Verify { | |
| 529 | /// The change to verify (default: every change in the log) | |
| 530 | change: Option<String>, | |
| 531 | /// Verify every change a revset selects: `release..main & unsigned()` | |
| 532 | #[arg(short = 'r', long, value_name = "REVSET", conflicts_with = "change")] | |
| 533 | revset: Option<String>, | |
| 534 | /// Machine-readable output: one JSON document on stdout | |
| 535 | #[arg(long)] | |
| 536 | json: bool, | |
| 537 | }, | |
| 538 | /// Git interoperability | |
| 539 | Git { | |
| 540 | #[command(subcommand)] | |
| 541 | cmd: GitCmd, | |
| 542 | }, | |
| 543 | /// Manage the trust policy (who may update which bookmarks) | |
| 544 | Policy { | |
| 545 | #[command(subcommand)] | |
| 546 | cmd: PolicyCmd, | |
| 547 | }, | |
| 548 | /// Set a configuration key (author.*, core.sign, core.op-retention, core.gc-grace, core.checkpoint-interval, filter.max-blob, filter.paths, storage.cache, workspace.bookmark, remote.<name>.url|token|ca, git.claim, git.colocate, auth.helper with --user) | |
| 549 | Config { | |
| 550 | /// The key, such as author.name or remote.origin.token | |
| 551 | key: String, | |
| 552 | /// The value to set (none with --unset); `-` reads it from standard input, out of the shell's history | |
| 553 | #[arg(required_unless_present = "unset")] | |
| 554 | value: Option<String>, | |
| 555 | /// Write to the user file (~/.config/oxy/config.toml) instead of this repository | |
| 556 | #[arg(long)] | |
| 557 | user: bool, | |
| 558 | /// Remove the key from this repository's file (or the user file with --user) | |
| 559 | #[arg(long)] | |
| 560 | unset: bool, | |
| 561 | }, | |
| 562 | } | |
| 563 | ||
| 564 | #[derive(Subcommand)] | |
| 565 | enum OpCmd { | |
| 566 | /// List operations, newest first, with what each changed | |
| 567 | Log { | |
| 568 | /// Show only the latest N (snapshots folded together count as one) | |
| 569 | #[arg(short = 'n', long)] | |
| 570 | limit: Option<usize>, | |
| 571 | /// Machine-readable output: one JSON document on stdout | |
| 572 | #[arg(long)] | |
| 573 | json: bool, | |
| 574 | }, | |
| 575 | /// Go back to the bookmarks and working copy as they were after OP (recorded as a new operation, so it can be undone) | |
| 576 | Restore { | |
| 577 | /// The operation: an ID prefix from oxy op log, @ or @- | |
| 578 | op: String, | |
| 579 | /// Throw away uncommitted working-copy changes | |
| 580 | #[arg(long)] | |
| 581 | discard: bool, | |
| 582 | }, | |
| 583 | /// Undo OP alone, keeping every operation after it: the bookmarks it moved go back | |
| 584 | Revert { | |
| 585 | /// The operation: an ID prefix from oxy op log | |
| 586 | op: String, | |
| 587 | }, | |
| 588 | } | |
| 589 | ||
| 590 | #[derive(Subcommand)] | |
| 591 | enum WorkspaceCmd { | |
| 592 | /// Create a workspace at PATH checked out on CHANGE... (default: the bookmark heads) | |
| 593 | Add { | |
| 594 | /// Directory for the new workspace (must be empty or absent) | |
| 595 | path: PathBuf, | |
| 596 | /// Changes whose state to check out there | |
| 597 | changes: Vec<String>, | |
| 598 | }, | |
| 599 | /// List workspaces and their working-copy changes | |
| 600 | List { | |
| 601 | /// Machine-readable output: one JSON document on stdout | |
| 602 | #[arg(long)] | |
| 603 | json: bool, | |
| 604 | }, | |
| 605 | /// Stop tracking a workspace (by id prefix or path); its files stay | |
| 606 | Forget { | |
| 607 | /// Workspace id prefix or path | |
| 608 | workspace: String, | |
| 609 | }, | |
| 610 | } | |
| 611 | ||
| 612 | #[derive(Subcommand)] | |
| 613 | enum BisectCmd { | |
| 614 | /// Begin; remembers where the working copy was | |
| 615 | Start, | |
| 616 | /// Mark CHANGE... (or the state under test) as good | |
| 617 | Good { | |
| 618 | /// Changes known to be good (default: the state under test) | |
| 619 | changes: Vec<String>, | |
| 620 | }, | |
| 621 | /// Mark CHANGE... (or the state under test) as bad | |
| 622 | Bad { | |
| 623 | /// Changes known to be bad (default: the state under test) | |
| 624 | changes: Vec<String>, | |
| 625 | }, | |
| 626 | /// The state under test cannot be tested; try another | |
| 627 | Skip, | |
| 628 | /// Run a command for each step: exit 0 good, 125 skip, else bad | |
| 629 | Run { | |
| 630 | /// Command and arguments to run at each step | |
| 631 | #[arg(required = true, trailing_var_arg = true)] | |
| 632 | command: Vec<String>, | |
| 633 | }, | |
| 634 | /// Where the search stands | |
| 635 | Status { | |
| 636 | /// Machine-readable output: one JSON document on stdout | |
| 637 | #[arg(long)] | |
| 638 | json: bool, | |
| 639 | }, | |
| 640 | /// Restore the working copy and forget the search | |
| 641 | Reset, | |
| 642 | } | |
| 643 | ||
| 644 | #[derive(Subcommand)] | |
| 645 | enum BookmarkCmd { | |
| 646 | /// Create a bookmark at CHANGE... (default: the working copy's base) | |
| 647 | Create { | |
| 648 | /// Name of the new bookmark | |
| 649 | name: String, | |
| 650 | /// Its heads (default: the working copy's base). A state is its | |
| 651 | /// heads and what they depend on: a change alone may leave out | |
| 652 | /// earlier changes of the current state, which is refused unless | |
| 653 | /// --upto or --only says which is meant | |
| 654 | changes: Vec<String>, | |
| 655 | /// Everything of the current state up to CHANGE..., as a Git | |
| 656 | /// branch at a commit would hold | |
| 657 | #[arg(long, conflicts_with = "only")] | |
| 658 | upto: bool, | |
| 659 | /// Just CHANGE... and what they depend on. Other changes to the | |
| 660 | /// same files are left out, so a file can come out partly edited; | |
| 661 | /// to add one change to a bookmark's work, give the bookmark's | |
| 662 | /// heads with it instead: `bookmark set NAME <its heads> CHANGE` | |
| 663 | #[arg(long)] | |
| 664 | only: bool, | |
| 665 | }, | |
| 666 | /// Point an existing bookmark at CHANGE... | |
| 667 | Set { | |
| 668 | /// The bookmark to move | |
| 669 | name: String, | |
| 670 | /// Its new heads. A change alone may leave out changes the | |
| 671 | /// bookmark now holds that were recorded before it, which is | |
| 672 | /// refused unless --upto or --only says which is meant | |
| 673 | changes: Vec<String>, | |
| 674 | /// Everything the bookmark holds up to CHANGE..., as a Git branch | |
| 675 | /// moved to a commit would hold | |
| 676 | #[arg(long, conflicts_with = "only")] | |
| 677 | upto: bool, | |
| 678 | /// Just CHANGE... and what they depend on. Other changes to the | |
| 679 | /// same files are left out, so a file can come out partly edited; | |
| 680 | /// to add one change to a bookmark's work, give the bookmark's | |
| 681 | /// heads with it instead: `bookmark set NAME <its heads> CHANGE` | |
| 682 | #[arg(long)] | |
| 683 | only: bool, | |
| 684 | }, | |
| 685 | /// Remove a bookmark; its changes stay | |
| 686 | Delete { | |
| 687 | /// The bookmark to remove | |
| 688 | name: String, | |
| 689 | }, | |
| 690 | /// List bookmarks with their heads and difference from origin | |
| 691 | List { | |
| 692 | /// Also list remotes' bookmarks that have no local bookmark | |
| 693 | #[arg(long, short)] | |
| 694 | all: bool, | |
| 695 | /// Each head of every bookmark, as `bookmark show` prints them | |
| 696 | #[arg(long, short)] | |
| 697 | verbose: bool, | |
| 698 | /// Machine-readable output: one JSON document on stdout | |
| 699 | #[arg(long)] | |
| 700 | json: bool, | |
| 701 | }, | |
| 702 | /// A bookmark's heads: each change nothing else on it depends on, | |
| 703 | /// newest first. A bookmark name is also a revset for its heads | |
| 704 | /// (`oxy log -r NAME`, or `bookmark(NAME)` to mean only the bookmark) | |
| 705 | Show { | |
| 706 | /// The bookmark, or REMOTE/NAME (default: the current one) | |
| 707 | name: Option<String>, | |
| 708 | /// Machine-readable output: one JSON document on stdout | |
| 709 | #[arg(long)] | |
| 710 | json: bool, | |
| 711 | }, | |
| 712 | /// Make NAME this workspace's bookmark and check it out. Uncommitted work stops it; --carry takes the work along, --discard drops it | |
| 713 | Switch { | |
| 714 | /// The bookmark to work on; one only a remote has (NAME or REMOTE/NAME) is created from it | |
| 715 | name: String, | |
| 716 | /// Throw away uncommitted working-copy changes | |
| 717 | #[arg(long)] | |
| 718 | discard: bool, | |
| 719 | /// Take uncommitted changes along; refused when NAME has other | |
| 720 | /// versions of the files they touch | |
| 721 | #[arg(long, conflicts_with = "discard")] | |
| 722 | carry: bool, | |
| 723 | }, | |
| 724 | } | |
| 725 | ||
| 726 | #[derive(Subcommand)] | |
| 727 | enum WitnessCmd { | |
| 728 | /// Serve a witness (C2SP tlog-witness: POST /add-checkpoint), signing | |
| 729 | /// with this oxy home's key (author.key) | |
| 730 | Serve { | |
| 731 | /// The witness's name in cosignatures: a host name | |
| 732 | #[arg(long)] | |
| 733 | name: String, | |
| 734 | /// A log to witness, as ORIGIN=FILE: its origin (HOST/log) and its | |
| 735 | /// key's identity file (a hub's /.well-known/oxyhub-key); repeatable | |
| 736 | #[arg(long = "log", required = true)] | |
| 737 | logs: Vec<String>, | |
| 738 | /// Where to keep what it cosigned | |
| 739 | #[arg(long, default_value = "witness-state")] | |
| 740 | state: PathBuf, | |
| 741 | /// Address and port to listen on | |
| 742 | #[arg(long, default_value = "127.0.0.1:8891")] | |
| 743 | bind: String, | |
| 744 | /// PEM certificate chain; enables HTTPS together with --tls-key | |
| 745 | #[arg(long, requires = "tls_key")] | |
| 746 | tls_cert: Option<PathBuf>, | |
| 747 | /// PEM private key for --tls-cert | |
| 748 | #[arg(long, requires = "tls_cert")] | |
| 749 | tls_key: Option<PathBuf>, | |
| 750 | }, | |
| 751 | } | |
| 752 | ||
| 753 | #[derive(Subcommand)] | |
| 754 | enum TlogCmd { | |
| 755 | /// Check a hub's whole log: its checkpoint against the log key and the | |
| 756 | /// last checkpoint seen, and every entry against the root it signed; | |
| 757 | /// then list this repository's entries (or a key's, or all) | |
| 758 | Verify { | |
| 759 | /// The hub, as https://oxygenhub.io (default: this checkout's remote) | |
| 760 | hub: Option<String>, | |
| 761 | /// List the entries a key signed (a KeyId) instead | |
| 762 | #[arg(long, conflicts_with = "all")] | |
| 763 | key: Option<String>, | |
| 764 | /// List every entry | |
| 765 | #[arg(long)] | |
| 766 | all: bool, | |
| 767 | /// Machine-readable output | |
| 768 | #[arg(long)] | |
| 769 | json: bool, | |
| 770 | }, | |
| 771 | } | |
| 772 | ||
| 773 | #[derive(Subcommand)] | |
| 774 | enum AuthCmd { | |
| 775 | /// Sign in to HUB in the browser (device sign-in) and keep the token for | |
| 776 | /// every repository there | |
| 777 | Login { | |
| 778 | /// The hub, as https://oxygenhub.io or oxygenhub.io (default: this checkout's origin) | |
| 779 | hub: Option<String>, | |
| 780 | /// Read a token from standard input instead (for CI and scripts) | |
| 781 | #[arg(long)] | |
| 782 | with_token: bool, | |
| 783 | /// PEM file of extra roots to trust (a hub with a self-signed certificate) | |
| 784 | #[arg(long)] | |
| 785 | ca: Option<PathBuf>, | |
| 786 | }, | |
| 787 | /// List the hubs you are signed in to, checking each token with its hub | |
| 788 | Status, | |
| 789 | /// Forget HUB's token and revoke it on the hub | |
| 790 | Logout { | |
| 791 | /// The hub (default: this checkout's origin, or the only one) | |
| 792 | hub: Option<String>, | |
| 793 | }, | |
| 794 | /// Keep an agent grant token (from `oh agent grant`) for keyless | |
| 795 | /// signing: with `author.signer = keyless`, commits and pushes are | |
| 796 | /// signed by fresh keys the hub certifies, in the grantor's name | |
| 797 | /// (the token is read from standard input, so it stays out of the | |
| 798 | /// shell's history) | |
| 799 | Grant { | |
| 800 | /// The hub (default: this checkout's origin) | |
| 801 | hub: Option<String>, | |
| 802 | }, | |
| 803 | } | |
| 804 | ||
| 805 | #[derive(Subcommand)] | |
| 806 | enum SubmoduleCmd { | |
| 807 | /// Clone URL (Oxygen or Git) into PATH and register it in .oxymodules | |
| 808 | Add { | |
| 809 | /// URL or path of the Oxygen or Git repository | |
| 810 | url: String, | |
| 811 | /// Where to put it, relative to the repository root | |
| 812 | path: String, | |
| 813 | /// Bookmark (Oxygen) or branch (Git) to check out and track | |
| 814 | #[arg(long)] | |
| 815 | bookmark: Option<String>, | |
| 816 | }, | |
| 817 | /// Bring nested repositories to their pins | |
| 818 | Update { | |
| 819 | /// Clone nested repositories that are absent | |
| 820 | #[arg(long)] | |
| 821 | init: bool, | |
| 822 | /// Subrepos to update (default: all) | |
| 823 | paths: Vec<String>, | |
| 824 | }, | |
| 825 | /// List subrepos with their pins and on-disk state | |
| 826 | Status { | |
| 827 | /// Machine-readable output: one JSON document on stdout | |
| 828 | #[arg(long)] | |
| 829 | json: bool, | |
| 830 | }, | |
| 831 | /// Unregister PATH; the directory stays, the node goes with the next commit | |
| 832 | Remove { | |
| 833 | /// The subrepo to unregister | |
| 834 | path: String, | |
| 835 | /// Delete its directory too (#164); refused while it has uncommitted edits | |
| 836 | #[arg(long)] | |
| 837 | delete: bool, | |
| 838 | /// With --delete: delete it even with uncommitted edits | |
| 839 | #[arg(long, requires = "delete")] | |
| 840 | force: bool, | |
| 841 | }, | |
| 842 | } | |
| 843 | ||
| 844 | #[derive(Subcommand)] | |
| 845 | enum ArtifactCmd { | |
| 846 | /// Pack DIR into an artifact: each file a blob of this repository's store, so files and chunks it holds cost nothing | |
| 847 | Pack { | |
| 848 | /// The directory whose files make the artifact | |
| 849 | dir: std::path::PathBuf, | |
| 850 | /// container, runner-image, package, archive or report | |
| 851 | #[arg(long, default_value = "archive")] | |
| 852 | kind: String, | |
| 853 | /// x86_64, riscv64, aarch64… | |
| 854 | #[arg(long)] | |
| 855 | arch: Option<String>, | |
| 856 | /// linux, darwin, none… | |
| 857 | #[arg(long)] | |
| 858 | os: Option<String>, | |
| 859 | /// The default command, one word per flag | |
| 860 | #[arg(long = "entrypoint", value_name = "WORD")] | |
| 861 | entrypoint: Vec<String>, | |
| 862 | /// KEY=VALUE for the environment (repeatable) | |
| 863 | #[arg(long = "env", value_name = "KEY=VALUE")] | |
| 864 | env: Vec<String>, | |
| 865 | /// KEY=VALUE metadata (repeatable) | |
| 866 | #[arg(long = "label", value_name = "KEY=VALUE")] | |
| 867 | labels: Vec<String>, | |
| 868 | /// Machine-readable output: one JSON document on stdout | |
| 869 | #[arg(long)] | |
| 870 | json: bool, | |
| 871 | }, | |
| 872 | /// The artifacts kept here | |
| 873 | List { | |
| 874 | /// Machine-readable output: one JSON document on stdout | |
| 875 | #[arg(long)] | |
| 876 | json: bool, | |
| 877 | }, | |
| 878 | /// One artifact and its files | |
| 879 | Show { | |
| 880 | /// Its name, or a unique prefix of at least six characters | |
| 881 | name: String, | |
| 882 | /// Machine-readable output: one JSON document on stdout | |
| 883 | #[arg(long)] | |
| 884 | json: bool, | |
| 885 | }, | |
| 886 | /// Write an artifact's files into DEST (missing or empty) | |
| 887 | Unpack { | |
| 888 | /// Its name, or a unique prefix of at least six characters | |
| 889 | name: String, | |
| 890 | /// Where to write its files | |
| 891 | dest: std::path::PathBuf, | |
| 892 | }, | |
| 893 | /// Check an artifact's name, tree and every file against the store | |
| 894 | Verify { | |
| 895 | /// Its name, or a unique prefix of at least six characters | |
| 896 | name: String, | |
| 897 | }, | |
| 898 | } | |
| 899 | ||
| 900 | #[derive(Subcommand)] | |
| 901 | enum ReceiptCmd { | |
| 902 | /// The receipts kept for a state (default: the bookmark heads) | |
| 903 | List { | |
| 904 | /// Change, bookmark or tag whose state to look at | |
| 905 | rev: Option<String>, | |
| 906 | /// Machine-readable output: one JSON document on stdout | |
| 907 | #[arg(long)] | |
| 908 | json: bool, | |
| 909 | }, | |
| 910 | /// One receipt in full, and whether its signature verifies | |
| 911 | Show { | |
| 912 | /// The receipt (a prefix of its hash, as `list` shows it) | |
| 913 | receipt: String, | |
| 914 | /// Change, bookmark or tag whose state holds it | |
| 915 | #[arg(long)] | |
| 916 | rev: Option<String>, | |
| 917 | /// Machine-readable output: one JSON document on stdout | |
| 918 | #[arg(long)] | |
| 919 | json: bool, | |
| 920 | }, | |
| 921 | /// Write a signed receipt to FILE | |
| 922 | Export { | |
| 923 | /// The receipt (a prefix of its hash, as `list` shows it) | |
| 924 | receipt: String, | |
| 925 | /// Where to write it | |
| 926 | file: String, | |
| 927 | /// Change, bookmark or tag whose state holds it | |
| 928 | #[arg(long)] | |
| 929 | rev: Option<String>, | |
| 930 | /// `oxygen`: the runner's signed receipt, as `verify` and `import` | |
| 931 | /// read it. `slsa`: SLSA provenance in a DSSE envelope, signed with | |
| 932 | /// your key, for in-toto and cosign (`oxy identity export --pem` | |
| 933 | /// gives the key they check it with) | |
| 934 | #[arg(long, default_value = "oxygen", value_parser = ["oxygen", "slsa"])] | |
| 935 | format: String, | |
| 936 | }, | |
| 937 | /// Check a signed receipt in FILE without keeping it | |
| 938 | Verify { | |
| 939 | /// A signed receipt, as a runner or `export` writes it | |
| 940 | file: String, | |
| 941 | /// Machine-readable output: one JSON document on stdout | |
| 942 | #[arg(long)] | |
| 943 | json: bool, | |
| 944 | }, | |
| 945 | /// Verify a signed receipt in FILE and keep it with its state | |
| 946 | Import { | |
| 947 | /// A signed receipt, as a runner or `export` writes it | |
| 948 | file: String, | |
| 949 | }, | |
| 950 | } | |
| 951 | ||
| 952 | #[derive(Subcommand)] | |
| 953 | enum TagCmd { | |
| 954 | /// Tag the state of CHANGE... (default: the bookmark heads) | |
| 955 | Create { | |
| 956 | /// Name of the tag; tags never move | |
| 957 | name: String, | |
| 958 | /// Changes whose state to tag (default: the bookmark heads) | |
| 959 | changes: Vec<String>, | |
| 960 | /// A message recorded with the tag | |
| 961 | #[arg(short, long, default_value = "")] | |
| 962 | message: String, | |
| 963 | }, | |
| 964 | /// Withdraw a tag: a signed withdrawal, kept beside the tag's creation; | |
| 965 | /// `oxy push` sends it to a remote that has the tag (the name is never | |
| 966 | /// used again) | |
| 967 | Delete { | |
| 968 | /// Name of the tag | |
| 969 | name: String, | |
| 970 | /// Why it is withdrawn, recorded with the withdrawal | |
| 971 | #[arg(short, long, default_value = "")] | |
| 972 | reason: String, | |
| 973 | }, | |
| 974 | /// List tags and the states they name | |
| 975 | List { | |
| 976 | /// Machine-readable output: one JSON document on stdout | |
| 977 | #[arg(long)] | |
| 978 | json: bool, | |
| 979 | }, | |
| 980 | } | |
| 981 | ||
| 982 | #[derive(Subcommand)] | |
| 983 | enum RemoteCmd { | |
| 984 | /// Add a remote | |
| 985 | Add { | |
| 986 | /// Name to refer to the remote by, such as origin | |
| 987 | name: String, | |
| 988 | /// A local path or the http(s) URL of a hub repository; or a store the repository is kept on: s3://BUCKET/PREFIX, gs://BUCKET/PREFIX, az://ACCOUNT/CONTAINER/PREFIX, dir://PATH | |
| 989 | url: String, | |
| 990 | /// For an s3:// store: the AWS profile to use (default: AWS_PROFILE, else "default") | |
| 991 | #[arg(long)] | |
| 992 | profile: Option<String>, | |
| 993 | /// For an s3:// store: its region (default: the environment, the profile, else asked of S3) | |
| 994 | #[arg(long)] | |
| 995 | region: Option<String>, | |
| 996 | /// For an s3://, gs:// or az:// store: another endpoint, such as an S3-compatible service | |
| 997 | #[arg(long)] | |
| 998 | endpoint: Option<String>, | |
| 999 | /// For a gs:// store: a service account key file, or application default credentials | |
| 1000 | #[arg(long)] | |
| 1001 | credentials: Option<String>, | |
| 1002 | }, | |
| 1003 | /// Check a remote: that it is reachable with the credentials in hand, what it holds, and (for a store) that it behaves | |
| 1004 | Check { | |
| 1005 | /// The remote to check | |
| 1006 | name: String, | |
| 1007 | }, | |
| 1008 | /// Remove a remote | |
| 1009 | Remove { | |
| 1010 | /// The remote to remove | |
| 1011 | name: String, | |
| 1012 | }, | |
| 1013 | /// List remotes and their URLs | |
| 1014 | List { | |
| 1015 | /// Machine-readable output: one JSON document on stdout | |
| 1016 | #[arg(long)] | |
| 1017 | json: bool, | |
| 1018 | }, | |
| 1019 | } | |
| 1020 | ||
| 1021 | #[derive(Subcommand)] | |
| 1022 | enum ProposalCmd { | |
| 1023 | /// Show a proposal by its hub URL | |
| 1024 | Show { | |
| 1025 | /// The proposal's URL on the hub | |
| 1026 | url: String, | |
| 1027 | /// Machine-readable output: one JSON document on stdout | |
| 1028 | #[arg(long)] | |
| 1029 | json: bool, | |
| 1030 | }, | |
| 1031 | /// Fetch the proposal's changes, set the target bookmark to the projected state, and push with your key | |
| 1032 | Land { | |
| 1033 | /// The proposal's URL on the hub | |
| 1034 | url: String, | |
| 1035 | /// Land even when the hub reports blockers | |
| 1036 | #[arg(long)] | |
| 1037 | force: bool, | |
| 1038 | }, | |
| 1039 | } | |
| 1040 | ||
| 1041 | #[derive(Subcommand)] | |
| 1042 | enum IdentityCmd { | |
| 1043 | /// Generate a composite Ed25519 + ML-DSA-65 key pair and make it your author identity | |
| 1044 | Create { | |
| 1045 | /// Your name (default: author.name) | |
| 1046 | #[arg(long)] | |
| 1047 | name: Option<String>, | |
| 1048 | /// Your email (default: author.email) | |
| 1049 | #[arg(long)] | |
| 1050 | email: Option<String>, | |
| 1051 | /// Record the author in this repository only, not in the user config | |
| 1052 | #[arg(long)] | |
| 1053 | local: bool, | |
| 1054 | /// Expire the identity this many days from now | |
| 1055 | #[arg(long)] | |
| 1056 | expires: Option<u64>, | |
| 1057 | }, | |
| 1058 | /// Show the configured identity | |
| 1059 | Show, | |
| 1060 | /// Write your identity object to a file, to hand to a policy holder | |
| 1061 | Export { | |
| 1062 | /// File to write (default: <key>.identity, or <key>.pem with --pem, in the current directory) | |
| 1063 | path: Option<PathBuf>, | |
| 1064 | /// Write the Ed25519 public key as PEM instead, as cosign's --key and | |
| 1065 | /// other DSSE verifiers take it | |
| 1066 | #[arg(long)] | |
| 1067 | pem: bool, | |
| 1068 | }, | |
| 1069 | /// Sign a host's challenge (hex, at least 16 bytes) to prove you hold your key; prints the signature as hex | |
| 1070 | SignChallenge { | |
| 1071 | /// The host's challenge, as hex | |
| 1072 | challenge: String, | |
| 1073 | /// Machine-readable output: one JSON document on stdout | |
| 1074 | #[arg(long)] | |
| 1075 | json: bool, | |
| 1076 | }, | |
| 1077 | /// Move to a fresh key pair; the policy follows if it names your key | |
| 1078 | Rotate, | |
| 1079 | /// Re-issue your identity with a new expiry (in days from now; 0 = never) | |
| 1080 | Renew { | |
| 1081 | /// Days from now until it expires; 0 for never | |
| 1082 | #[arg(long)] | |
| 1083 | expires: u64, | |
| 1084 | }, | |
| 1085 | /// Revoke a key: yours by default, or any key if you hold a policy root | |
| 1086 | Revoke { | |
| 1087 | /// KeyId prefix (default: your own key) | |
| 1088 | key: Option<String>, | |
| 1089 | /// Why, recorded in the revocation | |
| 1090 | #[arg(long, default_value = "")] | |
| 1091 | reason: String, | |
| 1092 | }, | |
| 1093 | /// Keys allowed to sign or seal on your behalf | |
| 1094 | Delegate { | |
| 1095 | #[command(subcommand)] | |
| 1096 | cmd: DelegateCmd, | |
| 1097 | }, | |
| 1098 | } | |
| 1099 | ||
| 1100 | #[derive(Subcommand)] | |
| 1101 | enum DelegateCmd { | |
| 1102 | /// Add a delegate from an exported identity file | |
| 1103 | Add { | |
| 1104 | /// The delegates' exported identity files: one rotation for all of them | |
| 1105 | #[arg(required = true)] | |
| 1106 | identity_file: Vec<PathBuf>, | |
| 1107 | /// What it may do for you: changes, refs, seal (comma-separated) or all. | |
| 1108 | /// An agent gets `changes`; a host that lands for you needs `changes,refs` | |
| 1109 | #[arg(long, default_value = "changes")] | |
| 1110 | scope: String, | |
| 1111 | /// How long for: 30m, 8h, 7d, or a number of days (default: until removed) | |
| 1112 | #[arg(long)] | |
| 1113 | expires: Option<String>, | |
| 1114 | }, | |
| 1115 | /// Remove a delegate by KeyId prefix | |
| 1116 | Remove { | |
| 1117 | /// KeyId prefix of the delegate | |
| 1118 | key: String, | |
| 1119 | }, | |
| 1120 | /// List delegates | |
| 1121 | List, | |
| 1122 | } | |
| 1123 | ||
| 1124 | #[derive(Subcommand)] | |
| 1125 | enum GitCmd { | |
| 1126 | /// Keep a .git beside .oxy, up to date after every oxy command, so editors' Git features work here (one way: oxy to Git) | |
| 1127 | Colocate { | |
| 1128 | /// Stop updating it (the .git is left as it is) | |
| 1129 | #[arg(long)] | |
| 1130 | off: bool, | |
| 1131 | }, | |
| 1132 | /// Export the current bookmark to a Git repository (created bare if absent) | |
| 1133 | Export { | |
| 1134 | /// Git repository to write to (created bare if absent) | |
| 1135 | git_dir: PathBuf, | |
| 1136 | /// Git branch to write (default: the bookmark name) | |
| 1137 | #[arg(long)] | |
| 1138 | branch: Option<String>, | |
| 1139 | }, | |
| 1140 | /// Fetch a branch from a Git remote (path or URL) into the current bookmark | |
| 1141 | Fetch { | |
| 1142 | /// Git remote: a path or URL | |
| 1143 | url: String, | |
| 1144 | /// Branch to fetch | |
| 1145 | #[arg(long, default_value = "main")] | |
| 1146 | branch: String, | |
| 1147 | /// Author your own commits (Git author email = your identity's) with your key; sets git.claim | |
| 1148 | #[arg(long)] | |
| 1149 | claim: bool, | |
| 1150 | }, | |
| 1151 | /// Push the current bookmark to a branch of a Git remote (fetch first) | |
| 1152 | Push { | |
| 1153 | /// Git remote: a path or URL | |
| 1154 | url: String, | |
| 1155 | /// Branch to push to | |
| 1156 | #[arg(long, default_value = "main")] | |
| 1157 | branch: String, | |
| 1158 | }, | |
| 1159 | /// Recover an import that stopped before it moved the bookmark: point the bookmark at every imported commit's change in the store, so oxy git fetch carries on from there | |
| 1160 | Recover, | |
| 1161 | /// Check this repository's import of a Git branch: every commit's tree must be reproduced exactly | |
| 1162 | Verify { | |
| 1163 | /// The Git repository the import came from | |
| 1164 | git_dir: PathBuf, | |
| 1165 | /// Branch that was imported | |
| 1166 | #[arg(long, default_value = "main")] | |
| 1167 | branch: String, | |
| 1168 | /// Rebuild only the last N commits (the others are followed, not checked) | |
| 1169 | #[arg(long, value_name = "N")] | |
| 1170 | last: Option<usize>, | |
| 1171 | /// Stop after N differences (0: check every commit whatever it finds) | |
| 1172 | #[arg(long, value_name = "N", default_value_t = 50)] | |
| 1173 | max_differences: usize, | |
| 1174 | }, | |
| 1175 | /// Import a Git branch into a new Oxygen repository at DEST | |
| 1176 | Import { | |
| 1177 | /// Git repository to read | |
| 1178 | git_dir: PathBuf, | |
| 1179 | /// Directory for the new Oxygen repository | |
| 1180 | dest: PathBuf, | |
| 1181 | /// Branch to import | |
| 1182 | #[arg(long, default_value = "main")] | |
| 1183 | branch: String, | |
| 1184 | /// Author your own commits (Git author email = your identity's) with your key; sets git.claim | |
| 1185 | #[arg(long)] | |
| 1186 | claim: bool, | |
| 1187 | }, | |
| 1188 | } | |
| 1189 | ||
| 1190 | #[derive(Subcommand)] | |
| 1191 | enum PolicyCmd { | |
| 1192 | /// Create the initial policy with your identity as the only root | |
| 1193 | Init, | |
| 1194 | /// Show the current policy | |
| 1195 | Show { | |
| 1196 | /// Machine-readable output: one JSON document on stdout | |
| 1197 | #[arg(long)] | |
| 1198 | json: bool, | |
| 1199 | }, | |
| 1200 | /// Allow an identity (from `oxy identity export`) to update bookmarks | |
| 1201 | Grant { | |
| 1202 | /// The identity to grant, exported with `oxy identity export` | |
| 1203 | identity_file: PathBuf, | |
| 1204 | /// Reference pattern the grant applies to (default: every bookmark) | |
| 1205 | #[arg(long)] | |
| 1206 | pattern: Option<String>, | |
| 1207 | /// Where to write the proposal when more root signatures are needed | |
| 1208 | #[arg(long)] | |
| 1209 | out: Option<PathBuf>, | |
| 1210 | }, | |
| 1211 | /// Allow holders of an issuer's certificates for SUBJECT to sign (keyless) | |
| 1212 | GrantSubject { | |
| 1213 | /// Who: `repo:OWNER/NAME:bookmark:main`, `agent:NAME@ACCOUNT`; `*` matches any run of characters | |
| 1214 | subject: String, | |
| 1215 | /// The issuer's identity file, e.g. a hub's /.well-known/oxyhub-key | |
| 1216 | #[arg(long)] | |
| 1217 | issuer: PathBuf, | |
| 1218 | /// What its keys may sign: changes, refs, seal | |
| 1219 | #[arg(long, default_value = "changes,refs")] | |
| 1220 | scope: String, | |
| 1221 | /// Reference pattern the grant applies to (default: every bookmark) | |
| 1222 | #[arg(long)] | |
| 1223 | pattern: Option<String>, | |
| 1224 | /// Where to write the proposal when more root signatures are needed | |
| 1225 | #[arg(long)] | |
| 1226 | out: Option<PathBuf>, | |
| 1227 | }, | |
| 1228 | /// Revoke a key (KeyId prefix of an identity in the policy) | |
| 1229 | Revoke { | |
| 1230 | /// KeyId prefix of the identity | |
| 1231 | key: String, | |
| 1232 | /// Where to write the proposal when more root signatures are needed | |
| 1233 | #[arg(long)] | |
| 1234 | out: Option<PathBuf>, | |
| 1235 | }, | |
| 1236 | /// Manage root keys and the signature threshold for policy updates | |
| 1237 | Root { | |
| 1238 | #[command(subcommand)] | |
| 1239 | cmd: RootCmd, | |
| 1240 | }, | |
| 1241 | /// Add your root signature to a proposal file; applies it once the threshold is met | |
| 1242 | Sign { | |
| 1243 | /// The proposal file to sign | |
| 1244 | proposal: PathBuf, | |
| 1245 | /// Where to write the proposal if it still needs signatures (default: in place) | |
| 1246 | #[arg(long)] | |
| 1247 | out: Option<PathBuf>, | |
| 1248 | }, | |
| 1249 | } | |
| 1250 | ||
| 1251 | #[derive(Subcommand)] | |
| 1252 | enum RootCmd { | |
| 1253 | /// Make an exported identity a root key | |
| 1254 | Add { | |
| 1255 | /// The identity to make a root, exported with `oxy identity export` | |
| 1256 | identity_file: PathBuf, | |
| 1257 | /// Where to write the proposal when more root signatures are needed | |
| 1258 | #[arg(long)] | |
| 1259 | out: Option<PathBuf>, | |
| 1260 | }, | |
| 1261 | /// Remove a root key by KeyId prefix | |
| 1262 | Remove { | |
| 1263 | /// KeyId prefix of the root key | |
| 1264 | key: String, | |
| 1265 | /// Where to write the proposal when more root signatures are needed | |
| 1266 | #[arg(long)] | |
| 1267 | out: Option<PathBuf>, | |
| 1268 | }, | |
| 1269 | /// Require this many root signatures on policy updates | |
| 1270 | Threshold { | |
| 1271 | /// Number of root signatures | |
| 1272 | n: u32, | |
| 1273 | /// Where to write the proposal when more root signatures are needed | |
| 1274 | #[arg(long)] | |
| 1275 | out: Option<PathBuf>, | |
| 1276 | }, | |
| 1277 | } | |
| 1278 | ||
| 1279 | /// After a command, bring the side-by-side `.git` up to date when this | |
| 1280 | /// checkout keeps one (`git.colocate`). It never fails the command: a | |
| 1281 | /// problem is a warning, and the next command tries again. | |
| 1282 | 187× | pub fn after_command() { |
| 1283 | 187× | if AT_OP.get().and_then(|o| o.as_deref()).is_some() { |
| 1284 | 0× | return; |
| 1285 | 187× | } |
| 1286 | 187× | let Ok(dir) = std::env::current_dir() else { |
| 1287 | 0× | return; |
| 1288 | }; | |
| 1289 | 187× | let Ok(repo) = Repo::open(&dir) else { return }; |
| 1290 | 160× | if !repo.config.git_colocate || repo.is_bare() { |
| 1291 | 157× | return; |
| 1292 | 3× | } |
| 1293 | 3× | if let Err(e) = repo.git_colocate_sync() { |
| 1294 | 0× | eprintln!( |
| 1295 | 0× | "{} the .git beside this checkout was not updated: {e}", |
| 1296 | 0× | ui::warn("!") |
| 1297 | 0× | ); |
| 1298 | 3× | } |
| 1299 | 187× | } |
| 1300 | ||
| 1301 | /// Open the repository containing the current directory. | |
| 1302 | 159× | fn open() -> Result<Repo> { |
| 1303 | 159× | let repo = Repo::open(&std::env::current_dir()?).context("opening repository")?; |
| 1304 | 156× | match AT_OP.get().and_then(|o| o.as_deref()) { |
| 1305 | 0× | Some(op) => Ok(repo.at_op(op)?), |
| 1306 | 156× | None => Ok(repo), |
| 1307 | } | |
| 1308 | 159× | } |
| 1309 | ||
| 1310 | /// `oxy man`: every page into `dir`, or the top-level page on stdout. | |
| 1311 | 2× | fn man(dir: Option<PathBuf>) -> Result<i32> { |
| 1312 | use clap::CommandFactory; | |
| 1313 | 2× | let cmd = Cli::command(); |
| 1314 | 2× | match dir { |
| 1315 | 1× | Some(dir) => { |
| 1316 | 1× | std::fs::create_dir_all(&dir)?; |
| 1317 | 1× | clap_mangen::generate_to(cmd, &dir)?; |
| 1318 | 1× | let pages = std::fs::read_dir(&dir)? |
| 1319 | 135× | .filter(|e| { |
| 1320 | 135× | e.as_ref() |
| 1321 | 135× | .is_ok_and(|e| e.path().extension().is_some_and(|x| x == "1")) |
| 1322 | 135× | }) |
| 1323 | 1× | .count(); |
| 1324 | 1× | println!( |
| 1325 | "{}", | |
| 1326 | 1× | ui::done(format!("Wrote {pages} man pages to {}", dir.display())) |
| 1327 | ); | |
| 1328 | } | |
| 1329 | 1× | None => clap_mangen::Man::new(cmd).render(&mut std::io::stdout())?, |
| 1330 | } | |
| 1331 | 2× | Ok(0) |
| 1332 | 2× | } |
| 1333 | ||
| 1334 | /// Parse arguments and run; returns the process exit code. | |
| 1335 | 200× | pub fn run() -> Result<i32> { |
| 1336 | // The cloud stores (s3://, gs://, az://) for remotes kept on them. | |
| 1337 | 200× | oxy_strata::register(); |
| 1338 | 200× | let cli = Cli::parse(); |
| 1339 | 200× | ui::init(cli.plain); |
| 1340 | 200× | AT_OP.set(cli.at_op.clone()).ok(); |
| 1341 | 0× | match cli.cmd { |
| 1342 | 4× | Cmd::Completions { shell } => { |
| 1343 | use clap::CommandFactory; | |
| 1344 | 4× | clap_complete::generate(shell, &mut Cli::command(), "oxy", &mut std::io::stdout()); |
| 1345 | 4× | Ok(0) |
| 1346 | } | |
| 1347 | 2× | Cmd::Man { dir } => man(dir), |
| 1348 | 21× | Cmd::Init { path, bare, store } => workspace::init(path, bare, store.as_deref()), |
| 1349 | 15× | Cmd::Status { json } => workspace::status(json), |
| 1350 | Cmd::Prompt { | |
| 1351 | 0× | format, |
| 1352 | 0× | glyphs, |
| 1353 | 0× | color, |
| 1354 | 0× | } => workspace::prompt(&format, glyphs, color.as_deref()), |
| 1355 | Cmd::Commit { | |
| 1356 | 36× | message, |
| 1357 | 36× | file, |
| 1358 | 36× | depends_on, |
| 1359 | 36× | paths, |
| 1360 | 36× | interactive, |
| 1361 | 36× | hunks, |
| 1362 | } => { | |
| 1363 | 36× | let message = match (message, file) { |
| 1364 | 35× | (Some(m), _) => m, |
| 1365 | 1× | (None, Some(f)) if f.as_os_str() == "-" => { |
| 1366 | 0× | let mut s = String::new(); |
| 1367 | 0× | std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?; |
| 1368 | 0× | s |
| 1369 | } | |
| 1370 | 1× | (None, Some(f)) => std::fs::read_to_string(&f).map_err(|e| { |
| 1371 | 0× | anyhow::anyhow!("reading the message from {}: {e}", f.display()) |
| 1372 | 0× | })?, |
| 1373 | 0× | (None, None) => anyhow::bail!("a commit message is required (-m or -F)"), |
| 1374 | }; | |
| 1375 | 36× | workspace::commit(&message, &depends_on, &paths, interactive, &hunks) |
| 1376 | } | |
| 1377 | 3× | Cmd::Cat { path, change } => workspace::cat(&path, change.as_deref()), |
| 1378 | 1× | Cmd::Root { json } => workspace::root(json), |
| 1379 | Cmd::Mcp { | |
| 1380 | 3× | dir, |
| 1381 | 3× | allow_destructive, |
| 1382 | 3× | } => mcp::serve(dir, allow_destructive), |
| 1383 | 1× | Cmd::Version { json } => workspace::version(json), |
| 1384 | 2× | Cmd::Discard { paths, hunks } => workspace::discard(&paths, &hunks), |
| 1385 | 2× | Cmd::Hunks { json } => workspace::hunks(json), |
| 1386 | 0× | Cmd::Squash { change, into } => workspace::squash(&change, &into), |
| 1387 | Cmd::Rebase { | |
| 1388 | 0× | change, |
| 1389 | 0× | onto, |
| 1390 | 0× | into_working_copy, |
| 1391 | 0× | } => workspace::rebase(&change, &onto, into_working_copy), |
| 1392 | 0× | Cmd::Blame { path, json } => workspace::blame(&path, json), |
| 1393 | 1× | Cmd::Bisect { cmd } => match cmd { |
| 1394 | 0× | BisectCmd::Start => workspace::bisect_start(), |
| 1395 | 0× | BisectCmd::Good { changes } => workspace::bisect_mark(Some(true), &changes), |
| 1396 | 0× | BisectCmd::Bad { changes } => workspace::bisect_mark(Some(false), &changes), |
| 1397 | 0× | BisectCmd::Skip => workspace::bisect_mark(None, &[]), |
| 1398 | 0× | BisectCmd::Run { command } => workspace::bisect_run(&command), |
| 1399 | 1× | BisectCmd::Status { json } => workspace::bisect_status(json), |
| 1400 | 0× | BisectCmd::Reset => workspace::bisect_reset(), |
| 1401 | }, | |
| 1402 | Cmd::Log { | |
| 1403 | 10× | revset, |
| 1404 | 10× | limit, |
| 1405 | 10× | verbose, |
| 1406 | 10× | path, |
| 1407 | 10× | lines, |
| 1408 | 10× | json, |
| 1409 | 10× | } => workspace::log( |
| 1410 | 10× | revset.as_deref(), |
| 1411 | 10× | limit, |
| 1412 | 10× | verbose, |
| 1413 | 10× | path.as_deref(), |
| 1414 | 10× | lines.as_deref(), |
| 1415 | 10× | json, |
| 1416 | ), | |
| 1417 | 1× | Cmd::Diff { change, json } => workspace::diff(change.as_deref(), json), |
| 1418 | Cmd::Describe { | |
| 1419 | 4× | message, |
| 1420 | 4× | change, |
| 1421 | 4× | json, |
| 1422 | 4× | } => match message { |
| 1423 | 0× | Some(m) => workspace::describe(&m, change.as_deref()), |
| 1424 | 4× | None if change.is_some() => { |
| 1425 | 0× | anyhow::bail!( |
| 1426 | "naming works on the working copy; to change a head's message pass -m" | |
| 1427 | ) | |
| 1428 | } | |
| 1429 | 4× | None => workspace::describe_name(json), |
| 1430 | }, | |
| 1431 | Cmd::Amend { | |
| 1432 | 0× | message, |
| 1433 | 0× | into, |
| 1434 | 0× | paths, |
| 1435 | 0× | } => workspace::amend(message.as_deref(), into.as_deref(), &paths), |
| 1436 | 0× | Cmd::Split { change, paths } => workspace::split(&change, &paths), |
| 1437 | Cmd::Abandon { | |
| 1438 | 0× | change, |
| 1439 | 0× | revset, |
| 1440 | 0× | yes, |
| 1441 | 0× | } => workspace::abandon(change.as_deref(), revset.as_deref(), yes), |
| 1442 | Cmd::Show { | |
| 1443 | 3× | change, |
| 1444 | 3× | revset, |
| 1445 | 3× | stat, |
| 1446 | 3× | json, |
| 1447 | 3× | } => workspace::show(change.as_deref(), revset.as_deref(), stat, json), |
| 1448 | Cmd::Checkout { | |
| 1449 | 0× | changes, |
| 1450 | 0× | discard, |
| 1451 | 0× | carry, |
| 1452 | 0× | } => workspace::checkout(&changes, discard, carry), |
| 1453 | Cmd::Resolve { | |
| 1454 | 1× | paths, |
| 1455 | 1× | both, |
| 1456 | 1× | reverse, |
| 1457 | 1× | json, |
| 1458 | 1× | } => workspace::resolve(&paths, both, reverse, json), |
| 1459 | 0× | Cmd::Undo => workspace::undo(), |
| 1460 | Cmd::Gc { | |
| 1461 | 3× | now, |
| 1462 | 3× | expire_snapshots, |
| 1463 | 3× | remote, |
| 1464 | 3× | } => match remote { |
| 1465 | 2× | Some(name) => workspace::gc_remote(&name, now), |
| 1466 | 1× | None => workspace::gc(now, expire_snapshots), |
| 1467 | }, | |
| 1468 | 0× | Cmd::Fsck { json } => workspace::fsck(json), |
| 1469 | 0× | Cmd::Doctor { offline, json } => workspace::doctor(offline, json), |
| 1470 | 0× | Cmd::Checkpoint => workspace::checkpoint(), |
| 1471 | 1× | Cmd::Op { cmd } => match cmd { |
| 1472 | 1× | OpCmd::Log { limit, json } => workspace::op_log(limit, json), |
| 1473 | 0× | OpCmd::Restore { op, discard } => workspace::op_restore(&op, discard), |
| 1474 | 0× | OpCmd::Revert { op } => workspace::op_revert(&op), |
| 1475 | }, | |
| 1476 | Cmd::Clone { | |
| 1477 | 5× | url, |
| 1478 | 5× | dest, |
| 1479 | 5× | max_blob, |
| 1480 | 5× | sparse, |
| 1481 | 5× | depth, |
| 1482 | 5× | recursive, |
| 1483 | 5× | } => sync::clone(&url, dest, max_blob.as_deref(), &sparse, depth, recursive), |
| 1484 | 0× | Cmd::Deepen { depth, remote } => sync::deepen(&remote, depth), |
| 1485 | 0× | Cmd::Sparse { paths } => workspace::sparse(&paths), |
| 1486 | 0× | Cmd::Lock { path } => workspace::lock(&path), |
| 1487 | 0× | Cmd::Unlock { path } => workspace::unlock(&path), |
| 1488 | 1× | Cmd::Workspace { cmd } => match cmd { |
| 1489 | 0× | WorkspaceCmd::Add { path, changes } => workspace::workspace_add(&path, &changes), |
| 1490 | 1× | WorkspaceCmd::List { json } => workspace::workspace_list(json), |
| 1491 | 0× | WorkspaceCmd::Forget { workspace } => workspace::workspace_forget(&workspace), |
| 1492 | }, | |
| 1493 | 17× | Cmd::Bookmark { cmd } => match cmd { |
| 1494 | BookmarkCmd::Create { | |
| 1495 | 6× | name, |
| 1496 | 6× | changes, |
| 1497 | 6× | upto, |
| 1498 | 6× | only, |
| 1499 | 6× | } => workspace::bookmark_create(&name, &changes, upto, only), |
| 1500 | BookmarkCmd::Set { | |
| 1501 | 3× | name, |
| 1502 | 3× | changes, |
| 1503 | 3× | upto, |
| 1504 | 3× | only, |
| 1505 | 3× | } => workspace::bookmark_set(&name, &changes, upto, only), |
| 1506 | 0× | BookmarkCmd::Delete { name } => workspace::bookmark_delete(&name), |
| 1507 | 5× | BookmarkCmd::List { all, verbose, json } => { |
| 1508 | 5× | workspace::bookmark_list(all, verbose, json) |
| 1509 | } | |
| 1510 | 0× | BookmarkCmd::Show { name, json } => workspace::bookmark_show(name.as_deref(), json), |
| 1511 | BookmarkCmd::Switch { | |
| 1512 | 3× | name, |
| 1513 | 3× | discard, |
| 1514 | 3× | carry, |
| 1515 | 3× | } => workspace::bookmark_switch(&name, discard, carry), |
| 1516 | }, | |
| 1517 | 0× | Cmd::Merge { bookmark } => workspace::merge(&bookmark), |
| 1518 | 0× | Cmd::Auth { cmd } => match cmd { |
| 1519 | AuthCmd::Login { | |
| 1520 | 0× | hub, |
| 1521 | 0× | with_token, |
| 1522 | 0× | ca, |
| 1523 | 0× | } => auth::login(hub.as_deref(), with_token, ca), |
| 1524 | 0× | AuthCmd::Status => auth::status(), |
| 1525 | 0× | AuthCmd::Logout { hub } => auth::logout(hub.as_deref()), |
| 1526 | 0× | AuthCmd::Grant { hub } => auth::grant(hub.as_deref()), |
| 1527 | }, | |
| 1528 | 2× | Cmd::Submodule { cmd } => match cmd { |
| 1529 | SubmoduleCmd::Add { | |
| 1530 | 1× | url, |
| 1531 | 1× | path, |
| 1532 | 1× | bookmark, |
| 1533 | 1× | } => sync::submodule_add(&url, &path, bookmark.as_deref()), |
| 1534 | 0× | SubmoduleCmd::Update { init, paths } => sync::submodule_update(init, &paths), |
| 1535 | 1× | SubmoduleCmd::Status { json } => sync::submodule_status(json), |
| 1536 | SubmoduleCmd::Remove { | |
| 1537 | 0× | path, |
| 1538 | 0× | delete, |
| 1539 | 0× | force, |
| 1540 | 0× | } => sync::submodule_remove(&path, delete, force), |
| 1541 | }, | |
| 1542 | Cmd::Witness { | |
| 1543 | cmd: | |
| 1544 | WitnessCmd::Serve { | |
| 1545 | 0× | name, |
| 1546 | 0× | logs, |
| 1547 | 0× | state, |
| 1548 | 0× | bind, |
| 1549 | 0× | tls_cert, |
| 1550 | 0× | tls_key, |
| 1551 | }, | |
| 1552 | 0× | } => tlog::witness_serve(&name, &logs, state, &bind, tls_cert.zip(tls_key)), |
| 1553 | 0× | Cmd::Tlog { cmd } => match cmd { |
| 1554 | TlogCmd::Verify { | |
| 1555 | 0× | hub, |
| 1556 | 0× | key, |
| 1557 | 0× | all, |
| 1558 | 0× | json, |
| 1559 | 0× | } => tlog::verify(hub.as_deref(), key.as_deref(), all, json), |
| 1560 | }, | |
| 1561 | 5× | Cmd::Artifact { cmd } => match cmd { |
| 1562 | ArtifactCmd::Pack { | |
| 1563 | 2× | dir, |
| 1564 | 2× | kind, |
| 1565 | 2× | arch, |
| 1566 | 2× | os, |
| 1567 | 2× | entrypoint, |
| 1568 | 2× | env, |
| 1569 | 2× | labels, |
| 1570 | 2× | json, |
| 1571 | 2× | } => artifact::pack(&dir, &kind, arch, os, entrypoint, &env, &labels, json), |
| 1572 | 1× | ArtifactCmd::List { json } => artifact::list(json), |
| 1573 | 0× | ArtifactCmd::Show { name, json } => artifact::show(&name, json), |
| 1574 | 1× | ArtifactCmd::Unpack { name, dest } => artifact::unpack(&name, &dest), |
| 1575 | 1× | ArtifactCmd::Verify { name } => artifact::verify(&name), |
| 1576 | }, | |
| 1577 | 10× | Cmd::Receipt { cmd } => match cmd { |
| 1578 | 2× | ReceiptCmd::List { rev, json } => receipt::list(rev.as_deref(), json), |
| 1579 | ReceiptCmd::Show { | |
| 1580 | 1× | receipt: r, |
| 1581 | 1× | rev, |
| 1582 | 1× | json, |
| 1583 | 1× | } => receipt::show(&r, rev.as_deref(), json), |
| 1584 | ReceiptCmd::Export { | |
| 1585 | 3× | receipt: r, |
| 1586 | 3× | file, |
| 1587 | 3× | rev, |
| 1588 | 3× | format, |
| 1589 | 3× | } => receipt::export(&r, &file, rev.as_deref(), &format), |
| 1590 | 2× | ReceiptCmd::Verify { file, json } => receipt::verify(&file, json), |
| 1591 | 2× | ReceiptCmd::Import { file } => receipt::import(&file), |
| 1592 | }, | |
| 1593 | 3× | Cmd::Tag { cmd } => match cmd { |
| 1594 | TagCmd::Create { | |
| 1595 | 2× | name, |
| 1596 | 2× | changes, |
| 1597 | 2× | message, |
| 1598 | 2× | } => workspace::tag_create(&name, &changes, &message), |
| 1599 | 0× | TagCmd::Delete { name, reason } => workspace::tag_delete(&name, &reason), |
| 1600 | 1× | TagCmd::List { json } => workspace::tag_list(json), |
| 1601 | }, | |
| 1602 | 10× | Cmd::Remote { cmd } => match cmd { |
| 1603 | RemoteCmd::Add { | |
| 1604 | 4× | name, |
| 1605 | 4× | url, |
| 1606 | 4× | profile, |
| 1607 | 4× | region, |
| 1608 | 4× | endpoint, |
| 1609 | 4× | credentials, |
| 1610 | 4× | } => sync::remote_add( |
| 1611 | 4× | &name, |
| 1612 | 4× | &url, |
| 1613 | 4× | &[ |
| 1614 | 4× | ("profile", profile), |
| 1615 | 4× | ("region", region), |
| 1616 | 4× | ("endpoint", endpoint), |
| 1617 | 4× | ("credentials", credentials), |
| 1618 | 4× | ], |
| 1619 | ), | |
| 1620 | 3× | RemoteCmd::Check { name } => sync::remote_check(&name), |
| 1621 | 1× | RemoteCmd::Remove { name } => sync::remote_remove(&name), |
| 1622 | 2× | RemoteCmd::List { json } => sync::remote_list(json), |
| 1623 | }, | |
| 1624 | 1× | Cmd::Fetch { remote, json } => sync::fetch(remote.as_deref(), json), |
| 1625 | 4× | Cmd::Pull { remote, json } => sync::pull(remote.as_deref(), json), |
| 1626 | Cmd::Push { | |
| 1627 | 7× | remote, |
| 1628 | 7× | force, |
| 1629 | 7× | bookmark, |
| 1630 | 7× | delete, |
| 1631 | 7× | json, |
| 1632 | 7× | } => match delete { |
| 1633 | 0× | Some(name) => sync::push_delete(remote.as_deref(), &name, json), |
| 1634 | 7× | None => sync::push(remote.as_deref(), force, bookmark.as_deref(), json), |
| 1635 | }, | |
| 1636 | Cmd::Serve { | |
| 1637 | 0× | root, |
| 1638 | 0× | bind, |
| 1639 | 0× | tokens, |
| 1640 | 0× | tls_cert, |
| 1641 | 0× | tls_key, |
| 1642 | 0× | } => sync::serve(root, &bind, tokens, tls_cert.zip(tls_key)), |
| 1643 | 10× | Cmd::Identity { cmd } => match cmd { |
| 1644 | IdentityCmd::Create { | |
| 1645 | 7× | name, |
| 1646 | 7× | email, |
| 1647 | 7× | local, |
| 1648 | 7× | expires, |
| 1649 | 7× | } => identity::create(name, email, local, expires), |
| 1650 | 2× | IdentityCmd::Show => identity::show(), |
| 1651 | 1× | IdentityCmd::Export { path, pem } => identity::export(path, pem), |
| 1652 | 0× | IdentityCmd::SignChallenge { challenge, json } => { |
| 1653 | 0× | identity::sign_challenge(&challenge, json) |
| 1654 | } | |
| 1655 | 0× | IdentityCmd::Rotate => identity::rotate(), |
| 1656 | 0× | IdentityCmd::Renew { expires } => identity::renew(expires), |
| 1657 | 0× | IdentityCmd::Revoke { key, reason } => identity::revoke(key.as_deref(), &reason), |
| 1658 | 0× | IdentityCmd::Delegate { cmd } => match cmd { |
| 1659 | DelegateCmd::Add { | |
| 1660 | 0× | identity_file, |
| 1661 | 0× | scope, |
| 1662 | 0× | expires, |
| 1663 | 0× | } => identity::delegate_add(&identity_file, &scope, expires.as_deref()), |
| 1664 | 0× | DelegateCmd::Remove { key } => identity::delegate_remove(&key), |
| 1665 | 0× | DelegateCmd::List => identity::delegate_list(), |
| 1666 | }, | |
| 1667 | }, | |
| 1668 | Cmd::Git { | |
| 1669 | 2× | cmd: GitCmd::Colocate { off }, |
| 1670 | 2× | } => sync::git_colocate(off), |
| 1671 | Cmd::Git { | |
| 1672 | 0× | cmd: GitCmd::Export { git_dir, branch }, |
| 1673 | 0× | } => sync::git_export(&git_dir, branch.as_deref()), |
| 1674 | Cmd::Git { | |
| 1675 | 0× | cmd: GitCmd::Fetch { url, branch, claim }, |
| 1676 | 0× | } => sync::git_fetch(&url, &branch, claim), |
| 1677 | Cmd::Git { | |
| 1678 | 0× | cmd: GitCmd::Push { url, branch }, |
| 1679 | 0× | } => sync::git_push(&url, &branch), |
| 1680 | Cmd::Git { | |
| 1681 | cmd: GitCmd::Recover, | |
| 1682 | 0× | } => sync::git_recover(), |
| 1683 | Cmd::Git { | |
| 1684 | cmd: | |
| 1685 | GitCmd::Import { | |
| 1686 | 0× | git_dir, |
| 1687 | 0× | dest, |
| 1688 | 0× | branch, |
| 1689 | 0× | claim, |
| 1690 | }, | |
| 1691 | 0× | } => sync::git_import(&git_dir, &dest, &branch, claim), |
| 1692 | Cmd::Git { | |
| 1693 | cmd: | |
| 1694 | GitCmd::Verify { | |
| 1695 | 0× | git_dir, |
| 1696 | 0× | branch, |
| 1697 | 0× | last, |
| 1698 | 0× | max_differences, |
| 1699 | }, | |
| 1700 | 0× | } => sync::git_verify(&git_dir, &branch, last, max_differences), |
| 1701 | 5× | Cmd::Policy { cmd } => match cmd { |
| 1702 | 1× | PolicyCmd::Init => identity::policy_init(), |
| 1703 | 3× | PolicyCmd::Show { json } => identity::policy_show(json), |
| 1704 | PolicyCmd::Grant { | |
| 1705 | 0× | identity_file, |
| 1706 | 0× | pattern, |
| 1707 | 0× | out, |
| 1708 | 0× | } => identity::policy_grant(&identity_file, pattern.as_deref(), out), |
| 1709 | PolicyCmd::GrantSubject { | |
| 1710 | 1× | subject, |
| 1711 | 1× | issuer, |
| 1712 | 1× | scope, |
| 1713 | 1× | pattern, |
| 1714 | 1× | out, |
| 1715 | 1× | } => identity::policy_grant_subject(&subject, &issuer, &scope, pattern.as_deref(), out), |
| 1716 | 0× | PolicyCmd::Revoke { key, out } => identity::policy_revoke(&key, out), |
| 1717 | 0× | PolicyCmd::Root { cmd } => match cmd { |
| 1718 | 0× | RootCmd::Add { identity_file, out } => { |
| 1719 | 0× | identity::policy_root_add(&identity_file, out) |
| 1720 | } | |
| 1721 | 0× | RootCmd::Remove { key, out } => identity::policy_root_remove(&key, out), |
| 1722 | 0× | RootCmd::Threshold { n, out } => identity::policy_threshold(n, out), |
| 1723 | }, | |
| 1724 | 0× | PolicyCmd::Sign { proposal, out } => identity::policy_sign(&proposal, out), |
| 1725 | }, | |
| 1726 | Cmd::Seal { | |
| 1727 | 0× | flow: Some(url), |
| 1728 | 0× | job: Some(job), |
| 1729 | 0× | reject, |
| 1730 | 0× | json, |
| 1731 | .. | |
| 1732 | 0× | } => approve::seal(&url, &job, reject, json), |
| 1733 | Cmd::Seal { | |
| 1734 | 0× | changes, |
| 1735 | 0× | proposal: Some(url), |
| 1736 | 0× | json, |
| 1737 | .. | |
| 1738 | 0× | } if changes.is_empty() => proposal::seal(&url, json), |
| 1739 | 0× | Cmd::Seal { changes, json, .. } => identity::seal(&changes, json), |
| 1740 | 0× | Cmd::Proposal { cmd } => match cmd { |
| 1741 | 0× | ProposalCmd::Show { url, json } => proposal::show(&url, json), |
| 1742 | 0× | ProposalCmd::Land { url, force } => proposal::land(&url, force), |
| 1743 | }, | |
| 1744 | Cmd::Verify { | |
| 1745 | 1× | change, |
| 1746 | 1× | revset, |
| 1747 | 1× | json, |
| 1748 | 1× | } => identity::verify(change.as_deref(), revset.as_deref(), json), |
| 1749 | Cmd::Config { | |
| 1750 | 3× | key, |
| 1751 | 3× | value, |
| 1752 | 3× | user, |
| 1753 | 3× | unset, |
| 1754 | 3× | } => identity::config(&key, value.as_deref().unwrap_or(""), user, unset), |
| 1755 | } | |
| 1756 | 200× | } |
| 1757 | ||
| 1758 | #[cfg(test)] | |
| 1759 | mod help_tests { | |
| 1760 | use super::Cli; | |
| 1761 | use clap::CommandFactory; | |
| 1762 | | |
| 1763 | /// Every command and every argument explains itself, so `--help`, | |
| 1764 | /// completions and the man pages are complete. | |
| 1765 | #[test] | |
| 1766 | 1× | fn every_command_and_argument_has_help() { |
| 1767 | 135× | fn walk(cmd: &clap::Command, path: &str, missing: &mut Vec<String>) { |
| 1768 | 135× | let here = format!("{path} {}", cmd.get_name()).trim().to_string(); |
| 1769 | 135× | if cmd.get_about().is_none() && !path.is_empty() { |
| 1770 | 0× | missing.push(format!("{here} (command)")); |
| 1771 | 135× | } |
| 1772 | 257× | for a in cmd.get_arguments() { |
| 1773 | 257× | let id = a.get_id().as_str(); |
| 1774 | 257× | if id == "help" || id == "version" || (a.is_global_set() && !path.is_empty()) { |
| 1775 | 0× | continue; |
| 1776 | 257× | } |
| 1777 | 257× | if a.get_help().is_none() && a.get_long_help().is_none() { |
| 1778 | 0× | missing.push(format!("{here} {id}")); |
| 1779 | 257× | } |
| 1780 | } | |
| 1781 | 135× | for sub in cmd.get_subcommands() { |
| 1782 | 134× | walk(sub, &here, missing); |
| 1783 | 134× | } |
| 1784 | 135× | } |
| 1785 | 1× | let mut missing = Vec::new(); |
| 1786 | 1× | walk(&Cli::command(), "", &mut missing); |
| 1787 | 1× | assert!( |
| 1788 | 1× | missing.is_empty(), |
| 1789 | "{} without help:\n{}", | |
| 1790 | 0× | missing.len(), |
| 1791 | 0× | missing.join("\n") |
| 1792 | ); | |
| 1793 | 1× | } |
| 1794 | } |