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}