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