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        /// Operate on `/etc/gitconfig` (`git config --system`).
226        #[arg(long)]
227        system: bool,
228        /// Operate on `.git/config.worktree` for the current worktree.
229        #[arg(long)]
230        worktree: bool,
231        /// Operate on the given config file directly. Treated as
232        /// "global-like" for the success message.
233        #[arg(long, value_name = "PATH")]
234        file: Option<std::path::PathBuf>,
235        /// Overwrite existing config and hooks.
236        #[arg(short, long)]
237        force: bool,
238        /// Only set the filter config; don't install hooks.
239        #[arg(long)]
240        skip_repo: bool,
241        /// Configure the smudge filter to pass pointer text through
242        /// unchanged. Use with a follow-up `git lfs pull` to download
243        /// content on demand.
244        #[arg(long)]
245        skip_smudge: bool,
246    },
247    /// Reverse of `install`: clear the `filter.lfs.*` config and remove
248    /// the LFS git hooks. Hooks that don't match what we'd write are left
249    /// untouched.
250    Uninstall {
251        /// Optional mode: `hooks` removes only the LFS git hooks and
252        /// leaves the filter config alone (the inverse of `--skip-repo`).
253        mode: Option<String>,
254        /// Operate on the local repo only (default: --global).
255        #[arg(short, long)]
256        local: bool,
257        /// Operate on `/etc/gitconfig` (`git config --system`).
258        #[arg(long)]
259        system: bool,
260        /// Operate on `.git/config.worktree` for the current worktree.
261        #[arg(long)]
262        worktree: bool,
263        /// Operate on the given config file directly. Treated as
264        /// "global-like" for the success message.
265        #[arg(long, value_name = "PATH")]
266        file: Option<std::path::PathBuf>,
267        /// Only unset config; don't touch hooks.
268        #[arg(long)]
269        skip_repo: bool,
270    },
271    /// Track a file pattern with git-lfs by adding it to .gitattributes.
272    /// With no patterns, lists currently-tracked patterns.
273    Track {
274        /// File patterns to track (e.g. "*.jpg", "data/*.bin").
275        patterns: Vec<String>,
276        /// Mark the tracked pattern as `lockable` (`*.psd lockable`).
277        #[arg(short = 'l', long)]
278        lockable: bool,
279        /// Re-track an existing pattern, removing its `lockable` flag.
280        #[arg(long)]
281        not_lockable: bool,
282        /// Print what would happen without modifying `.gitattributes` or
283        /// re-staging files.
284        #[arg(long)]
285        dry_run: bool,
286        /// Extra logging: print "Found N files previously added to Git
287        /// matching pattern" lines.
288        #[arg(short, long)]
289        verbose: bool,
290        /// Listing mode only: emit JSON instead of the human-readable
291        /// listing.
292        #[arg(long)]
293        json: bool,
294        /// Listing mode only: suppress the "Listing excluded patterns"
295        /// section.
296        #[arg(long)]
297        no_excluded: bool,
298        /// Treat each pattern as a literal filename — escape glob
299        /// metacharacters (`*`, `?`, `[`, `]`, backslash, space) so
300        /// the entry in `.gitattributes` matches that exact name even
301        /// when it contains shell-glob characters.
302        #[arg(long)]
303        filename: bool,
304        /// Don't modify `.gitattributes` — the user has already added
305        /// the LFS filter line themselves. Still walks the index and
306        /// touches matching files' mtime so they show as modified on
307        /// the next `git status`.
308        #[arg(long)]
309        no_modify_attrs: bool,
310    },
311    /// Stop tracking a file pattern with git-lfs by removing it from
312    /// .gitattributes. The matching pointer files in history (and the
313    /// objects in the local store) are left in place.
314    Untrack {
315        /// File patterns to untrack.
316        patterns: Vec<String>,
317    },
318    /// Run the long-running filter-process protocol with git over stdin/stdout.
319    /// This is what git invokes via filter.lfs.process and is the batched
320    /// alternative to per-invocation `clean`/`smudge`.
321    FilterProcess {
322        /// Pass smudge requests' pointer text through unchanged;
323        /// equivalent to `GIT_LFS_SKIP_SMUDGE=1`. Wired up by
324        /// `install --skip-smudge`.
325        #[arg(long)]
326        skip: bool,
327    },
328    /// Download every LFS object reachable from the given refs (default: HEAD)
329    /// that isn't already in the local store. Walks history, dedupes by OID.
330    Fetch {
331        /// First positional arg is treated as a remote name (if it
332        /// resolves); subsequent args are refs.
333        args: Vec<String>,
334        /// List the objects that would be fetched without downloading
335        /// them (one `fetch <oid> => <path>` line per object).
336        #[arg(long)]
337        dry_run: bool,
338        /// JSON output. With `--dry-run`, queries the server's batch
339        /// endpoint to populate `actions` URLs.
340        #[arg(long)]
341        json: bool,
342        /// Walk every local ref under `refs/heads/*` + `refs/tags/*`.
343        #[arg(long)]
344        all: bool,
345        /// Re-download objects we already have (e.g. recovery from a
346        /// corrupt local store).
347        #[arg(long)]
348        refetch: bool,
349        /// Read refs from stdin, one per line. Blank lines dropped.
350        #[arg(long)]
351        stdin: bool,
352        /// Run `prune` after the fetch completes.
353        #[arg(long)]
354        prune: bool,
355        /// Comma-separated globs; only matching paths are fetched.
356        /// Falls back to `lfs.fetchinclude` when omitted.
357        #[arg(short = 'I', long)]
358        include: Vec<String>,
359        /// Comma-separated globs; matching paths are skipped. Falls
360        /// back to `lfs.fetchexclude` when omitted.
361        #[arg(short = 'X', long)]
362        exclude: Vec<String>,
363    },
364    /// `fetch` then re-run the smudge filter so the working tree contains
365    /// real LFS file contents instead of pointer text. Requires
366    /// `git lfs install` to have wired up the smudge filter.
367    Pull {
368        /// Refs to scan for LFS pointers. Defaults to `HEAD`.
369        refs: Vec<String>,
370        /// Comma-separated globs; only matching paths are pulled.
371        /// Falls back to `lfs.fetchinclude` when omitted.
372        #[arg(short = 'I', long)]
373        include: Vec<String>,
374        /// Comma-separated globs; matching paths are skipped. Falls
375        /// back to `lfs.fetchexclude` when omitted.
376        #[arg(short = 'X', long)]
377        exclude: Vec<String>,
378    },
379    /// Upload every LFS object reachable from the given refs that the
380    /// remote doesn't already have. The "doesn't have" set is approximated
381    /// by `refs/remotes/<remote>/*`; the LFS server's batch API also
382    /// dedupes server-side so missing exclusions don't waste bandwidth.
383    Push {
384        /// Name of the remote (e.g. "origin") whose tracking refs are
385        /// excluded from the upload set.
386        remote: String,
387        /// Refs (or, with `--object-id`, raw OIDs) to push. With
388        /// `--all`, restricts the all-refs walk to these; with
389        /// `--stdin`, ignored (a warning is emitted).
390        args: Vec<String>,
391        /// List the objects that would be pushed without actually
392        /// uploading them (one `push <oid> => <path>` line per object).
393        #[arg(long)]
394        dry_run: bool,
395        /// Push every local ref under `refs/heads/*` and `refs/tags/*`
396        /// (intersected with `args` if any are given).
397        #[arg(long)]
398        all: bool,
399        /// Read refs (or OIDs, with `--object-id`) from stdin, one per
400        /// line. Blank lines are skipped.
401        #[arg(long)]
402        stdin: bool,
403        /// Treat positional args / stdin entries as raw LFS OIDs
404        /// rather than git refs, and upload those objects directly
405        /// from the local store.
406        #[arg(long)]
407        object_id: bool,
408    },
409    /// Deprecated. Wraps `git clone` so the working tree is populated
410    /// with pointer text first, then runs `git lfs pull` to download
411    /// LFS content in batch. Modern `git clone` parallelizes the
412    /// smudge filter and is no slower; prefer it.
413    Clone {
414        /// `git clone` and LFS pass-through args. The repository URL
415        /// is required; an optional target directory follows.
416        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
417        args: Vec<String>,
418    },
419    /// Git post-checkout hook entry point. Receives `<prev-sha>
420    /// <post-sha> <flag>` (flag is "1" if HEAD moved). Currently a
421    /// no-op stub — exists so installed hook scripts don't fail. Real
422    /// behavior arrives with `track --lockable`.
423    PostCheckout { args: Vec<String> },
424    /// Git post-commit hook entry point. No arguments. Currently a
425    /// no-op stub.
426    PostCommit { args: Vec<String> },
427    /// Git post-merge hook entry point. Receives `<squash-flag>`.
428    /// Currently a no-op stub.
429    PostMerge { args: Vec<String> },
430    /// Git pre-push hook entry point — not typically invoked by hand.
431    /// Reads `<local-ref> <local-sha> <remote-ref> <remote-sha>` lines
432    /// from stdin and uploads the LFS objects newly reachable from each
433    /// `<local-sha>`.
434    PrePush {
435        /// Name of the remote being pushed to.
436        remote: String,
437        /// URL of the remote (informational; we use `lfs.url` config).
438        url: Option<String>,
439        /// List the objects that would be pushed without actually
440        /// uploading them.
441        #[arg(long)]
442        dry_run: bool,
443    },
444    /// Print the git-lfs version and exit.
445    Version,
446    /// Debug helper: build a pointer from a file, parse one from disk
447    /// or stdin, or just check whether some bytes are a valid pointer.
448    Pointer {
449        /// Build a pointer from this file (read content, hash, encode).
450        #[arg(short, long)]
451        file: Option<PathBuf>,
452        /// Parse and display this existing pointer file.
453        #[arg(short, long)]
454        pointer: Option<PathBuf>,
455        /// Read a pointer from stdin (mutually exclusive with --pointer).
456        #[arg(long)]
457        stdin: bool,
458        /// Validity check mode: exit 0 if input parses, 1 if not, 2 if
459        /// `--strict` and not byte-canonical.
460        #[arg(long)]
461        check: bool,
462        /// In `--check`, also reject non-canonical pointers.
463        #[arg(long)]
464        strict: bool,
465        /// Explicitly disable strict mode (paired with `--strict`).
466        #[arg(long)]
467        no_strict: bool,
468    },
469    /// Show the LFS environment: version, endpoints, on-disk paths, and
470    /// the three `filter.lfs.*` config values.
471    Env,
472    /// List the configured LFS pointer extensions (`lfs.extension.<name>.*`).
473    /// Extensions chain external clean/smudge programs around each LFS
474    /// object; this prints their resolved configuration in priority order.
475    Ext,
476    /// (Re-)install the four LFS git hooks (`pre-push`, `post-checkout`,
477    /// `post-commit`, `post-merge`) for the current repository.
478    Update {
479        /// Overwrite any custom hook contents.
480        #[arg(long)]
481        force: bool,
482        /// Print install instructions instead of writing the hook files.
483        #[arg(long)]
484        manual: bool,
485    },
486    /// Analyze or rewrite history for LFS conversion. Phase 1 ships
487    /// `info` only; `import` and `export` will land in subsequent phases.
488    Migrate {
489        #[command(subcommand)]
490        cmd: MigrateCmd,
491    },
492    /// Replace pointer text in the working tree with actual LFS object
493    /// content. With no args, materializes every LFS pointer in HEAD's
494    /// tree. With paths (literal file names or trailing-slash directory
495    /// prefixes), restricts to matching pointers.
496    ///
497    /// During a merge conflict, `--to <path> --ours/--theirs/--base
498    /// <file>` writes the LFS content from one of the conflicted
499    /// stages to `<path>` (creating intermediate directories) so the
500    /// user can compare or salvage versions.
501    Checkout {
502        /// Paths to check out. Empty = everything in HEAD's tree.
503        /// In conflict mode (`--to`), exactly one path is required.
504        paths: Vec<String>,
505        /// Conflict-mode: write the chosen stage's content to this
506        /// path instead of into the working tree. Resolves relative
507        /// to the current directory.
508        #[arg(long, value_name = "PATH")]
509        to: Option<String>,
510        /// Conflict-mode: pull from stage 2 (HEAD's version). Mutually
511        /// exclusive with `--theirs` and `--base`.
512        #[arg(long)]
513        ours: bool,
514        /// Conflict-mode: pull from stage 3 (the merging-in version).
515        #[arg(long)]
516        theirs: bool,
517        /// Conflict-mode: pull from stage 1 (the common ancestor).
518        #[arg(long)]
519        base: bool,
520    },
521    /// Delete local LFS objects that aren't reachable from HEAD or any
522    /// unpushed commit. Reclaims disk for repos whose history has moved
523    /// past their objects.
524    Prune {
525        /// Don't delete anything; just report what would go.
526        #[arg(short, long)]
527        dry_run: bool,
528        /// Print each prunable object's OID and size.
529        #[arg(short, long)]
530        verbose: bool,
531    },
532    /// Check the integrity of LFS objects and pointers reachable from
533    /// `<refspec>` (default: HEAD). Exit 1 if anything is corrupt.
534    Fsck {
535        /// Ref to scan. Defaults to HEAD.
536        refspec: Option<String>,
537        /// Only check objects (verify store contents match pointer OIDs).
538        #[arg(long)]
539        objects: bool,
540        /// Only check pointers (flag non-canonical pointer encodings).
541        #[arg(long)]
542        pointers: bool,
543        /// Report problems but don't move corrupt objects to `<lfs>/bad/`.
544        #[arg(short, long)]
545        dry_run: bool,
546    },
547    /// Show staged + unstaged changes, classifying each blob as LFS,
548    /// Git, or working-tree File.
549    Status {
550        /// Stable one-line-per-change format for scripts.
551        #[arg(short, long)]
552        porcelain: bool,
553        /// Stable JSON output for scripts; only LFS entries are reported.
554        #[arg(short, long)]
555        json: bool,
556    },
557    /// Acquire an exclusive server-side lock on one or more files.
558    /// Other users will be unable to push changes to a locked file.
559    Lock {
560        /// Paths to lock (repo-relative or absolute, must resolve inside
561        /// the working tree).
562        paths: Vec<String>,
563        /// Specify which remote to use when interacting with locks.
564        #[arg(short, long)]
565        remote: Option<String>,
566        /// Refspec to associate the lock with. Defaults to the current
567        /// branch's tracked upstream (`branch.<current>.merge`) or the
568        /// current branch's full ref (`refs/heads/<branch>`).
569        #[arg(long = "ref")]
570        refspec: Option<String>,
571        /// Stable JSON output for scripts.
572        #[arg(short, long)]
573        json: bool,
574    },
575    /// List file locks held on the server.
576    Locks {
577        /// Specify which remote to use when interacting with locks.
578        #[arg(short, long)]
579        remote: Option<String>,
580        /// Filter results to a particular path.
581        #[arg(short, long)]
582        path: Option<String>,
583        /// Filter results to a particular lock id.
584        #[arg(short, long)]
585        id: Option<String>,
586        /// Maximum number of results to return.
587        #[arg(short, long)]
588        limit: Option<u32>,
589        /// Refspec to filter locks by (defaults to current branch /
590        /// tracked upstream — same auto-resolution as `git lfs lock`).
591        #[arg(long = "ref")]
592        refspec: Option<String>,
593        /// Verify ownership: prefix locks owned by the authenticated user
594        /// with `O ` (others get `  `).
595        #[arg(long)]
596        verify: bool,
597        /// List from the on-disk cache of own locks instead of querying
598        /// the server. Combine with `--path` / `--id` / `--limit` to
599        /// filter; `--verify` is rejected. Useful when offline or to
600        /// confirm what `git lfs lock` recorded locally.
601        #[arg(long)]
602        local: bool,
603        /// Stable JSON output for scripts.
604        #[arg(short, long)]
605        json: bool,
606    },
607    /// Release a file lock previously acquired with `git lfs lock`.
608    /// Either provide one or more paths, or `--id <id>` (mutually
609    /// exclusive).
610    Unlock {
611        /// Paths to unlock; mutually exclusive with `--id`.
612        paths: Vec<String>,
613        /// Lock id to release; mutually exclusive with paths.
614        #[arg(short, long)]
615        id: Option<String>,
616        /// Forcibly break another user's lock(s).
617        #[arg(short, long)]
618        force: bool,
619        /// Specify which remote to use when interacting with locks.
620        #[arg(short, long)]
621        remote: Option<String>,
622        /// Refspec to send with the unlock request (defaults to current
623        /// branch / tracked upstream).
624        #[arg(long = "ref")]
625        refspec: Option<String>,
626        /// Stable JSON output for scripts.
627        #[arg(short, long)]
628        json: bool,
629    },
630    /// List LFS-tracked files visible at a ref (default: HEAD), or across
631    /// all reachable history with `--all`.
632    LsFiles {
633        /// Ref to list. Defaults to HEAD.
634        refspec: Option<String>,
635        /// Show full 64-char OID instead of the 10-char prefix.
636        #[arg(short, long)]
637        long: bool,
638        /// Append humanized size in parens.
639        #[arg(short, long)]
640        size: bool,
641        /// Print only the path.
642        #[arg(short, long)]
643        name_only: bool,
644        /// Walk every reachable ref's full history.
645        #[arg(short, long)]
646        all: bool,
647        /// Multi-line per-file block (size, checkout, download, oid, version).
648        #[arg(short, long)]
649        debug: bool,
650        /// Stable JSON output for scripts.
651        #[arg(short, long)]
652        json: bool,
653    },
654}