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