Skip to main content

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}