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}