#![warn(missing_docs)]
#![warn(
clippy::all,
clippy::as_conversions,
clippy::clone_on_ref_ptr,
clippy::dbg_macro
)]
#![allow(clippy::too_many_arguments, clippy::blocks_in_conditions)]
#![allow(rustdoc::bare_urls)]
use std::ffi::OsString;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use clap::{Args, Command as ClapCommand, CommandFactory, Parser, ValueEnum};
use lib::core::untracked_file_cache::UntrackedFileStrategy;
use lib::git::NonZeroOid;
#[derive(Clone, Debug)]
pub struct Revset(pub String);
impl FromStr for Revset {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.to_string()))
}
}
impl Display for Revset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Parser)]
pub enum WrappedCommand {
#[clap(external_subcommand)]
WrappedCommand(Vec<String>),
}
#[derive(Args, Debug, Default)]
pub struct ResolveRevsetOptions {
#[clap(action, long = "hidden")]
pub show_hidden_commits: bool,
}
#[derive(Args, Debug)]
pub struct MoveOptions {
#[clap(action, short = 'f', long = "force-rewrite", visible_alias = "fr")]
pub force_rewrite_public_commits: bool,
#[clap(action, long = "in-memory", conflicts_with_all(&["force_on_disk", "merge"]))]
pub force_in_memory: bool,
#[clap(action, long = "on-disk")]
pub force_on_disk: bool,
#[clap(action(clap::ArgAction::SetFalse), long = "no-deduplicate-commits")]
pub detect_duplicate_commits_via_patch_id: bool,
#[clap(action, name = "merge", short = 'm', long = "merge")]
pub resolve_merge_conflicts: bool,
#[clap(long, conflicts_with = "merge")]
pub reparent: bool,
#[clap(action, long = "debug-dump-rebase-constraints")]
pub dump_rebase_constraints: bool,
#[clap(action, long = "debug-dump-rebase-plan")]
pub dump_rebase_plan: bool,
}
#[derive(Args, Debug)]
pub struct TraverseCommitsOptions {
#[clap(value_parser)]
pub num_commits: Option<usize>,
#[clap(action, short = 'a', long = "all")]
pub all_the_way: bool,
#[clap(action, short = 'b', long = "branch")]
pub move_by_branches: bool,
#[clap(action, short = 'o', long = "oldest")]
pub oldest: bool,
#[clap(action, short = 'n', long = "newest", conflicts_with("oldest"))]
pub newest: bool,
#[clap(
action,
short = 'i',
long = "interactive",
conflicts_with("newest"),
conflicts_with("oldest")
)]
pub interactive: bool,
#[clap(action, short = 'm', long = "merge")]
pub merge: bool,
#[clap(action, short = 'f', long = "force", conflicts_with("merge"))]
pub force: bool,
}
#[derive(Args, Debug)]
pub struct SwitchOptions {
#[clap(action, short = 'i', long = "interactive")]
pub interactive: bool,
#[clap(value_parser, short = 'c', long = "create")]
pub branch_name: Option<String>,
#[clap(action, short = 'f', long = "force")]
pub force: bool,
#[clap(action, short = 'm', long = "merge", conflicts_with("force"))]
pub merge: bool,
#[clap(action, short = 'd', long = "detach")]
pub detach: bool,
#[clap(value_parser)]
pub target: Option<Revset>,
}
#[derive(Debug, Parser)]
pub enum HookSubcommand {
DetectEmptyCommit {
#[clap(value_parser)]
old_commit_oid: String,
},
PreAutoGc,
PostApplypatch,
PostCheckout {
#[clap(value_parser)]
previous_commit: String,
#[clap(value_parser)]
current_commit: String,
#[clap(value_parser)]
is_branch_checkout: isize,
},
PostCommit,
PostMerge {
#[clap(value_parser)]
is_squash_merge: isize,
},
PostRewrite {
#[clap(value_parser)]
rewrite_type: String,
},
ReferenceTransaction {
#[clap(value_parser)]
transaction_state: String,
},
RegisterExtraPostRewriteHook,
SkipUpstreamAppliedCommit {
#[clap(value_parser)]
commit_oid: String,
},
}
#[derive(Debug, Parser)]
pub struct HookArgs {
#[clap(subcommand)]
pub subcommand: HookSubcommand,
}
#[derive(Debug, Parser)]
pub struct InitArgs {
#[clap(action, long = "uninstall")]
pub uninstall: bool,
#[clap(value_parser, long = "main-branch", conflicts_with = "uninstall")]
pub main_branch_name: Option<String>,
}
#[derive(Debug, Parser)]
pub struct InstallManPagesArgs {
pub path: PathBuf,
}
#[derive(Debug, Parser)]
pub struct QueryArgs {
#[clap(value_parser)]
pub revset: Revset,
#[clap(flatten)]
pub resolve_revset_options: ResolveRevsetOptions,
#[clap(action, short = 'b', long = "branches")]
pub show_branches: bool,
#[clap(action, short = 'r', long = "raw", conflicts_with("show_branches"))]
pub raw: bool,
}
#[derive(Debug, Parser)]
pub struct MessageArgs {
#[clap(value_parser, short = 'm', long = "message")]
pub messages: Vec<String>,
#[clap(value_parser, long = "fixup", conflicts_with_all(&["messages"]))]
pub commit_to_fixup: Option<Revset>,
}
#[derive(Debug, Parser)]
pub struct RecordArgs {
#[clap(flatten)]
pub message_args: MessageArgs,
#[clap(action, short = 'i', long = "interactive")]
pub interactive: bool,
#[clap(action, short = 'c', long = "create")]
pub create: Option<String>,
#[clap(action, short = 'd', long = "detach", conflicts_with("create"))]
pub detach: bool,
#[clap(action, short = 'I', long = "insert")]
pub insert: bool,
#[clap(action, short = 's', long = "stash", conflicts_with_all(&["create", "detach"]))]
pub stash: bool,
#[clap(value_parser, long = "untracked", conflicts_with_all(&["interactive"]))]
pub untracked_file_strategy: Option<UntrackedFileStrategy>,
}
#[derive(Debug, Parser)]
pub struct SmartlogArgs {
#[clap(value_parser, long = "event-id")]
pub event_id: Option<isize>,
#[clap(value_parser)]
pub revset: Option<Revset>,
#[clap(long)]
pub reverse: bool,
#[clap(long)]
pub exact: bool,
#[clap(flatten)]
pub resolve_revset_options: ResolveRevsetOptions,
}
#[derive(Clone, Debug, ValueEnum)]
pub enum ForgeKind {
Branch,
Github,
Phabricator,
}
#[derive(Debug, Parser)]
pub struct SubmitArgs {
#[clap(value_parser, default_value = "stack()")]
pub revsets: Vec<Revset>,
#[clap(flatten)]
pub resolve_revset_options: ResolveRevsetOptions,
#[clap(short = 'F', long = "forge")]
pub forge_kind: Option<ForgeKind>,
#[clap(action, short = 'c', long = "create")]
pub create: bool,
#[clap(action, short = 'd', long = "draft")]
pub draft: bool,
#[clap(short = 'm', long = "message")]
pub message: Option<String>,
#[clap(short = 'j', long = "jobs")]
pub num_jobs: Option<usize>,
#[clap(short = 's', long = "strategy")]
pub execution_strategy: Option<TestExecutionStrategy>,
#[clap(short = 'n', long = "dry-run")]
pub dry_run: bool,
}
#[derive(Debug, Parser)]
pub struct TestArgs {
#[clap(subcommand)]
pub subcommand: TestSubcommand,
}
#[derive(Debug, Parser)]
pub enum Command {
Amend {
#[clap(flatten)]
move_options: MoveOptions,
#[clap(action, long = "untracked")]
untracked_file_strategy: Option<UntrackedFileStrategy>,
},
BugReport,
Difftool(scm_diff_editor::Opts),
Gc,
Hide {
#[clap(value_parser)]
revsets: Vec<Revset>,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
#[clap(action, long = "no-delete-branches")]
no_delete_branches: bool,
#[clap(action, short = 'r', long = "recursive")]
recursive: bool,
},
#[clap(hide = true)]
Hook(HookArgs),
Init(InitArgs),
InstallManPages(InstallManPagesArgs),
Move {
#[clap(action(clap::ArgAction::Append), short = 's', long = "source")]
source: Vec<Revset>,
#[clap(
action(clap::ArgAction::Append),
short = 'b',
long = "base",
conflicts_with = "source"
)]
base: Vec<Revset>,
#[clap(
action(clap::ArgAction::Append),
short = 'x',
long = "exact",
conflicts_with_all(&["source", "base"])
)]
exact: Vec<Revset>,
#[clap(value_parser, short = 'd', long = "dest")]
dest: Option<Revset>,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
#[clap(flatten)]
move_options: MoveOptions,
#[clap(action, short = 'F', long = "fixup", conflicts_with_all(&["insert", "reparent"]))]
fixup: bool,
#[clap(action, short = 'I', long = "insert")]
insert: bool,
#[clap(action, long = "dry-run", conflicts_with = "force_on_disk")]
dry_run: bool,
},
Next {
#[clap(flatten)]
traverse_commits_options: TraverseCommitsOptions,
},
Prev {
#[clap(flatten)]
traverse_commits_options: TraverseCommitsOptions,
},
Query(QueryArgs),
Repair {
#[clap(action(clap::ArgAction::SetFalse), long = "no-dry-run")]
dry_run: bool,
},
Restack {
#[clap(value_parser, default_value = "draft()")]
revsets: Vec<Revset>,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
#[clap(flatten)]
move_options: MoveOptions,
},
Record(RecordArgs),
Reword {
#[clap(
value_parser,
default_value = "stack() | @",
default_value_if("commit_to_fixup", clap::builder::ArgPredicate::IsPresent, "@"),
default_value_if("messages", clap::builder::ArgPredicate::IsPresent, "@")
)]
revsets: Vec<Revset>,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
#[clap(action, short = 'f', long = "force-rewrite", visible_alias = "fr")]
force_rewrite_public_commits: bool,
#[clap(flatten)]
message_args: MessageArgs,
#[clap(action, short = 'd', long = "discard", conflicts_with_all(&["messages", "commit_to_fixup"]))]
discard: bool,
},
Smartlog(SmartlogArgs),
#[clap(hide = true)]
Snapshot {
#[clap(subcommand)]
subcommand: SnapshotSubcommand,
},
Split {
#[clap(value_parser)]
revset: Revset,
#[clap(value_parser, required = true)]
files: Vec<String>,
#[clap(action, short = 'b', long)]
before: bool,
#[clap(action, short = 'd', long)]
detach: bool,
#[clap(action, short = 'D', long = "discard", conflicts_with("detach"))]
discard: bool,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
#[clap(flatten)]
move_options: MoveOptions,
},
Submit(SubmitArgs),
Switch {
#[clap(flatten)]
switch_options: SwitchOptions,
},
Sync {
#[clap(
action,
short = 'p',
long = "pull",
visible_short_alias = 'u',
visible_alias = "--update"
)]
pull: bool,
#[clap(flatten)]
move_options: MoveOptions,
#[clap(value_parser)]
revsets: Vec<Revset>,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
},
Test(TestArgs),
Undo {
#[clap(action, short = 'i', long = "interactive")]
interactive: bool,
#[clap(action, short = 'y', long = "yes")]
yes: bool,
},
Unhide {
#[clap(value_parser)]
revsets: Vec<Revset>,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
#[clap(action, short = 'r', long = "recursive")]
recursive: bool,
},
Wrap {
#[clap(value_parser, long = "git-executable")]
git_executable: Option<PathBuf>,
#[clap(subcommand)]
command: WrappedCommand,
},
}
#[derive(Clone, Debug, ValueEnum)]
pub enum ColorSetting {
Auto,
Always,
Never,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum TestExecutionStrategy {
WorkingCopy,
Worktree,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum TestSearchStrategy {
Linear,
Reverse,
Binary,
}
#[derive(Debug, Parser)]
pub struct GlobalArgs {
#[clap(value_parser, short = 'C', global = true)]
pub working_directory: Option<PathBuf>,
#[clap(value_parser, long = "color", value_enum, global = true)]
pub color: Option<ColorSetting>,
}
#[derive(Debug, Parser)]
#[clap(version = env!("CARGO_PKG_VERSION"), author = "Waleed Khan <me@waleedkhan.name>")]
pub struct Opts {
#[clap(flatten)]
pub global_args: GlobalArgs,
#[clap(subcommand)]
pub command: Command,
}
#[derive(Debug, Parser)]
pub enum SnapshotSubcommand {
Create,
Restore {
#[clap(value_parser)]
snapshot_oid: NonZeroOid,
},
}
#[derive(Debug, Parser)]
pub enum TestSubcommand {
Clean {
#[clap(value_parser, default_value = "stack() | @")]
revset: Revset,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
},
Run {
#[clap(value_parser, short = 'x', long = "exec")]
exec: Option<String>,
#[clap(value_parser, short = 'c', long = "command", conflicts_with("exec"))]
command: Option<String>,
#[clap(value_parser, default_value = "stack() | @")]
revset: Revset,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
#[clap(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
verbosity: u8,
#[clap(short = 's', long = "strategy")]
strategy: Option<TestExecutionStrategy>,
#[clap(short = 'S', long = "search")]
search: Option<TestSearchStrategy>,
#[clap(short = 'b', long = "bisect", conflicts_with("search"))]
bisect: bool,
#[clap(long = "no-cache")]
no_cache: bool,
#[clap(short = 'i', long = "interactive")]
interactive: bool,
#[clap(short = 'j', long = "jobs")]
jobs: Option<usize>,
},
Show {
#[clap(value_parser, short = 'x', long = "exec")]
exec: Option<String>,
#[clap(value_parser, short = 'c', long = "command", conflicts_with("exec"))]
command: Option<String>,
#[clap(value_parser, default_value = "stack() | @")]
revset: Revset,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
#[clap(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
verbosity: u8,
},
Fix {
#[clap(value_parser, short = 'x', long = "exec")]
exec: Option<String>,
#[clap(value_parser, short = 'c', long = "command", conflicts_with("exec"))]
command: Option<String>,
#[clap(value_parser, short = 'n', long = "dry-run")]
dry_run: bool,
#[clap(value_parser, default_value = "stack()")]
revset: Revset,
#[clap(flatten)]
resolve_revset_options: ResolveRevsetOptions,
#[clap(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
verbosity: u8,
#[clap(short = 's', long = "strategy")]
strategy: Option<TestExecutionStrategy>,
#[clap(long = "no-cache")]
no_cache: bool,
#[clap(short = 'j', long = "jobs")]
jobs: Option<usize>,
#[clap(flatten)]
move_options: MoveOptions,
},
}
pub fn write_man_pages(man_dir: &Path) -> std::io::Result<()> {
let man1_dir = man_dir.join("man1");
std::fs::create_dir_all(&man1_dir)?;
let app =
Opts::command().name("git-branchless");
generate_man_page(&man1_dir, "git-branchless", &app)?;
for subcommand in app.get_subcommands() {
let subcommand_exe_name = format!("git-branchless-{}", subcommand.get_name());
generate_man_page(&man1_dir, &subcommand_exe_name, subcommand)?;
}
Ok(())
}
fn generate_man_page(man1_dir: &Path, name: &str, command: &ClapCommand) -> std::io::Result<()> {
let rendered_man_page = {
let mut buffer = Vec::new();
clap_mangen::Man::new(command.clone())
.title(name)
.render(&mut buffer)?;
buffer
};
let output_path = man1_dir.join(format!("{name}.1"));
std::fs::write(output_path, rendered_man_page)?;
Ok(())
}
pub fn rewrite_args(args: Vec<OsString>) -> Vec<OsString> {
let first_arg = match args.first() {
None => return args,
Some(first_arg) => first_arg.clone(),
};
let exe_path = PathBuf::from(first_arg);
let exe_name = match exe_path.file_name().and_then(|arg| arg.to_str()) {
Some(exe_name) => exe_name,
None => return args,
};
let exe_name = match exe_name.strip_suffix(std::env::consts::EXE_SUFFIX) {
Some(exe_name) => exe_name,
None => exe_name,
};
let args = match exe_name.strip_prefix("git-branchless-") {
Some(subcommand) => {
let mut new_args = vec![OsString::from("git-branchless"), OsString::from(subcommand)];
new_args.extend(args.into_iter().skip(1));
new_args
}
None => {
let mut new_args = vec![OsString::from(exe_name)];
new_args.extend(args.into_iter().skip(1));
new_args
}
};
let args = match args.as_slice() {
[first, subcommand, rest @ ..] if exe_name == "git-branchless" => {
let mut new_args = vec![first.clone()];
match subcommand
.to_str()
.and_then(|arg| arg.strip_prefix("hook-"))
{
Some(hook_subcommand) => {
new_args.push(OsString::from("hook"));
new_args.push(OsString::from(hook_subcommand));
}
None => {
new_args.push(subcommand.clone());
}
}
new_args.extend(rest.iter().cloned());
new_args
}
other => other.to_vec(),
};
args
}
#[cfg(test)]
mod tests {
use super::rewrite_args;
use std::ffi::OsString;
#[test]
fn test_rewrite_args() {
assert_eq!(
rewrite_args(vec![OsString::from("git-branchless")]),
vec![OsString::from("git-branchless")]
);
assert_eq!(
rewrite_args(vec![OsString::from("git-branchless-smartlog")]),
vec![OsString::from("git-branchless"), OsString::from("smartlog")]
);
if std::env::consts::EXE_SUFFIX == ".exe" {
assert_eq!(
rewrite_args(vec![OsString::from("git-branchless-smartlog.exe")]),
vec![OsString::from("git-branchless"), OsString::from("smartlog")]
);
}
assert_eq!(
rewrite_args(vec![
OsString::from("git-branchless-smartlog"),
OsString::from("foo"),
OsString::from("bar")
]),
vec![
OsString::from("git-branchless"),
OsString::from("smartlog"),
OsString::from("foo"),
OsString::from("bar")
]
);
assert_eq!(
rewrite_args(vec![
OsString::from("git-branchless"),
OsString::from("hook-post-commit"),
]),
vec![
OsString::from("git-branchless"),
OsString::from("hook"),
OsString::from("post-commit"),
]
);
assert_eq!(
rewrite_args(vec![
OsString::from("git-branchless-hook"),
OsString::from("post-commit"),
]),
vec![
OsString::from("git-branchless"),
OsString::from("hook"),
OsString::from("post-commit"),
]
);
assert_eq!(
rewrite_args(vec![
OsString::from("git-branchless"),
OsString::from("hook-post-checkout"),
OsString::from("3"),
OsString::from("2"),
OsString::from("1"),
]),
vec![
OsString::from("git-branchless"),
OsString::from("hook"),
OsString::from("post-checkout"),
OsString::from("3"),
OsString::from("2"),
OsString::from("1"),
]
);
assert_eq!(
rewrite_args(vec![
OsString::from("target/debug/git-branchless"),
OsString::from("hook-detect-empty-commit"),
OsString::from("abc123"),
]),
vec![
OsString::from("git-branchless"),
OsString::from("hook"),
OsString::from("detect-empty-commit"),
OsString::from("abc123"),
]
);
}
}