git_lfs/args.rs
1//! Clap CLI surface (struct `Cli` + subcommands).
2//!
3//! Extracted from `main.rs` so xtask (and any future tool) can
4//! reuse the command tree for man-page generation, completion
5//! scripts, etc. Keep this file focused on the clap derive — all
6//! dispatch / business logic stays in main.rs and the per-command
7//! modules.
8
9use std::path::PathBuf;
10
11use clap::{Parser, Subcommand};
12
13#[derive(Parser)]
14#[command(
15 name = "git-lfs",
16 about = "Git LFS — large file storage for git",
17 // We want `git lfs --version` to print the same banner as
18 // `git lfs version`. clap's auto-derived `--version` would
19 // emit `git-lfs <version>` (one token, no `/` separator),
20 // which doesn't match the user-agent style upstream uses.
21 // Suppress clap's flag and handle --version ourselves.
22 disable_version_flag = true,
23 max_term_width = 100,
24)]
25pub struct Cli {
26 /// Print the version banner and exit.
27 #[arg(long, short = 'V', global = true)]
28 pub version: bool,
29
30 #[command(subcommand)]
31 pub command: Option<Command>,
32}
33
34#[derive(Subcommand)]
35pub enum MigrateCmd {
36 /// Rewrite history so files matching the include filter become LFS
37 /// pointers. With `--no-rewrite`, history is preserved and one
38 /// new commit is appended on top of HEAD with the named paths
39 /// converted in place.
40 Import {
41 /// Without `--no-rewrite`: branches/refs to rewrite (empty =
42 /// current branch). With `--no-rewrite`: working-tree paths
43 /// to convert.
44 args: Vec<String>,
45 /// Walk every local branch and tag.
46 #[arg(long)]
47 everything: bool,
48 /// Convert paths matching this glob (repeatable). Required
49 /// unless `--above` is set or `--no-rewrite` is given.
50 #[arg(short = 'I', long = "include")]
51 include: Vec<String>,
52 /// Exclude paths matching this glob (repeatable).
53 #[arg(short = 'X', long = "exclude")]
54 exclude: Vec<String>,
55 /// Restrict the rewrite to commits reachable from these refs.
56 /// Repeatable.
57 #[arg(long = "include-ref")]
58 include_ref: Vec<String>,
59 /// Exclude commits reachable from these refs. Repeatable.
60 #[arg(long = "exclude-ref")]
61 exclude_ref: Vec<String>,
62 /// Only convert files at least this large (e.g. `1mb`,
63 /// `500k`).
64 #[arg(long, default_value = "")]
65 above: String,
66 /// Don't rewrite history. Read named paths from the working
67 /// tree, convert in place, append one new commit on top of
68 /// HEAD.
69 #[arg(long)]
70 no_rewrite: bool,
71 /// Commit message for the `--no-rewrite` commit.
72 #[arg(short, long)]
73 message: Option<String>,
74 /// Skip the prompt confirming history rewrite. Currently we
75 /// never prompt, so this is accepted as a no-op for parity
76 /// with upstream's CLI surface.
77 #[arg(long)]
78 yes: bool,
79 /// Walk every commit and convert files that *should* be LFS
80 /// pointers (per their commit's `.gitattributes`) but
81 /// currently aren't. Mutually exclusive with `--include`,
82 /// `--exclude`, `--no-rewrite`.
83 #[arg(long)]
84 fixup: bool,
85 /// Don't fetch missing LFS objects from the remote before the
86 /// rewrite — accepted as a no-op since we never auto-fetch
87 /// today.
88 #[arg(long)]
89 skip_fetch: bool,
90 /// Write a comma-separated `<old>,<new>` mapping of every
91 /// rewritten commit OID to the named file.
92 #[arg(long = "object-map")]
93 object_map: Option<std::path::PathBuf>,
94 /// Print a per-commit progress line as the rewrite walks
95 /// history.
96 #[arg(long)]
97 verbose: bool,
98 /// Remote to consult when fetching missing LFS objects (default
99 /// `origin`).
100 #[arg(long)]
101 remote: Option<String>,
102 },
103 /// Inverse of import: rewrite history so LFS pointers become the
104 /// raw bytes they reference. Requires the LFS objects to already
105 /// be in the local store — `git lfs fetch` first if not. Pointers
106 /// whose objects are missing are left as-is.
107 Export {
108 /// Branches / refs to rewrite. Empty = current branch.
109 branches: Vec<String>,
110 /// Walk every local branch and tag.
111 #[arg(long)]
112 everything: bool,
113 /// Convert pointers at paths matching this glob (repeatable).
114 /// Required.
115 #[arg(short = 'I', long = "include")]
116 include: Vec<String>,
117 /// Don't convert pointers at paths matching this glob.
118 #[arg(short = 'X', long = "exclude")]
119 exclude: Vec<String>,
120 /// Restrict the rewrite to commits reachable from these refs.
121 /// Repeatable.
122 #[arg(long = "include-ref")]
123 include_ref: Vec<String>,
124 /// Exclude commits reachable from these refs. Repeatable.
125 #[arg(long = "exclude-ref")]
126 exclude_ref: Vec<String>,
127 /// Don't fetch missing LFS objects from the remote before the
128 /// rewrite — leave their pointers in place.
129 #[arg(long)]
130 skip_fetch: bool,
131 /// Write a comma-separated `<old>,<new>` mapping of every
132 /// rewritten commit OID to the named file. Useful as input to
133 /// `git filter-repo` or other downstream tools.
134 #[arg(long = "object-map")]
135 object_map: Option<std::path::PathBuf>,
136 /// Print a per-commit progress line as the rewrite walks
137 /// history.
138 #[arg(long)]
139 verbose: bool,
140 /// Remote to consult when fetching missing LFS objects (default
141 /// `origin`).
142 #[arg(long)]
143 remote: Option<String>,
144 /// Skip the prompt confirming history rewrite. Currently we
145 /// never prompt, so this is accepted as a no-op for parity
146 /// with upstream's CLI surface.
147 #[arg(long)]
148 yes: bool,
149 },
150 /// Walk history and report file extensions by total size.
151 /// Read-only — no objects or history change.
152 Info {
153 /// Branches / refs to scan. Empty = current branch.
154 branches: Vec<String>,
155 /// Walk every local branch and tag.
156 #[arg(long)]
157 everything: bool,
158 /// Only include paths matching this glob (repeatable).
159 #[arg(short = 'I', long = "include")]
160 include: Vec<String>,
161 /// Exclude paths matching this glob (repeatable).
162 #[arg(short = 'X', long = "exclude")]
163 exclude: Vec<String>,
164 /// Restrict the scan to commits reachable from these refs.
165 /// Repeatable.
166 #[arg(long = "include-ref")]
167 include_ref: Vec<String>,
168 /// Exclude commits reachable from these refs. Repeatable.
169 #[arg(long = "exclude-ref")]
170 exclude_ref: Vec<String>,
171 /// Only count files at least this large (e.g. `1mb`, `500k`).
172 #[arg(long, default_value = "")]
173 above: String,
174 /// Maximum extension rows to show.
175 #[arg(long, default_value_t = 5)]
176 top: usize,
177 /// How to handle existing LFS pointer blobs:
178 /// `follow` (default), `ignore`, or `no-follow`. Defaults
179 /// based on `--fixup`: `ignore` with the flag, `follow`
180 /// without.
181 #[arg(long)]
182 pointers: Option<String>,
183 /// Force the size unit for byte counts (`b`, `kb`, `mb`,
184 /// `gb`, `tb`, `pb`). Auto-scaled when omitted.
185 #[arg(long)]
186 unit: Option<String>,
187 /// Don't fetch missing LFS objects from the remote — accepted
188 /// as a no-op since we don't auto-fetch today.
189 #[arg(long)]
190 skip_fetch: bool,
191 /// Remote to consult (no-op for now; reserved for the
192 /// auto-fetch path).
193 #[arg(long)]
194 remote: Option<String>,
195 /// Walk history looking for files that *should* be LFS but
196 /// aren't (per `.gitattributes`). Implies `--pointers=ignore`.
197 #[arg(long)]
198 fixup: bool,
199 },
200}
201
202#[derive(Subcommand)]
203pub enum Command {
204 /// Run the clean filter: read content on stdin, write a pointer on stdout.
205 Clean {
206 /// Working-tree path of the file being cleaned. Substituted for
207 /// `%f` in any configured `lfs.extension.<name>.clean` command.
208 path: Option<PathBuf>,
209 },
210 /// Run the smudge filter: read a pointer on stdin, write content on stdout.
211 Smudge {
212 /// Working-tree path of the file being smudged (currently unused).
213 path: Option<PathBuf>,
214 /// Pass the pointer text through unchanged; equivalent to
215 /// `GIT_LFS_SKIP_SMUDGE=1`. Wired up by `install --skip-smudge`.
216 #[arg(long)]
217 skip: bool,
218 },
219 /// Configure git to invoke git-lfs as the clean/smudge/process filter,
220 /// and install the LFS git hooks.
221 Install {
222 /// Set config in the local repo only (default: --global).
223 #[arg(short, long)]
224 local: bool,
225 /// Overwrite existing config and hooks.
226 #[arg(short, long)]
227 force: bool,
228 /// Only set the filter config; don't install hooks.
229 #[arg(long)]
230 skip_repo: bool,
231 /// Configure the smudge filter to pass pointer text through
232 /// unchanged. Use with a follow-up `git lfs pull` to download
233 /// content on demand.
234 #[arg(long)]
235 skip_smudge: bool,
236 },
237 /// Reverse of `install`: clear the `filter.lfs.*` config and remove
238 /// the LFS git hooks. Hooks that don't match what we'd write are left
239 /// untouched.
240 Uninstall {
241 /// Operate on the local repo only (default: --global).
242 #[arg(short, long)]
243 local: bool,
244 /// Only unset config; don't touch hooks.
245 #[arg(long)]
246 skip_repo: bool,
247 },
248 /// Track a file pattern with git-lfs by adding it to .gitattributes.
249 /// With no patterns, lists currently-tracked patterns.
250 Track {
251 /// File patterns to track (e.g. "*.jpg", "data/*.bin").
252 patterns: Vec<String>,
253 /// Mark the tracked pattern as `lockable` (`*.psd lockable`).
254 #[arg(short = 'l', long)]
255 lockable: bool,
256 /// Re-track an existing pattern, removing its `lockable` flag.
257 #[arg(long)]
258 not_lockable: bool,
259 /// Print what would happen without modifying `.gitattributes` or
260 /// re-staging files.
261 #[arg(long)]
262 dry_run: bool,
263 /// Extra logging: print "Found N files previously added to Git
264 /// matching pattern" lines.
265 #[arg(short, long)]
266 verbose: bool,
267 /// Listing mode only: emit JSON instead of the human-readable
268 /// listing.
269 #[arg(long)]
270 json: bool,
271 /// Listing mode only: suppress the "Listing excluded patterns"
272 /// section.
273 #[arg(long)]
274 no_excluded: bool,
275 /// Treat each pattern as a literal filename — escape glob
276 /// metacharacters (`*`, `?`, `[`, `]`, backslash, space) so
277 /// the entry in `.gitattributes` matches that exact name even
278 /// when it contains shell-glob characters.
279 #[arg(long)]
280 filename: bool,
281 },
282 /// Stop tracking a file pattern with git-lfs by removing it from
283 /// .gitattributes. The matching pointer files in history (and the
284 /// objects in the local store) are left in place.
285 Untrack {
286 /// File patterns to untrack.
287 patterns: Vec<String>,
288 },
289 /// Run the long-running filter-process protocol with git over stdin/stdout.
290 /// This is what git invokes via filter.lfs.process and is the batched
291 /// alternative to per-invocation `clean`/`smudge`.
292 FilterProcess {
293 /// Pass smudge requests' pointer text through unchanged;
294 /// equivalent to `GIT_LFS_SKIP_SMUDGE=1`. Wired up by
295 /// `install --skip-smudge`.
296 #[arg(long)]
297 skip: bool,
298 },
299 /// Download every LFS object reachable from the given refs (default: HEAD)
300 /// that isn't already in the local store. Walks history, dedupes by OID.
301 Fetch {
302 /// First positional arg is treated as a remote name (if it
303 /// resolves); subsequent args are refs.
304 args: Vec<String>,
305 /// List the objects that would be fetched without downloading
306 /// them (one `fetch <oid> => <path>` line per object).
307 #[arg(long)]
308 dry_run: bool,
309 /// JSON output. With `--dry-run`, queries the server's batch
310 /// endpoint to populate `actions` URLs.
311 #[arg(long)]
312 json: bool,
313 /// Walk every local ref under `refs/heads/*` + `refs/tags/*`.
314 #[arg(long)]
315 all: bool,
316 /// Re-download objects we already have (e.g. recovery from a
317 /// corrupt local store).
318 #[arg(long)]
319 refetch: bool,
320 /// Read refs from stdin, one per line. Blank lines dropped.
321 #[arg(long)]
322 stdin: bool,
323 /// Run `prune` after the fetch completes.
324 #[arg(long)]
325 prune: bool,
326 /// Comma-separated globs; only matching paths are fetched.
327 /// Falls back to `lfs.fetchinclude` when omitted.
328 #[arg(short = 'I', long)]
329 include: Vec<String>,
330 /// Comma-separated globs; matching paths are skipped. Falls
331 /// back to `lfs.fetchexclude` when omitted.
332 #[arg(short = 'X', long)]
333 exclude: Vec<String>,
334 },
335 /// `fetch` then re-run the smudge filter so the working tree contains
336 /// real LFS file contents instead of pointer text. Requires
337 /// `git lfs install` to have wired up the smudge filter.
338 Pull {
339 /// Refs to scan for LFS pointers. Defaults to `HEAD`.
340 refs: Vec<String>,
341 /// Comma-separated globs; only matching paths are pulled.
342 /// Falls back to `lfs.fetchinclude` when omitted.
343 #[arg(short = 'I', long)]
344 include: Vec<String>,
345 /// Comma-separated globs; matching paths are skipped. Falls
346 /// back to `lfs.fetchexclude` when omitted.
347 #[arg(short = 'X', long)]
348 exclude: Vec<String>,
349 },
350 /// Upload every LFS object reachable from the given refs that the
351 /// remote doesn't already have. The "doesn't have" set is approximated
352 /// by `refs/remotes/<remote>/*`; the LFS server's batch API also
353 /// dedupes server-side so missing exclusions don't waste bandwidth.
354 Push {
355 /// Name of the remote (e.g. "origin") whose tracking refs are
356 /// excluded from the upload set.
357 remote: String,
358 /// Refs (or, with `--object-id`, raw OIDs) to push. With
359 /// `--all`, restricts the all-refs walk to these; with
360 /// `--stdin`, ignored (a warning is emitted).
361 args: Vec<String>,
362 /// List the objects that would be pushed without actually
363 /// uploading them (one `push <oid> => <path>` line per object).
364 #[arg(long)]
365 dry_run: bool,
366 /// Push every local ref under `refs/heads/*` and `refs/tags/*`
367 /// (intersected with `args` if any are given).
368 #[arg(long)]
369 all: bool,
370 /// Read refs (or OIDs, with `--object-id`) from stdin, one per
371 /// line. Blank lines are skipped.
372 #[arg(long)]
373 stdin: bool,
374 /// Treat positional args / stdin entries as raw LFS OIDs
375 /// rather than git refs, and upload those objects directly
376 /// from the local store.
377 #[arg(long)]
378 object_id: bool,
379 },
380 /// Deprecated. Wraps `git clone` so the working tree is populated
381 /// with pointer text first, then runs `git lfs pull` to download
382 /// LFS content in batch. Modern `git clone` parallelizes the
383 /// smudge filter and is no slower; prefer it.
384 Clone {
385 /// `git clone` and LFS pass-through args. The repository URL
386 /// is required; an optional target directory follows.
387 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
388 args: Vec<String>,
389 },
390 /// Git post-checkout hook entry point. Receives `<prev-sha>
391 /// <post-sha> <flag>` (flag is "1" if HEAD moved). Currently a
392 /// no-op stub — exists so installed hook scripts don't fail. Real
393 /// behavior arrives with `track --lockable`.
394 PostCheckout { args: Vec<String> },
395 /// Git post-commit hook entry point. No arguments. Currently a
396 /// no-op stub.
397 PostCommit { args: Vec<String> },
398 /// Git post-merge hook entry point. Receives `<squash-flag>`.
399 /// Currently a no-op stub.
400 PostMerge { args: Vec<String> },
401 /// Git pre-push hook entry point — not typically invoked by hand.
402 /// Reads `<local-ref> <local-sha> <remote-ref> <remote-sha>` lines
403 /// from stdin and uploads the LFS objects newly reachable from each
404 /// `<local-sha>`.
405 PrePush {
406 /// Name of the remote being pushed to.
407 remote: String,
408 /// URL of the remote (informational; we use `lfs.url` config).
409 url: Option<String>,
410 /// List the objects that would be pushed without actually
411 /// uploading them.
412 #[arg(long)]
413 dry_run: bool,
414 },
415 /// Print the git-lfs version and exit.
416 Version,
417 /// Debug helper: build a pointer from a file, parse one from disk
418 /// or stdin, or just check whether some bytes are a valid pointer.
419 Pointer {
420 /// Build a pointer from this file (read content, hash, encode).
421 #[arg(short, long)]
422 file: Option<PathBuf>,
423 /// Parse and display this existing pointer file.
424 #[arg(short, long)]
425 pointer: Option<PathBuf>,
426 /// Read a pointer from stdin (mutually exclusive with --pointer).
427 #[arg(long)]
428 stdin: bool,
429 /// Validity check mode: exit 0 if input parses, 1 if not, 2 if
430 /// `--strict` and not byte-canonical.
431 #[arg(long)]
432 check: bool,
433 /// In `--check`, also reject non-canonical pointers.
434 #[arg(long)]
435 strict: bool,
436 /// Explicitly disable strict mode (paired with `--strict`).
437 #[arg(long)]
438 no_strict: bool,
439 },
440 /// Show the LFS environment: version, endpoints, on-disk paths, and
441 /// the three `filter.lfs.*` config values.
442 Env,
443 /// List the configured LFS pointer extensions (`lfs.extension.<name>.*`).
444 /// Extensions chain external clean/smudge programs around each LFS
445 /// object; this prints their resolved configuration in priority order.
446 Ext,
447 /// Analyze or rewrite history for LFS conversion. Phase 1 ships
448 /// `info` only; `import` and `export` will land in subsequent phases.
449 Migrate {
450 #[command(subcommand)]
451 cmd: MigrateCmd,
452 },
453 /// Replace pointer text in the working tree with actual LFS object
454 /// content. With no args, materializes every LFS pointer in HEAD's
455 /// tree. With paths (literal file names or trailing-slash directory
456 /// prefixes), restricts to matching pointers.
457 ///
458 /// During a merge conflict, `--to <path> --ours/--theirs/--base
459 /// <file>` writes the LFS content from one of the conflicted
460 /// stages to `<path>` (creating intermediate directories) so the
461 /// user can compare or salvage versions.
462 Checkout {
463 /// Paths to check out. Empty = everything in HEAD's tree.
464 /// In conflict mode (`--to`), exactly one path is required.
465 paths: Vec<String>,
466 /// Conflict-mode: write the chosen stage's content to this
467 /// path instead of into the working tree. Resolves relative
468 /// to the current directory.
469 #[arg(long, value_name = "PATH")]
470 to: Option<String>,
471 /// Conflict-mode: pull from stage 2 (HEAD's version). Mutually
472 /// exclusive with `--theirs` and `--base`.
473 #[arg(long)]
474 ours: bool,
475 /// Conflict-mode: pull from stage 3 (the merging-in version).
476 #[arg(long)]
477 theirs: bool,
478 /// Conflict-mode: pull from stage 1 (the common ancestor).
479 #[arg(long)]
480 base: bool,
481 },
482 /// Delete local LFS objects that aren't reachable from HEAD or any
483 /// unpushed commit. Reclaims disk for repos whose history has moved
484 /// past their objects.
485 Prune {
486 /// Don't delete anything; just report what would go.
487 #[arg(short, long)]
488 dry_run: bool,
489 /// Print each prunable object's OID and size.
490 #[arg(short, long)]
491 verbose: bool,
492 },
493 /// Check the integrity of LFS objects and pointers reachable from
494 /// `<refspec>` (default: HEAD). Exit 1 if anything is corrupt.
495 Fsck {
496 /// Ref to scan. Defaults to HEAD.
497 refspec: Option<String>,
498 /// Only check objects (verify store contents match pointer OIDs).
499 #[arg(long)]
500 objects: bool,
501 /// Only check pointers (flag non-canonical pointer encodings).
502 #[arg(long)]
503 pointers: bool,
504 /// Report problems but don't move corrupt objects to `<lfs>/bad/`.
505 #[arg(short, long)]
506 dry_run: bool,
507 },
508 /// Show staged + unstaged changes, classifying each blob as LFS,
509 /// Git, or working-tree File.
510 Status {
511 /// Stable one-line-per-change format for scripts.
512 #[arg(short, long)]
513 porcelain: bool,
514 /// Stable JSON output for scripts; only LFS entries are reported.
515 #[arg(short, long)]
516 json: bool,
517 },
518 /// Acquire an exclusive server-side lock on one or more files.
519 /// Other users will be unable to push changes to a locked file.
520 Lock {
521 /// Paths to lock (repo-relative or absolute, must resolve inside
522 /// the working tree).
523 paths: Vec<String>,
524 /// Specify which remote to use when interacting with locks.
525 #[arg(short, long)]
526 remote: Option<String>,
527 /// Refspec to associate the lock with. Defaults to the current
528 /// branch's tracked upstream (`branch.<current>.merge`) or the
529 /// current branch's full ref (`refs/heads/<branch>`).
530 #[arg(long = "ref")]
531 refspec: Option<String>,
532 /// Stable JSON output for scripts.
533 #[arg(short, long)]
534 json: bool,
535 },
536 /// List file locks held on the server.
537 Locks {
538 /// Specify which remote to use when interacting with locks.
539 #[arg(short, long)]
540 remote: Option<String>,
541 /// Filter results to a particular path.
542 #[arg(short, long)]
543 path: Option<String>,
544 /// Filter results to a particular lock id.
545 #[arg(short, long)]
546 id: Option<String>,
547 /// Maximum number of results to return.
548 #[arg(short, long)]
549 limit: Option<u32>,
550 /// Refspec to filter locks by (defaults to current branch /
551 /// tracked upstream — same auto-resolution as `git lfs lock`).
552 #[arg(long = "ref")]
553 refspec: Option<String>,
554 /// Verify ownership: prefix locks owned by the authenticated user
555 /// with `O ` (others get ` `).
556 #[arg(long)]
557 verify: bool,
558 /// Stable JSON output for scripts.
559 #[arg(short, long)]
560 json: bool,
561 },
562 /// Release a file lock previously acquired with `git lfs lock`.
563 /// Either provide one or more paths, or `--id <id>` (mutually
564 /// exclusive).
565 Unlock {
566 /// Paths to unlock; mutually exclusive with `--id`.
567 paths: Vec<String>,
568 /// Lock id to release; mutually exclusive with paths.
569 #[arg(short, long)]
570 id: Option<String>,
571 /// Forcibly break another user's lock(s).
572 #[arg(short, long)]
573 force: bool,
574 /// Specify which remote to use when interacting with locks.
575 #[arg(short, long)]
576 remote: Option<String>,
577 /// Refspec to send with the unlock request (defaults to current
578 /// branch / tracked upstream).
579 #[arg(long = "ref")]
580 refspec: Option<String>,
581 /// Stable JSON output for scripts.
582 #[arg(short, long)]
583 json: bool,
584 },
585 /// List LFS-tracked files visible at a ref (default: HEAD), or across
586 /// all reachable history with `--all`.
587 LsFiles {
588 /// Ref to list. Defaults to HEAD.
589 refspec: Option<String>,
590 /// Show full 64-char OID instead of the 10-char prefix.
591 #[arg(short, long)]
592 long: bool,
593 /// Append humanized size in parens.
594 #[arg(short, long)]
595 size: bool,
596 /// Print only the path.
597 #[arg(short, long)]
598 name_only: bool,
599 /// Walk every reachable ref's full history.
600 #[arg(short, long)]
601 all: bool,
602 /// Multi-line per-file block (size, checkout, download, oid, version).
603 #[arg(short, long)]
604 debug: bool,
605 /// Stable JSON output for scripts.
606 #[arg(short, long)]
607 json: bool,
608 },
609}