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