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