Skip to main content

git_branchless_opts/
lib.rs

1//! The command-line options for `git-branchless`.
2
3#![warn(missing_docs)]
4#![warn(
5    clippy::all,
6    clippy::as_conversions,
7    clippy::clone_on_ref_ptr,
8    clippy::dbg_macro
9)]
10#![allow(clippy::too_many_arguments, clippy::blocks_in_conditions)]
11// These URLs are printed verbatim in help output, so we don't want to add extraneous Markdown
12// formatting.
13#![allow(rustdoc::bare_urls)]
14
15use std::ffi::OsString;
16use std::fmt::Display;
17use std::path::{Path, PathBuf};
18use std::str::FromStr;
19
20use clap::{Args, Command as ClapCommand, CommandFactory, Parser, ValueEnum};
21use lib::core::untracked_file_cache::UntrackedFileStrategy;
22use lib::git::NonZeroOid;
23
24/// A revset expression. Can be a commit hash, branch name, or one of the
25/// various revset functions.
26#[derive(Clone, Debug)]
27pub struct Revset(pub String);
28
29impl FromStr for Revset {
30    type Err = std::convert::Infallible;
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        Ok(Self(s.to_string()))
34    }
35}
36
37impl Display for Revset {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "{}", self.0)
40    }
41}
42
43/// A command wrapped by `git-branchless wrap`. The arguments are forwarded to
44/// `git`.
45#[derive(Debug, Parser)]
46pub enum WrappedCommand {
47    /// The wrapped command.
48    #[clap(external_subcommand)]
49    WrappedCommand(Vec<String>),
50}
51
52/// Options for resolving revset expressions.
53#[derive(Args, Debug, Default)]
54pub struct ResolveRevsetOptions {
55    /// Include hidden commits in the results of evaluating revset expressions.
56    #[clap(action, long = "hidden")]
57    pub show_hidden_commits: bool,
58}
59
60/// Options for moving commits.
61#[derive(Args, Debug)]
62pub struct MoveOptions {
63    /// Force moving public commits, even though other people may have access to
64    /// those commits.
65    #[clap(action, short = 'f', long = "force-rewrite", visible_alias = "fr")]
66    pub force_rewrite_public_commits: bool,
67
68    /// Only attempt to perform an in-memory rebase. If it fails, do not
69    /// attempt an on-disk rebase.
70    #[clap(action, long = "in-memory", conflicts_with_all(&["force_on_disk", "merge"]))]
71    pub force_in_memory: bool,
72
73    /// Skip attempting to use an in-memory rebase, and try an
74    /// on-disk rebase directly.
75    #[clap(action, long = "on-disk")]
76    pub force_on_disk: bool,
77
78    /// Don't attempt to deduplicate commits. Normally, a commit with the same
79    /// contents as another commit which has already been applied to the target
80    /// branch is skipped. If set, this flag skips that check.
81    #[clap(action(clap::ArgAction::SetFalse), long = "no-deduplicate-commits")]
82    pub detect_duplicate_commits_via_patch_id: bool,
83
84    /// Attempt to resolve merge conflicts, if any. If a merge conflict
85    /// occurs and this option is not set, the operation is aborted.
86    #[clap(action, name = "merge", short = 'm', long = "merge")]
87    pub resolve_merge_conflicts: bool,
88
89    /// Keep all contents of descendant commits exactly the same (i.e. "reparent" them).
90    /// This can be useful when applying formatting or refactoring changes.
91    /// Use with care if the descendants are moved from a far-away branch, as it may
92    /// carry along a lot of unintended changes onto the destination.
93    #[clap(long, conflicts_with = "merge")]
94    pub reparent: bool,
95
96    /// Debugging option. Print the constraints used to create the rebase
97    /// plan before executing it.
98    #[clap(action, long = "debug-dump-rebase-constraints")]
99    pub dump_rebase_constraints: bool,
100
101    /// Debugging option. Print the rebase plan that will be executed before
102    /// executing it.
103    #[clap(action, long = "debug-dump-rebase-plan")]
104    pub dump_rebase_plan: bool,
105}
106
107/// Options for traversing commits.
108#[derive(Args, Debug)]
109pub struct TraverseCommitsOptions {
110    /// The number of commits to traverse.
111    ///
112    /// If not provided, defaults to 1.
113    #[clap(value_parser)]
114    pub num_commits: Option<usize>,
115
116    /// Traverse as many commits as possible.
117    #[clap(action, short = 'a', long = "all")]
118    pub all_the_way: bool,
119
120    /// Move the specified number of branches rather than commits.
121    #[clap(action, short = 'b', long = "branch")]
122    pub move_by_branches: bool,
123
124    /// When encountering multiple next commits, choose the oldest.
125    #[clap(action, short = 'o', long = "oldest")]
126    pub oldest: bool,
127
128    /// When encountering multiple next commits, choose the newest.
129    #[clap(action, short = 'n', long = "newest", conflicts_with("oldest"))]
130    pub newest: bool,
131
132    /// When encountering multiple next commits, interactively prompt which to
133    /// advance to.
134    #[clap(
135        action,
136        short = 'i',
137        long = "interactive",
138        conflicts_with("newest"),
139        conflicts_with("oldest")
140    )]
141    pub interactive: bool,
142
143    /// If the local changes conflict with the destination commit, attempt to
144    /// merge them.
145    #[clap(action, short = 'm', long = "merge")]
146    pub merge: bool,
147
148    /// If the local changes conflict with the destination commit, discard them.
149    /// (Use with caution!)
150    #[clap(action, short = 'f', long = "force", conflicts_with("merge"))]
151    pub force: bool,
152}
153
154/// Options for checking out a commit.
155#[derive(Args, Debug)]
156pub struct SwitchOptions {
157    /// Interactively select a commit to check out.
158    #[clap(action, short = 'i', long = "interactive")]
159    pub interactive: bool,
160
161    /// When checking out the target commit, also create a branch with the
162    /// provided name pointing to that commit.
163    #[clap(value_parser, short = 'c', long = "create")]
164    pub branch_name: Option<String>,
165
166    /// Forcibly switch commits, discarding any working copy changes if
167    /// necessary.
168    #[clap(action, short = 'f', long = "force")]
169    pub force: bool,
170
171    /// If the current working copy changes do not apply cleanly to the
172    /// target commit, start merge conflict resolution instead of aborting.
173    #[clap(action, short = 'm', long = "merge", conflicts_with("force"))]
174    pub merge: bool,
175
176    /// If the target is a branch, switch to that branch and immediately detach
177    /// from it.
178    #[clap(action, short = 'd', long = "detach")]
179    pub detach: bool,
180
181    /// The commit or branch to check out. If not provided, defaults to the
182    /// current commit.
183    ///
184    /// If a revset is provided, it must evaluate to set with exactly 1 head.
185    ///
186    /// If this is provided and the `--interactive` flag is passed, this
187    /// text is used to pre-fill the interactive commit selector.
188    #[clap(value_parser)]
189    pub target: Option<Revset>,
190}
191
192/// Internal use.
193#[derive(Debug, Parser)]
194pub enum HookSubcommand {
195    /// Internal use.
196    DetectEmptyCommit {
197        /// The OID of the commit currently being applied, to be checked for emptiness.
198        #[clap(value_parser)]
199        old_commit_oid: String,
200    },
201    /// Internal use.
202    PreAutoGc,
203    /// Internal use.
204    PostApplypatch,
205    /// Internal use.
206    PostCheckout {
207        /// The previous commit OID.
208        #[clap(value_parser)]
209        previous_commit: String,
210
211        /// The current commit OID.
212        #[clap(value_parser)]
213        current_commit: String,
214
215        /// Whether or not this was a branch checkout (versus a file checkout).
216        #[clap(value_parser)]
217        is_branch_checkout: isize,
218    },
219    /// Internal use.
220    PostCommit,
221    /// Internal use.
222    PostMerge {
223        /// Whether or not this is a squash merge. See githooks(5).
224        #[clap(value_parser)]
225        is_squash_merge: isize,
226    },
227    /// Internal use.
228    PostRewrite {
229        /// One of `amend` or `rebase`.
230        #[clap(value_parser)]
231        rewrite_type: String,
232    },
233    /// Internal use.
234    ReferenceTransaction {
235        /// One of `prepared`, `committed`, or `aborted`. See githooks(5).
236        #[clap(value_parser)]
237        transaction_state: String,
238    },
239    /// Internal use.
240    RegisterExtraPostRewriteHook,
241    /// Internal use.
242    SkipUpstreamAppliedCommit {
243        /// The OID of the commit that was skipped.
244        #[clap(value_parser)]
245        commit_oid: String,
246    },
247}
248
249/// Internal use.
250#[derive(Debug, Parser)]
251pub struct HookArgs {
252    /// The subcommand to run.
253    #[clap(subcommand)]
254    pub subcommand: HookSubcommand,
255}
256
257/// Initialize the branchless workflow for this repository.
258#[derive(Debug, Parser)]
259pub struct InitArgs {
260    /// Uninstall the branchless workflow instead of initializing it.
261    #[clap(action, long = "uninstall")]
262    pub uninstall: bool,
263
264    /// Use the provided name as the name of the main branch.
265    ///
266    /// If not set, it will be auto-detected. If it can't be auto-detected,
267    /// then you will be prompted to enter a value for the main branch name.
268    #[clap(value_parser, long = "main-branch", conflicts_with = "uninstall")]
269    pub main_branch_name: Option<String>,
270}
271
272/// Install git-branchless's man-pages to the given path.
273#[derive(Debug, Parser)]
274pub struct InstallManPagesArgs {
275    /// The path to install to. An example path might be `/usr/share/man`. The
276    /// provded path will be appended with `man1`, etc., as appropriate.
277    pub path: PathBuf,
278}
279
280/// Query the commit graph using the "revset" language and print matching
281/// commits.
282///
283/// See https://github.com/arxanas/git-branchless/wiki/Reference:-Revsets to
284/// learn more about revsets.
285///
286/// The outputted commits are guaranteed to be topologically sorted, with
287/// ancestor commits appearing first.
288#[derive(Debug, Parser)]
289pub struct QueryArgs {
290    /// The query to execute.
291    #[clap(value_parser)]
292    pub revset: Revset,
293
294    /// Options for resolving revset expressions.
295    #[clap(flatten)]
296    pub resolve_revset_options: ResolveRevsetOptions,
297
298    /// Print the branches attached to the resulting commits, rather than the commits themselves.
299    #[clap(action, short = 'b', long = "branches")]
300    pub show_branches: bool,
301
302    /// Print the OID of each matching commit, one per line. This output is
303    /// stable for use in scripts.
304    #[clap(action, short = 'r', long = "raw", conflicts_with("show_branches"))]
305    pub raw: bool,
306}
307
308/// Specify commit messages
309#[derive(Debug, Parser)]
310pub struct MessageArgs {
311    /// The commit message to use. Multiple messages will be combined
312    /// as separate paragraphs, similar to `git commit`.
313    /// If not provided, you will be prompted to provide a commit message
314    /// interactively.
315    #[clap(value_parser, short = 'm', long = "message")]
316    pub messages: Vec<String>,
317
318    /// A commit to "fix up". The message will be prefixed with `fixup!`
319    /// following the supplied commit, suitable for use with `git rebase --autosquash`.
320    #[clap(value_parser, long = "fixup", conflicts_with_all(&["messages"]))]
321    pub commit_to_fixup: Option<Revset>,
322}
323
324/// Create a commit by interactively selecting which changes to include.
325#[derive(Debug, Parser)]
326pub struct RecordArgs {
327    /// Options for supplying commit messages.
328    #[clap(flatten)]
329    pub message_args: MessageArgs,
330
331    /// Select changes to include interactively, rather than using the
332    /// current staged/unstaged changes.
333    #[clap(action, short = 'i', long = "interactive")]
334    pub interactive: bool,
335
336    /// Create and switch to a new branch with the given name before
337    /// committing.
338    #[clap(action, short = 'c', long = "create")]
339    pub create: Option<String>,
340
341    /// Detach the current branch before committing.
342    #[clap(action, short = 'd', long = "detach", conflicts_with("create"))]
343    pub detach: bool,
344
345    /// Insert the new commit between the current commit and its children,
346    /// if any.
347    #[clap(action, short = 'I', long = "insert")]
348    pub insert: bool,
349
350    /// After making the new commit, switch back to the previous commit.
351    #[clap(action, short = 's', long = "stash", conflicts_with_all(&["create", "detach"]))]
352    pub stash: bool,
353
354    /// How should newly encountered, untracked files be handled?
355    #[clap(value_parser, long = "untracked", conflicts_with_all(&["interactive"]))]
356    pub untracked_file_strategy: Option<UntrackedFileStrategy>,
357}
358
359/// Display a nice graph of the commits you've recently worked on.
360#[derive(Debug, Parser)]
361pub struct SmartlogArgs {
362    /// The point in time at which to show the smartlog. If not provided,
363    /// renders the smartlog as of the current time. If negative, is treated
364    /// as an offset from the current event.
365    #[clap(value_parser, long = "event-id")]
366    pub event_id: Option<isize>,
367
368    /// The commits to render. These commits, plus any related commits, will
369    /// be rendered.
370    #[clap(value_parser)]
371    pub revset: Option<Revset>,
372
373    /// Print the smartlog in the opposite of the usual order, with the latest
374    /// commits first. (DEPRECATED: should be configured with `branchless.smartlog.reverse`)
375    #[clap(long)]
376    pub reverse: bool,
377
378    /// Don't automatically add HEAD and the main branch to the list of commits
379    /// to present. They will still be added if included in the revset.
380    #[clap(long)]
381    pub exact: bool,
382
383    /// Options for resolving revset expressions.
384    #[clap(flatten)]
385    pub resolve_revset_options: ResolveRevsetOptions,
386}
387
388/// The Git hosting provider to use, called a "forge".
389#[derive(Clone, Debug, ValueEnum)]
390pub enum ForgeKind {
391    /// Force-push branches to the default push remote. You can configure the
392    /// default push remote with `git config remote.pushDefault <remote>`.
393    Branch,
394
395    /// Force-push branches to the remote and create a pull request for each
396    /// branch using the `gh` command-line tool. WARNING: likely buggy!
397    Github,
398
399    /// Submit code reviews to Phabricator using the `arc` command-line tool.
400    Phabricator,
401}
402
403/// Push commits to a remote.
404#[derive(Debug, Parser)]
405pub struct SubmitArgs {
406    /// The commits to push to the forge. Unless `--create` is passed, this will
407    /// only push commits that already have associated remote objects on the
408    /// forge.
409    #[clap(value_parser, default_value = "stack()")]
410    pub revsets: Vec<Revset>,
411
412    /// Options for resolving revset expressions.
413    #[clap(flatten)]
414    pub resolve_revset_options: ResolveRevsetOptions,
415
416    /// The Git hosting provider to use, called a "forge". If not provided, an
417    /// attempt will be made to automatically detect the forge used by the
418    /// repository. If no forge can be detected, will fall back to the "branch"
419    /// forge.
420    #[clap(short = 'F', long = "forge")]
421    pub forge_kind: Option<ForgeKind>,
422
423    /// If there is no associated remote commit or code review object for a
424    /// given local commit, create the remote object by pushing the local commit
425    /// to the forge.
426    #[clap(action, short = 'c', long = "create")]
427    pub create: bool,
428
429    /// If the forge supports it, create code reviews in "draft" mode.
430    #[clap(action, short = 'd', long = "draft")]
431    pub draft: bool,
432
433    /// If the forge supports it, an optional message to include with the create
434    /// or update operation.
435    #[clap(short = 'm', long = "message")]
436    pub message: Option<String>,
437
438    /// If the forge supports it, how many jobs to execute in parallel. The
439    /// value `0` indicates to use all CPUs.
440    #[clap(short = 'j', long = "jobs")]
441    pub num_jobs: Option<usize>,
442
443    /// If the forge supports it and uses a tool that needs access to the
444    /// working copy, what kind of execution strategy to use.
445    #[clap(short = 's', long = "strategy")]
446    pub execution_strategy: Option<TestExecutionStrategy>,
447
448    /// Don't push or create anything. Instead, report what would be pushed or
449    /// created. (This may still trigger fetching information from the forge.)
450    #[clap(short = 'n', long = "dry-run")]
451    pub dry_run: bool,
452}
453
454/// Run a command on each commit in a given set and aggregate the results.
455#[derive(Debug, Parser)]
456pub struct TestArgs {
457    /// The subcommand to run.
458    #[clap(subcommand)]
459    pub subcommand: TestSubcommand,
460}
461
462/// FIXME: write man-page text
463#[derive(Debug, Parser)]
464pub enum Command {
465    /// Amend the current HEAD commit.
466    Amend {
467        /// Options for moving commits.
468        #[clap(flatten)]
469        move_options: MoveOptions,
470
471        /// How should newly encountered, untracked files be handled?
472        #[clap(action, long = "untracked")]
473        untracked_file_strategy: Option<UntrackedFileStrategy>,
474    },
475
476    /// Gather information about recent operations to upload as part of a bug
477    /// report.
478    BugReport,
479
480    /// Use the partial commit selector UI as a Git-compatible difftool; see
481    /// git-difftool(1) for more information on Git difftools.
482    Difftool(scm_diff_editor::Opts),
483
484    /// Run internal garbage collection.
485    Gc,
486
487    /// Hide the provided commits from the smartlog.
488    Hide {
489        /// Zero or more commits to hide.
490        #[clap(value_parser)]
491        revsets: Vec<Revset>,
492
493        /// Options for resolving revset expressions.
494        #[clap(flatten)]
495        resolve_revset_options: ResolveRevsetOptions,
496
497        /// Don't delete branches that point to commits that would be hidden.
498        /// (Those commits will remain visible as a result.)
499        #[clap(action, long = "no-delete-branches")]
500        no_delete_branches: bool,
501
502        /// Also recursively hide all visible children commits of the provided
503        /// commits.
504        #[clap(action, short = 'r', long = "recursive")]
505        recursive: bool,
506    },
507
508    /// Internal use.
509    #[clap(hide = true)]
510    Hook(HookArgs),
511
512    /// Initialize the branchless workflow for this repository.
513    Init(InitArgs),
514
515    /// Install git-branchless's man-pages to the given path.
516    InstallManPages(InstallManPagesArgs),
517
518    /// Move a subtree of commits from one location to another.
519    ///
520    /// By default, `git move` tries to move the entire current stack if you
521    /// don't pass a `--source` or `--base` option (equivalent to writing
522    /// `--base HEAD`).
523    ///
524    /// By default, `git move` attempts to rebase all commits in-memory. If you
525    /// want to force an on-disk rebase, pass the `--on-disk` flag. Note that
526    /// `post-commit` hooks are not called during in-memory rebases.
527    Move {
528        /// The source commit to move. This commit, and all of its descendants,
529        /// will be moved.
530        #[clap(action(clap::ArgAction::Append), short = 's', long = "source")]
531        source: Vec<Revset>,
532
533        /// A commit inside a subtree to move. The entire subtree, starting from
534        /// the main branch, will be moved, not just the commits descending from
535        /// this commit.
536        #[clap(
537            action(clap::ArgAction::Append),
538            short = 'b',
539            long = "base",
540            conflicts_with = "source"
541        )]
542        base: Vec<Revset>,
543
544        /// A set of specific commits to move. These will be removed from their
545        /// current locations and any unmoved children will be moved to their
546        /// nearest unmoved ancestor.
547        #[clap(
548            action(clap::ArgAction::Append),
549            short = 'x',
550            long = "exact",
551            conflicts_with_all(&["source", "base"])
552        )]
553        exact: Vec<Revset>,
554
555        /// The destination commit to move all source commits onto. If not
556        /// provided, defaults to the current commit.
557        #[clap(value_parser, short = 'd', long = "dest")]
558        dest: Option<Revset>,
559
560        /// Options for resolving revset expressions.
561        #[clap(flatten)]
562        resolve_revset_options: ResolveRevsetOptions,
563
564        /// Options for moving commits.
565        #[clap(flatten)]
566        move_options: MoveOptions,
567
568        /// Combine the moved commits and squash them into the destination commit.
569        #[clap(action, short = 'F', long = "fixup", conflicts_with_all(&["insert", "reparent"]))]
570        fixup: bool,
571
572        /// Insert the subtree between the destination and it's children, if any.
573        /// Only supported if the moved subtree has a single head.
574        #[clap(action, short = 'I', long = "insert")]
575        insert: bool,
576
577        /// Test whether an in-memory rebase would succeed.
578        #[clap(action, long = "dry-run", conflicts_with = "force_on_disk")]
579        dry_run: bool,
580    },
581
582    /// Move to a later commit in the current stack.
583    Next {
584        /// Options for traversing commits.
585        #[clap(flatten)]
586        traverse_commits_options: TraverseCommitsOptions,
587    },
588
589    /// Move to an earlier commit in the current stack.
590    Prev {
591        /// Options for traversing commits.
592        #[clap(flatten)]
593        traverse_commits_options: TraverseCommitsOptions,
594    },
595
596    /// Query the commit graph using the "revset" language and print matching
597    /// commits.
598    ///
599    /// See https://github.com/arxanas/git-branchless/wiki/Reference:-Revsets to
600    /// learn more about revsets.
601    ///
602    /// The outputted commits are guaranteed to be topologically sorted, with
603    /// ancestor commits appearing first.
604    Query(QueryArgs),
605
606    /// Restore internal invariants by reconciling the internal operation log
607    /// with the state of the Git repository.
608    Repair {
609        /// Apply changes.
610        #[clap(action(clap::ArgAction::SetFalse), long = "no-dry-run")]
611        dry_run: bool,
612    },
613
614    /// Fix up commits abandoned by a previous rewrite operation.
615    Restack {
616        /// The IDs of the abandoned commits whose descendants should be
617        /// restacked. If not provided, all abandoned commits are restacked.
618        #[clap(value_parser, default_value = "draft()")]
619        revsets: Vec<Revset>,
620
621        /// Options for resolving revset expressions.
622        #[clap(flatten)]
623        resolve_revset_options: ResolveRevsetOptions,
624
625        /// Options for moving commits.
626        #[clap(flatten)]
627        move_options: MoveOptions,
628    },
629
630    /// Create a commit by interactively selecting which changes to include.
631    Record(RecordArgs),
632
633    /// Reword commits.
634    Reword {
635        /// Zero or more commits to reword.
636        #[clap(
637            value_parser,
638            default_value = "stack() | @",
639            default_value_if("commit_to_fixup", clap::builder::ArgPredicate::IsPresent, "@"),
640            default_value_if("messages", clap::builder::ArgPredicate::IsPresent, "@")
641        )]
642        revsets: Vec<Revset>,
643
644        /// Options for resolving revset expressions.
645        #[clap(flatten)]
646        resolve_revset_options: ResolveRevsetOptions,
647
648        /// Force rewording public commits, even though other people may have access to
649        /// those commits.
650        #[clap(action, short = 'f', long = "force-rewrite", visible_alias = "fr")]
651        force_rewrite_public_commits: bool,
652
653        /// Options for supplying commit messages.
654        #[clap(flatten)]
655        message_args: MessageArgs,
656
657        /// Throw away the original commit messages.
658        ///
659        /// If `commit.template` is set, then the editor is pre-populated with
660        /// that; otherwise, the editor starts empty.
661        #[clap(action, short = 'd', long = "discard", conflicts_with_all(&["messages", "commit_to_fixup"]))]
662        discard: bool,
663    },
664
665    /// `smartlog` command.
666    Smartlog(SmartlogArgs),
667
668    #[clap(hide = true)]
669    /// Manage working copy snapshots.
670    Snapshot {
671        /// The subcommand to run.
672        #[clap(subcommand)]
673        subcommand: SnapshotSubcommand,
674    },
675
676    /// Split commits.
677    Split {
678        /// Commit to split. If a revset is given, it must resolve to a single commit.
679        #[clap(value_parser)]
680        revset: Revset,
681
682        /// Files to extract from the commit.
683        #[clap(value_parser, required = true)]
684        files: Vec<String>,
685
686        /// Insert the extracted commit before (as a parent of) the split commit.
687        #[clap(action, short = 'b', long)]
688        before: bool,
689
690        /// Restack any descendents onto the split commit, not the extracted commit.
691        #[clap(action, short = 'd', long)]
692        detach: bool,
693
694        /// After extracting the changes, don't recommit them.
695        #[clap(action, short = 'D', long = "discard", conflicts_with("detach"))]
696        discard: bool,
697
698        /// Options for resolving revset expressions.
699        #[clap(flatten)]
700        resolve_revset_options: ResolveRevsetOptions,
701
702        /// Options for moving commits.
703        #[clap(flatten)]
704        move_options: MoveOptions,
705    },
706
707    /// Push commits to a remote.
708    Submit(SubmitArgs),
709
710    /// Switch to the provided branch or commit.
711    Switch {
712        /// Options for switching.
713        #[clap(flatten)]
714        switch_options: SwitchOptions,
715    },
716
717    /// Move any local commit stacks on top of the main branch.
718    Sync {
719        /// Run `git fetch` to update remote references before carrying out the
720        /// sync.
721        #[clap(
722            action,
723            short = 'p',
724            long = "pull",
725            visible_short_alias = 'u',
726            visible_alias = "--update"
727        )]
728        pull: bool,
729
730        /// Options for moving commits.
731        #[clap(flatten)]
732        move_options: MoveOptions,
733
734        /// The commits whose stacks will be moved on top of the main branch. If
735        /// no commits are provided, all draft commits will be synced.
736        #[clap(value_parser)]
737        revsets: Vec<Revset>,
738
739        /// Options for resolving revset expressions.
740        #[clap(flatten)]
741        resolve_revset_options: ResolveRevsetOptions,
742    },
743
744    /// Run a command on each commit in a given set and aggregate the results.
745    Test(TestArgs),
746
747    /// Browse or return to a previous state of the repository.
748    Undo {
749        /// Interactively browse through previous states of the repository
750        /// before selecting one to return to.
751        #[clap(action, short = 'i', long = "interactive")]
752        interactive: bool,
753
754        /// Skip confirmation and apply changes immediately.
755        #[clap(action, short = 'y', long = "yes")]
756        yes: bool,
757    },
758
759    /// Unhide previously-hidden commits from the smartlog.
760    Unhide {
761        /// Zero or more commits to unhide.
762        #[clap(value_parser)]
763        revsets: Vec<Revset>,
764
765        /// Options for resolving revset expressions.
766        #[clap(flatten)]
767        resolve_revset_options: ResolveRevsetOptions,
768
769        /// Also recursively unhide all children commits of the provided commits.
770        #[clap(action, short = 'r', long = "recursive")]
771        recursive: bool,
772    },
773
774    /// Wrap a Git command inside a branchless transaction.
775    Wrap {
776        /// The `git` executable to invoke.
777        #[clap(value_parser, long = "git-executable")]
778        git_executable: Option<PathBuf>,
779
780        /// The arguments to pass to `git`.
781        #[clap(subcommand)]
782        command: WrappedCommand,
783    },
784}
785
786/// Whether to display terminal colors.
787#[derive(Clone, Debug, ValueEnum)]
788pub enum ColorSetting {
789    /// Automatically determine whether to display colors from the terminal and environment variables.
790    /// This is the default behavior.
791    Auto,
792    /// Always display terminal colors.
793    Always,
794    /// Never display terminal colors.
795    Never,
796}
797
798/// How to execute tests.
799#[derive(Clone, Copy, Debug, ValueEnum)]
800pub enum TestExecutionStrategy {
801    /// Default. Run the tests in the working copy. This requires a clean working copy. This is
802    /// useful if you want to reuse build artifacts in the current directory.
803    WorkingCopy,
804
805    /// Run the tests in a separate worktree (managed by git-branchless). This is useful if you want
806    /// to run tests in parallel, or if you want to run tests on a different commit without
807    /// invalidating build artifacts in the current directory, or if you want to run tests while
808    /// your working copy is dirty.
809    Worktree,
810}
811
812/// How to conduct searches on the commit graph.
813#[derive(Clone, Copy, Debug, ValueEnum)]
814pub enum TestSearchStrategy {
815    /// Visit commits starting from the earliest commit and exit early when a
816    /// failing commit is found.
817    Linear,
818
819    /// Visit commits starting from the latest commit and exit early when a
820    /// passing commit is found.
821    Reverse,
822
823    /// Visit commits starting from the middle of the commit graph and exit
824    /// early when a failing commit is found.
825    Binary,
826}
827
828/// Arguments which apply to all commands. Used during setup.
829#[derive(Debug, Parser)]
830pub struct GlobalArgs {
831    /// Change to the given directory before executing the rest of the program.
832    /// (The option is called `-C` for symmetry with Git.)
833    #[clap(value_parser, short = 'C', global = true)]
834    pub working_directory: Option<PathBuf>,
835
836    /// Flag to force enable or disable terminal colors.
837    #[clap(value_parser, long = "color", value_enum, global = true)]
838    pub color: Option<ColorSetting>,
839}
840
841/// Branchless workflow for Git.
842///
843/// See the documentation at https://github.com/arxanas/git-branchless/wiki.
844#[derive(Debug, Parser)]
845#[clap(version = env!("CARGO_PKG_VERSION"), author = "Waleed Khan <me@waleedkhan.name>")]
846pub struct Opts {
847    /// Global arguments.
848    #[clap(flatten)]
849    pub global_args: GlobalArgs,
850
851    /// The `git-branchless` subcommand to run.
852    #[clap(subcommand)]
853    pub command: Command,
854}
855
856/// `snapshot` subcommands.
857#[derive(Debug, Parser)]
858pub enum SnapshotSubcommand {
859    /// Create a new snapshot containing the working copy contents, and then
860    /// reset the working copy to the current `HEAD` commit.
861    ///
862    /// On success, prints the snapshot commit hash to stdout.
863    Create,
864
865    /// Restore the working copy contents from the provided snapshot.
866    Restore {
867        /// The commit hash for the snapshot.
868        #[clap(value_parser)]
869        snapshot_oid: NonZeroOid,
870    },
871}
872
873/// `test` subcommands.
874#[derive(Debug, Parser)]
875pub enum TestSubcommand {
876    /// Clean any cached test results.
877    Clean {
878        /// The set of commits whose results should be cleaned.
879        #[clap(value_parser, default_value = "stack() | @")]
880        revset: Revset,
881
882        /// Options for resolving revset expressions.
883        #[clap(flatten)]
884        resolve_revset_options: ResolveRevsetOptions,
885    },
886
887    /// Run a given command on a set of commits and present the successes and failures.
888    Run {
889        /// An ad-hoc command to execute on each commit.
890        #[clap(value_parser, short = 'x', long = "exec")]
891        exec: Option<String>,
892
893        /// The test command alias for the command to execute on each commit. Set with
894        /// `git config branchless.test.alias.<name> <command>`.
895        #[clap(value_parser, short = 'c', long = "command", conflicts_with("exec"))]
896        command: Option<String>,
897
898        /// The set of commits to test.
899        #[clap(value_parser, default_value = "stack() | @")]
900        revset: Revset,
901
902        /// Options for resolving revset expressions.
903        #[clap(flatten)]
904        resolve_revset_options: ResolveRevsetOptions,
905
906        /// Show the test output as well.
907        #[clap(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
908        verbosity: u8,
909
910        /// How to execute the tests.
911        #[clap(short = 's', long = "strategy")]
912        strategy: Option<TestExecutionStrategy>,
913
914        /// Search for the first commit that fails the test command, rather than
915        /// running on all commits.
916        #[clap(short = 'S', long = "search")]
917        search: Option<TestSearchStrategy>,
918
919        /// Shorthand for `--search binary`.
920        #[clap(short = 'b', long = "bisect", conflicts_with("search"))]
921        bisect: bool,
922
923        /// Don't read or write to the cache when executing the test commands.
924        #[clap(long = "no-cache")]
925        no_cache: bool,
926
927        /// Run the test command in the foreground rather than the background so
928        /// that the user can interact with it.
929        #[clap(short = 'i', long = "interactive")]
930        interactive: bool,
931
932        /// How many jobs to execute in parallel. The value `0` indicates to use all CPUs.
933        #[clap(short = 'j', long = "jobs")]
934        jobs: Option<usize>,
935    },
936
937    /// Show the results of a set of previous test runs.
938    Show {
939        /// An ad-hoc command to execute on each commit.
940        #[clap(value_parser, short = 'x', long = "exec")]
941        exec: Option<String>,
942
943        /// The test command alias for the command to execute on each commit. Set with
944        /// `git config branchless.test.alias.<name> <command>`.
945        #[clap(value_parser, short = 'c', long = "command", conflicts_with("exec"))]
946        command: Option<String>,
947
948        /// The set of commits to show the test output for.
949        #[clap(value_parser, default_value = "stack() | @")]
950        revset: Revset,
951
952        /// Options for resolving revset expressions.
953        #[clap(flatten)]
954        resolve_revset_options: ResolveRevsetOptions,
955
956        /// Show the test output as well.
957        #[clap(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
958        verbosity: u8,
959    },
960
961    /// Rewrites a set of commits one by one, by running a given command on each commit
962    ///
963    /// The descendants are "reparented" onto the rewritten commits without changing their
964    /// contents. This avoids the possibility of merge conflicts in descendants.
965    Fix {
966        /// An ad-hoc command to execute on each commit.
967        #[clap(value_parser, short = 'x', long = "exec")]
968        exec: Option<String>,
969
970        /// The test command alias for the command to execute on each commit. Set with
971        /// `git config branchless.test.alias.<name> <command>`.
972        #[clap(value_parser, short = 'c', long = "command", conflicts_with("exec"))]
973        command: Option<String>,
974
975        /// Don't rewrite any commits. Instead, just print a summary as usual.
976        #[clap(value_parser, short = 'n', long = "dry-run")]
977        dry_run: bool,
978
979        /// The set of commits to test.
980        #[clap(value_parser, default_value = "stack()")]
981        revset: Revset,
982
983        /// Options for resolving revset expressions.
984        #[clap(flatten)]
985        resolve_revset_options: ResolveRevsetOptions,
986
987        /// Show the test output as well.
988        #[clap(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
989        verbosity: u8,
990
991        /// How to execute the tests.
992        #[clap(short = 's', long = "strategy")]
993        strategy: Option<TestExecutionStrategy>,
994
995        /// Don't read or write to the cache when executing the test commands.
996        #[clap(long = "no-cache")]
997        no_cache: bool,
998
999        /// How many jobs to execute in parallel. The value `0` indicates to use all CPUs.
1000        #[clap(short = 'j', long = "jobs")]
1001        jobs: Option<usize>,
1002
1003        /// Options for moving commits.
1004        #[clap(flatten)]
1005        move_options: MoveOptions,
1006    },
1007}
1008
1009/// Generate and write man-pages into the specified directory.
1010///
1011/// The generated files are named things like `man1/git-branchless-smartlog.1`,
1012/// so this directory should be of the form `path/to/man`, to ensure that these
1013/// files get generated into the correct man-page section.
1014pub fn write_man_pages(man_dir: &Path) -> std::io::Result<()> {
1015    let man1_dir = man_dir.join("man1");
1016    std::fs::create_dir_all(&man1_dir)?;
1017
1018    let app =
1019        // Explicitly set the name here, or else clap thinks that the name of the
1020        // command is `git-branchless-opts` (and that its subcommands are
1021        // `git-branchless-opts-amend`, etc.).
1022        Opts::command().name("git-branchless");
1023    generate_man_page(&man1_dir, "git-branchless", &app)?;
1024    for subcommand in app.get_subcommands() {
1025        let subcommand_exe_name = format!("git-branchless-{}", subcommand.get_name());
1026        generate_man_page(&man1_dir, &subcommand_exe_name, subcommand)?;
1027    }
1028    Ok(())
1029}
1030
1031fn generate_man_page(man1_dir: &Path, name: &str, command: &ClapCommand) -> std::io::Result<()> {
1032    let rendered_man_page = {
1033        let mut buffer = Vec::new();
1034        clap_mangen::Man::new(command.clone())
1035            // The rendered man-page command name would be the subcommand only
1036            // (such as `amend(1)` instead of `git-branchless-amend(1)`), so
1037            // override the name here. Also, the top-level man-page name will be
1038            // `git-branchless-opts(1)` instead of `git-branchless(1)`, which is
1039            // also handled by this call to `.title`.
1040            .title(name)
1041            .render(&mut buffer)?;
1042        buffer
1043    };
1044    let output_path = man1_dir.join(format!("{name}.1"));
1045    std::fs::write(output_path, rendered_man_page)?;
1046    Ok(())
1047}
1048
1049/// Carry out some rewrites on the command-line arguments for uniformity.
1050///
1051/// For example, `git-branchless-smartlog` becomes `git-branchless smartlog`,
1052/// and the `.exe` suffix is removed on Windows. These are necessary for later
1053/// command-line argument parsing.
1054pub fn rewrite_args(args: Vec<OsString>) -> Vec<OsString> {
1055    let first_arg = match args.first() {
1056        None => return args,
1057        Some(first_arg) => first_arg.clone(),
1058    };
1059
1060    // Don't use `std::env::current_exe`, because it may or may not resolve the
1061    // symlink. We want to preserve the symlink in our case. See
1062    // https://doc.rust-lang.org/std/env/fn.current_exe.html#platform-specific-behavior
1063    let exe_path = PathBuf::from(first_arg);
1064    let exe_name = match exe_path.file_name().and_then(|arg| arg.to_str()) {
1065        Some(exe_name) => exe_name,
1066        None => return args,
1067    };
1068
1069    // On Windows, the first argument might be `git-branchless-smartlog.exe`
1070    // instead of just `git-branchless-smartlog`. Remove the suffix in that
1071    // case.
1072    let exe_name = match exe_name.strip_suffix(std::env::consts::EXE_SUFFIX) {
1073        Some(exe_name) => exe_name,
1074        None => exe_name,
1075    };
1076
1077    let args = match exe_name.strip_prefix("git-branchless-") {
1078        Some(subcommand) => {
1079            let mut new_args = vec![OsString::from("git-branchless"), OsString::from(subcommand)];
1080            new_args.extend(args.into_iter().skip(1));
1081            new_args
1082        }
1083        None => {
1084            let mut new_args = vec![OsString::from(exe_name)];
1085            new_args.extend(args.into_iter().skip(1));
1086            new_args
1087        }
1088    };
1089
1090    // For backward-compatibility, convert calls of the form
1091    // `git-branchless-hook-X Y Z` into `git-branchless hook X Y Z`.
1092    let args = match args.as_slice() {
1093        [first, subcommand, rest @ ..] if exe_name == "git-branchless" => {
1094            let mut new_args = vec![first.clone()];
1095            match subcommand
1096                .to_str()
1097                .and_then(|arg| arg.strip_prefix("hook-"))
1098            {
1099                Some(hook_subcommand) => {
1100                    new_args.push(OsString::from("hook"));
1101                    new_args.push(OsString::from(hook_subcommand));
1102                }
1103                None => {
1104                    new_args.push(subcommand.clone());
1105                }
1106            }
1107            new_args.extend(rest.iter().cloned());
1108            new_args
1109        }
1110        other => other.to_vec(),
1111    };
1112
1113    args
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118    use super::rewrite_args;
1119    use std::ffi::OsString;
1120
1121    #[test]
1122    fn test_rewrite_args() {
1123        assert_eq!(
1124            rewrite_args(vec![OsString::from("git-branchless")]),
1125            vec![OsString::from("git-branchless")]
1126        );
1127        assert_eq!(
1128            rewrite_args(vec![OsString::from("git-branchless-smartlog")]),
1129            vec![OsString::from("git-branchless"), OsString::from("smartlog")]
1130        );
1131
1132        // Should only happen on Windows.
1133        if std::env::consts::EXE_SUFFIX == ".exe" {
1134            assert_eq!(
1135                rewrite_args(vec![OsString::from("git-branchless-smartlog.exe")]),
1136                vec![OsString::from("git-branchless"), OsString::from("smartlog")]
1137            );
1138        }
1139
1140        assert_eq!(
1141            rewrite_args(vec![
1142                OsString::from("git-branchless-smartlog"),
1143                OsString::from("foo"),
1144                OsString::from("bar")
1145            ]),
1146            vec![
1147                OsString::from("git-branchless"),
1148                OsString::from("smartlog"),
1149                OsString::from("foo"),
1150                OsString::from("bar")
1151            ]
1152        );
1153
1154        assert_eq!(
1155            rewrite_args(vec![
1156                OsString::from("git-branchless"),
1157                OsString::from("hook-post-commit"),
1158            ]),
1159            vec![
1160                OsString::from("git-branchless"),
1161                OsString::from("hook"),
1162                OsString::from("post-commit"),
1163            ]
1164        );
1165        assert_eq!(
1166            rewrite_args(vec![
1167                OsString::from("git-branchless-hook"),
1168                OsString::from("post-commit"),
1169            ]),
1170            vec![
1171                OsString::from("git-branchless"),
1172                OsString::from("hook"),
1173                OsString::from("post-commit"),
1174            ]
1175        );
1176        assert_eq!(
1177            rewrite_args(vec![
1178                OsString::from("git-branchless"),
1179                OsString::from("hook-post-checkout"),
1180                OsString::from("3"),
1181                OsString::from("2"),
1182                OsString::from("1"),
1183            ]),
1184            vec![
1185                OsString::from("git-branchless"),
1186                OsString::from("hook"),
1187                OsString::from("post-checkout"),
1188                OsString::from("3"),
1189                OsString::from("2"),
1190                OsString::from("1"),
1191            ]
1192        );
1193        assert_eq!(
1194            rewrite_args(vec![
1195                OsString::from("target/debug/git-branchless"),
1196                OsString::from("hook-detect-empty-commit"),
1197                OsString::from("abc123"),
1198            ]),
1199            vec![
1200                OsString::from("git-branchless"),
1201                OsString::from("hook"),
1202                OsString::from("detect-empty-commit"),
1203                OsString::from("abc123"),
1204            ]
1205        );
1206    }
1207}