use std::{
io::{BufReader, IsTerminal, stdin},
path::PathBuf,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use clap::{CommandFactory, Parser};
use gitoxide_core as core;
use gitoxide_core::{pack::verify, repository::PathsOrPatterns};
use gix::{
Result,
bstr::{BString, io::BufReadExt},
error::{OptionExt, ResultExt, bail, message},
};
use crate::{
plumbing::{
options::{
Args, Subcommands, attributes, branch, commit, commitgraph, config, credential, exclude, free, fsck, index,
mailmap, merge, odb, revision, tag, tree, worktree,
},
show_progress,
},
shared::pretty::{init_tracing, prepare_and_run},
};
pub fn main() -> Result<()> {
let args: Args = Args::parse_from(gix::env::args_os());
let thread_limit = args.threads;
let verbose = args.verbose;
let format = args.format;
#[cfg(feature = "tracing")]
let trace = args.trace;
#[cfg(not(feature = "tracing"))]
let trace = 0;
let cmd = args.cmd;
#[cfg(feature = "tix")]
let cmd = match cmd {
Subcommands::Tix(command) if !command.requires_repository() => {
return command.run_without_repository(gix_tix::command::Invocation::GixTix);
}
cmd => cmd,
};
#[cfg(feature = "tix")]
let command_initializes_tracing = matches!(&cmd, Subcommands::Tix(_));
#[cfg(not(feature = "tix"))]
let command_initializes_tracing = false;
let _trace_guard = if command_initializes_tracing {
None
} else {
Some(init_tracing(trace)?)
};
#[cfg(feature = "gitoxide-core-tools-corpus")]
let trace_output = _trace_guard
.as_ref()
.and_then(crate::shared::pretty::TraceGuard::output)
.unwrap_or_default();
let object_hash = args.object_hash;
let config = args.config;
let repository = args.repository;
let repository_path = repository.clone();
enum Mode {
Strict,
StrictWithGitInstallConfig,
Lenient,
LenientWithGitInstallConfig,
}
let repository = {
let config = config.clone();
move |mut mode: Mode| -> Result<gix::Repository> {
let mut mapping: gix::sec::trust::Mapping<gix::open::Options> = Default::default();
if !config.is_empty() {
mode = match mode {
Mode::Lenient => Mode::Strict,
Mode::LenientWithGitInstallConfig => Mode::StrictWithGitInstallConfig,
_ => mode,
};
}
let strict_toggle = matches!(mode, Mode::Strict | Mode::StrictWithGitInstallConfig) || args.strict;
mapping.full = mapping.full.strict_config(strict_toggle);
mapping.reduced = mapping.reduced.strict_config(strict_toggle);
let git_installation = matches!(
mode,
Mode::StrictWithGitInstallConfig | Mode::LenientWithGitInstallConfig
);
let to_match_settings = |mut opts: gix::open::Options| {
opts.permissions.config.git_binary = git_installation;
opts.permissions.attributes.git_binary = git_installation;
if config.is_empty() {
opts
} else {
opts.cli_overrides(config.clone())
}
};
mapping.full.modify(to_match_settings);
mapping.reduced.modify(to_match_settings);
let mut repo = gix::ThreadSafeRepository::discover_with_environment_overrides_opts(
repository,
Default::default(),
mapping,
)
.map(gix::Repository::from)?;
if !config.is_empty() {
repo.config_snapshot_mut()
.append_config(config.iter(), gix::config::Source::Cli)
.or_raise(|| message("Unable to parse command-line configuration"))?;
}
{
let mut config_mut = repo.config_snapshot_mut();
if config_mut
.boolean(gix::config::tree::Gitoxide::PARSE_PRECIOUS)?
.is_none()
{
config_mut.set_raw_value(gix::config::tree::Gitoxide::PARSE_PRECIOUS, "true")?;
}
}
Ok(repo)
}
};
let auto_verbose = !args.no_verbose;
let should_interrupt = Arc::new(AtomicBool::new(false));
#[expect(unsafe_code)]
unsafe {
gix::interrupt::init_handler(1, {
let should_interrupt = Arc::clone(&should_interrupt);
move || should_interrupt.store(true, Ordering::SeqCst)
})?;
}
match cmd {
#[cfg(feature = "tix")]
Subcommands::Tix(command) => command.run_as(
repository(Mode::Lenient)?.into_sync(),
gix_tix::command::Invocation::GixTix,
),
Subcommands::Env => prepare_and_run("env", verbose, None, move |_progress, out, _err| core::env(out, format)),
Subcommands::Editor { paths } => core::repository::editor(repository(Mode::Lenient)?, paths),
Subcommands::Merge(merge::Platform { cmd }) => match cmd {
merge::SubCommands::File {
resolve_with,
ours,
base,
theirs,
} => prepare_and_run("merge-file", verbose, None, move |_progress, out, _err| {
core::repository::merge::file(
repository(Mode::Lenient)?,
out,
format,
resolve_with.map(Into::into),
base,
ours,
theirs,
)
}),
merge::SubCommands::Tree {
opts:
merge::SharedOptions {
in_memory,
file_favor,
tree_favor,
debug,
},
message,
update_head,
ours,
base,
theirs,
} => prepare_and_run("merge-tree", verbose, None, move |_progress, out, err| {
core::repository::merge::tree(
repository(Mode::Lenient)?,
out,
err,
base,
ours,
theirs,
core::repository::merge::tree::Options {
format,
file_favor: file_favor.map(Into::into),
in_memory,
tree_favor: tree_favor.map(Into::into),
debug,
message,
update_head,
},
)
}),
merge::SubCommands::Commit {
opts:
merge::SharedOptions {
in_memory,
file_favor,
tree_favor,
debug,
},
ours,
theirs,
} => prepare_and_run("merge-commit", verbose, None, move |_progress, out, err| {
core::repository::merge::commit(
repository(Mode::Lenient)?,
out,
err,
ours,
theirs,
core::repository::merge::tree::Options {
format,
file_favor: file_favor.map(Into::into),
tree_favor: tree_favor.map(Into::into),
in_memory,
debug,
message: None,
update_head: false,
},
)
}),
},
Subcommands::MergeBase(crate::plumbing::options::merge_base::Command { first, others }) => {
prepare_and_run("merge-base", verbose, None, move |_progress, out, _err| {
core::repository::merge_base(repository(Mode::Lenient)?, first, others, out, format)
})
}
Subcommands::Diff(crate::plumbing::options::diff::Platform { cmd }) => match cmd {
crate::plumbing::options::diff::SubCommands::Tree {
old_treeish,
new_treeish,
} => prepare_and_run("diff-tree", verbose, None, move |_progress, out, _err| {
core::repository::diff::tree(repository(Mode::Lenient)?, out, old_treeish, new_treeish)
}),
crate::plumbing::options::diff::SubCommands::File {
old_revspec,
new_revspec,
} => prepare_and_run("diff-file", verbose, None, move |_progress, out, _err| {
core::repository::diff::file(repository(Mode::Lenient)?, out, old_revspec, new_revspec)
}),
},
Subcommands::Log(crate::plumbing::options::log::Platform { pathspec }) => {
prepare_and_run("log", verbose, None, move |_progress, out, _err| {
core::repository::log::log(repository(Mode::Lenient)?, out, pathspec)
})
}
Subcommands::Worktree(worktree::Platform { cmd }) => match cmd {
worktree::SubCommands::List => {
prepare_and_run("worktree-list", verbose, None, move |_progress, out, _err| {
core::repository::worktree::list(repository(Mode::Lenient)?, out, format)
})
}
worktree::SubCommands::Add {
branch,
detach,
path,
commit_ish,
} => prepare_and_run("worktree-add", verbose, None, move |progress, out, _err| {
core::repository::worktree::add(
repository(Mode::Lenient)?,
&path,
out,
progress,
&should_interrupt,
core::repository::worktree::AddOptions {
new_branch: branch,
commit_ish,
detach,
format,
},
)
}),
worktree::SubCommands::Remove { force, worktree } => {
prepare_and_run("worktree-remove", verbose, None, move |progress, _out, _err| {
core::repository::worktree::remove(repository(Mode::Lenient)?, &worktree, force, progress, format)
})
}
},
Subcommands::IsClean | Subcommands::IsChanged => {
let mode = if matches!(cmd, Subcommands::IsClean) {
core::repository::dirty::Mode::IsClean
} else {
core::repository::dirty::Mode::IsDirty
};
prepare_and_run("clean", verbose, None, move |_progress, out, _err| {
core::repository::dirty::check(repository(Mode::Lenient)?, mode, out, format)
})
}
#[cfg(feature = "gitoxide-core-tools-clean")]
Subcommands::Clean(crate::plumbing::options::clean::Command {
debug,
dry_run: _,
execute,
ignored,
precious,
directories,
pathspec,
repositories,
pathspec_matches_result,
skip_hidden_repositories,
find_untracked_repositories,
}) => prepare_and_run("clean", verbose, None, move |_progress, out, err| {
core::repository::clean(
repository(Mode::Lenient)?,
out,
err,
pathspec,
core::repository::clean::Options {
debug,
format,
execute,
ignored,
precious,
directories,
repositories,
pathspec_matches_result,
skip_hidden_repositories: skip_hidden_repositories.map(Into::into),
find_untracked_repositories: find_untracked_repositories.into(),
},
)
}),
Subcommands::Status(crate::plumbing::options::status::Platform {
ignored,
untracked,
format: status_format,
statistics,
submodules,
no_write,
pathspec,
index_worktree_renames,
}) => prepare_and_run("status", auto_verbose, None, move |progress, out, err| {
use crate::plumbing::options::status::Submodules;
core::repository::status::show(
repository(Mode::Lenient)?,
pathspec,
out,
err,
progress,
core::repository::status::Options {
format: match status_format.unwrap_or_default() {
crate::plumbing::options::status::Format::Simplified => {
core::repository::status::Format::Simplified
}
crate::plumbing::options::status::Format::PorcelainV2 => {
core::repository::status::Format::PorcelainV2
}
},
ignored: ignored.map(|ignored| match ignored.unwrap_or_default() {
crate::plumbing::options::status::Ignored::Matching => {
core::repository::status::Ignored::Matching
}
crate::plumbing::options::status::Ignored::Collapsed => {
core::repository::status::Ignored::Collapsed
}
}),
untracked: untracked.map(|mode| match mode.unwrap_or_default() {
crate::plumbing::options::status::Untracked::No => gix::status::UntrackedFiles::None,
crate::plumbing::options::status::Untracked::Normal => gix::status::UntrackedFiles::Collapsed,
crate::plumbing::options::status::Untracked::All => gix::status::UntrackedFiles::Files,
}),
output_format: format,
statistics,
thread_limit: thread_limit.or(cfg!(target_os = "macos").then_some(3)), allow_write: !no_write,
index_worktree_renames: index_worktree_renames.map(|percentage| percentage.unwrap_or(0.5)),
submodules: submodules.map(|submodules| match submodules {
Submodules::All => core::repository::status::Submodules::All,
Submodules::RefChange => core::repository::status::Submodules::RefChange,
Submodules::Modifications => core::repository::status::Submodules::Modifications,
Submodules::None => core::repository::status::Submodules::None,
}),
},
)
}),
Subcommands::Dirwalk(crate::plumbing::options::dirwalk::Platform {
statistics,
untracked,
pathspec,
}) => prepare_and_run("dirwalk", auto_verbose, None, move |_progress, out, err| {
core::repository::dirwalk::walk(
repository(Mode::Lenient)?,
pathspec,
out,
err,
core::repository::dirwalk::Options {
output_format: format,
statistics,
untracked: match untracked {
crate::plumbing::options::dirwalk::Untracked::Collapsed => {
core::repository::dirwalk::Untracked::Collapsed
}
crate::plumbing::options::dirwalk::Untracked::Matching => {
core::repository::dirwalk::Untracked::Matching
}
},
},
)
}),
Subcommands::Submodule(platform) => match platform
.cmds
.unwrap_or(crate::plumbing::options::submodule::Subcommands::List { dirty_suffix: None })
{
crate::plumbing::options::submodule::Subcommands::List { dirty_suffix } => {
prepare_and_run("submodule-list", verbose, None, move |_progress, out, _err| {
core::repository::submodule::list(
repository(Mode::Lenient)?,
out,
format,
dirty_suffix.map(|suffix| suffix.unwrap_or_else(|| "dirty".to_string())),
)
})
}
},
#[cfg(feature = "gitoxide-core-tools-archive")]
Subcommands::Archive(crate::plumbing::options::archive::Platform {
format,
prefix,
compression_level,
add_path,
add_virtual_file,
output_file,
treeish,
}) => prepare_and_run("archive", auto_verbose, None, move |progress, _out, _err| {
if add_virtual_file.len() % 2 != 0 {
bail!(
"Virtual files must be specified in pairs of two: slash/separated/path content, got {}",
add_virtual_file.join(", ")
)
}
core::repository::archive::stream(
repository(Mode::Lenient)?,
&output_file,
treeish.as_deref(),
progress,
core::repository::archive::Options {
add_paths: add_path,
prefix,
files: add_virtual_file
.as_chunks::<2>()
.0
.iter()
.map(|c| (c[0].clone(), c[1].clone()))
.collect(),
format: format.map(|f| match f {
crate::plumbing::options::archive::Format::Internal => {
gix::worktree::archive::Format::InternalTransientNonPersistable
}
crate::plumbing::options::archive::Format::Tar => gix::worktree::archive::Format::Tar,
crate::plumbing::options::archive::Format::TarGz => {
gix::worktree::archive::Format::TarGz { compression_level }
}
crate::plumbing::options::archive::Format::Zip => {
gix::worktree::archive::Format::Zip { compression_level }
}
}),
},
)
}),
Subcommands::Branch(platform) => match platform.cmd {
branch::Subcommands::List { all } => {
use core::repository::branch::list;
let kind = if all { list::Kind::All } else { list::Kind::Local };
let options = list::Options { kind };
prepare_and_run("branch-list", auto_verbose, None, move |_progress, out, _err| {
core::repository::branch::list(repository(Mode::Lenient)?, out, format, options)
})
}
},
#[cfg(feature = "gitoxide-core-tools-corpus")]
Subcommands::Corpus(crate::plumbing::options::corpus::Platform { db, path, cmd }) => prepare_and_run(
"corpus",
auto_verbose,
core::corpus::PROGRESS_RANGE,
move |root_progress, _out, _err| {
let mut engine = core::corpus::Engine::open_or_create(
db,
core::corpus::engine::State {
gitoxide_version: option_env!("GIX_VERSION")
.ok_or_raise(|| message("GIX_VERSION must be set in build-script"))?
.into(),
progress: root_progress,
trace,
trace_output,
},
)?;
match cmd {
crate::plumbing::options::corpus::SubCommands::Run {
dry_run,
repo_sql_suffix,
include_task,
} => engine.run(path, thread_limit, dry_run, repo_sql_suffix, include_task),
crate::plumbing::options::corpus::SubCommands::Refresh => engine.refresh(path),
}
},
),
Subcommands::CommitGraph(cmd) => match cmd {
commitgraph::Subcommands::List { long_hashes, spec } => {
prepare_and_run("commitgraph-list", auto_verbose, None, move |_progress, out, _err| {
core::repository::commitgraph::list(repository(Mode::Lenient)?, spec, out, long_hashes, format)
})
.map(|_| ())
}
commitgraph::Subcommands::Verify { statistics } => {
prepare_and_run("commitgraph-verify", auto_verbose, None, move |_progress, out, err| {
let output_statistics = if statistics { Some(format) } else { None };
core::repository::commitgraph::verify(
repository(Mode::Lenient)?,
core::repository::commitgraph::verify::Context {
err,
out,
output_statistics,
},
)
})
.map(|_| ())
}
},
#[cfg(feature = "gitoxide-core-blocking-client")]
Subcommands::Clone(crate::plumbing::options::clone::Platform {
handshake_info,
bare,
no_tags,
ref_name,
revision,
remote,
shallow,
directory,
}) => {
let opts = core::repository::clone::Options {
format,
bare,
handshake_info,
no_tags,
ref_name,
revision,
shallow: shallow.into(),
};
prepare_and_run(
"clone",
auto_verbose,
core::repository::clone::PROGRESS_RANGE,
move |progress, out, err| core::repository::clone(remote, directory, config, progress, out, err, opts),
)
}
#[cfg(feature = "gitoxide-core-blocking-client")]
Subcommands::Fetch(crate::plumbing::options::fetch::Platform {
dry_run,
handshake_info,
negotiation_info,
open_negotiation_graph,
remote,
shallow,
ref_spec,
}) => {
let opts = core::repository::fetch::Options {
format,
dry_run,
remote,
handshake_info,
negotiation_info,
open_negotiation_graph,
shallow: shallow.into(),
ref_specs: ref_spec,
};
prepare_and_run(
"fetch",
auto_verbose,
core::repository::fetch::PROGRESS_RANGE,
move |progress, out, err| {
core::repository::fetch(repository(Mode::LenientWithGitInstallConfig)?, progress, out, err, opts)
},
)
}
Subcommands::ConfigTree => {
prepare_and_run("config-tree", false, None, move |_progress, _out, _err| show_progress())
}
Subcommands::Credential(cmd) => prepare_and_run("credential", false, None, move |_progress, _out, _err| {
core::repository::credential(
repository(Mode::StrictWithGitInstallConfig).ok(),
match cmd {
credential::Subcommands::Fill => gix::credentials::program::main::Action::Get,
credential::Subcommands::Approve => gix::credentials::program::main::Action::Store,
credential::Subcommands::Reject => gix::credentials::program::main::Action::Erase,
},
)
}),
#[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
Subcommands::Remote(crate::plumbing::options::remote::Platform {
name,
cmd,
handshake_info,
}) => {
use crate::plumbing::options::remote;
match cmd {
remote::Subcommands::Url { all, push } => {
prepare_and_run("remote-url", false, None, move |_progress, out, _err| {
core::repository::remote::url(
repository(Mode::LenientWithGitInstallConfig)?,
name.as_deref(),
if push {
gix::remote::Direction::Push
} else {
gix::remote::Direction::Fetch
},
all,
out,
)
})
}
remote::Subcommands::Refs | remote::Subcommands::RefMap { .. } => {
let kind = match cmd {
remote::Subcommands::Refs => core::repository::remote::refs::Kind::Remote,
remote::Subcommands::RefMap {
ref_spec,
show_unmapped_remote_refs,
} => core::repository::remote::refs::Kind::Tracking {
ref_specs: ref_spec,
show_unmapped_remote_refs,
},
remote::Subcommands::Url { .. } => unreachable!("handled above"),
};
let context = core::repository::remote::refs::Options {
name_or_url: name,
format,
handshake_info,
};
prepare_and_run(
"remote-refs",
auto_verbose,
core::repository::remote::refs::PROGRESS_RANGE,
move |progress, out, err| {
let res = core::repository::remote::refs(
repository(Mode::LenientWithGitInstallConfig)?,
kind,
progress,
out,
err,
context,
);
#[cfg(feature = "gitoxide-core-async-client")]
let res = futures_lite::future::block_on(res);
res
},
)
}
}
}
Subcommands::Config(config::Platform { filter, cmd }) => match cmd {
Some(config::Subcommands::Show) | None => {
prepare_and_run("config-show", verbose, None, move |_progress, out, _err| {
core::repository::config::show(
repository(Mode::LenientWithGitInstallConfig)?,
filter,
config,
format,
out,
)
})
.map(|_| ())
}
Some(config::Subcommands::List) => {
prepare_and_run("config-list-files", verbose, None, move |_progress, out, _err| {
core::repository::config::list_files(
repository(Mode::LenientWithGitInstallConfig)?,
config,
format,
out,
)
})
.map(|_| ())
}
Some(config::Subcommands::Fmt {
in_place,
in_file,
out_file,
}) => prepare_and_run("config-fmt", verbose, None, move |_progress, out, _err| {
let repo = in_file
.is_none()
.then(|| repository(Mode::LenientWithGitInstallConfig))
.transpose()?;
core::repository::config::fmt(repo, in_file, out_file, in_place, out)
})
.map(|_| ()),
},
Subcommands::Free(subcommands) => match subcommands {
free::Subcommands::Discover => prepare_and_run("discover", verbose, None, move |_progress, out, _err| {
core::discover(&repository_path, out)
}),
free::Subcommands::Trust { paths } => {
prepare_and_run("trust", verbose, None, move |_progress, out, _err| {
core::trust(&paths, out)
})
}
free::Subcommands::CommitGraph(cmd) => match cmd {
free::commitgraph::Subcommands::Verify { path, statistics } => {
prepare_and_run("commitgraph-verify", auto_verbose, None, move |_progress, out, err| {
let output_statistics = if statistics { Some(format) } else { None };
core::commitgraph::verify(
path,
core::commitgraph::verify::Context {
err,
out,
output_statistics,
},
)
})
.map(|_| ())
}
},
free::Subcommands::Index(free::index::Platform {
object_hash,
index_path,
cmd,
}) => match cmd {
free::index::Subcommands::FromList {
force,
index_output_path,
skip_hash,
file,
} => prepare_and_run("index-from-list", verbose, None, move |_progress, _out, _err| {
core::repository::index::from_list(file, index_output_path, force, object_hash, skip_hash)
}),
free::index::Subcommands::CheckoutExclusive {
directory,
empty_files,
repository,
keep_going,
} => prepare_and_run("index-checkout", auto_verbose, None, move |progress, _out, err| {
core::index::checkout_exclusive(
index_path,
directory,
repository,
err,
progress,
&should_interrupt,
core::index::checkout_exclusive::Options {
index: core::index::Options { object_hash, format },
empty_files,
keep_going,
thread_limit,
},
)
}),
free::index::Subcommands::Info { no_details } => {
prepare_and_run("index-info", verbose, None, move |_progress, out, err| {
core::index::information(
index_path,
out,
err,
core::index::information::Options {
index: core::index::Options { object_hash, format },
extension_details: !no_details,
},
)
})
}
free::index::Subcommands::Verify => {
prepare_and_run("index-verify", auto_verbose, None, move |_progress, out, _err| {
core::index::verify(index_path, out, core::index::Options { object_hash, format })
})
}
},
free::Subcommands::Mailmap {
cmd: free::mailmap::Platform { path, cmd },
} => match cmd {
free::mailmap::Subcommands::Verify => prepare_and_run(
"mailmap-verify",
auto_verbose,
core::mailmap::PROGRESS_RANGE,
move |_progress, out, _err| core::mailmap::verify(path, format, out),
),
},
#[cfg(feature = "gitoxide-core-blocking-client")]
free::Subcommands::Remote(subcommands) => match subcommands {
free::remote::Subcommands::Refs {
protocol,
refs_directory,
write_reflog,
url,
} => prepare_and_run(
"remote-refs",
verbose,
core::remote::PROGRESS_RANGE,
move |progress, out, _err| {
core::remote::refs(
protocol,
&url,
refs_directory,
progress,
core::remote::Context {
format,
out,
object_hash,
write_reflog,
},
)
},
),
},
free::Subcommands::Pack(subcommands) => match subcommands {
free::pack::Subcommands::Create {
repository,
expansion,
thin,
statistics,
nondeterministic_count,
tips,
pack_cache_size_mb,
counting_threads,
object_cache_size_mb,
output_directory,
} => {
let has_tips = !tips.is_empty();
prepare_and_run(
"pack-create",
verbose,
core::pack::create::PROGRESS_RANGE,
move |progress, out, _err| {
let input = if has_tips { None } else { stdin_or_bail()?.into() };
let repository = repository.unwrap_or_else(|| PathBuf::from("."));
let context = core::pack::create::Context {
thread_limit,
thin,
nondeterministic_thread_count: nondeterministic_count.then_some(counting_threads),
pack_cache_size_in_bytes: pack_cache_size_mb.unwrap_or(0) * 1_000_000,
object_cache_size_in_bytes: object_cache_size_mb.unwrap_or(0) * 1_000_000,
statistics: if statistics { Some(format) } else { None },
out,
expansion: expansion.unwrap_or(if has_tips {
core::pack::create::ObjectExpansion::TreeTraversal
} else {
core::pack::create::ObjectExpansion::None
}),
};
core::pack::create(repository, tips, input, output_directory, progress, context)
},
)
}
#[cfg(any(feature = "gitoxide-core-blocking-client", feature = "gitoxide-core-async-client"))]
free::pack::Subcommands::Receive {
protocol,
url,
directory,
refs,
refs_directory,
} => prepare_and_run(
"pack-receive",
verbose,
core::pack::receive::PROGRESS_RANGE,
move |progress, out, _err| {
let res = core::pack::receive(
protocol,
&url,
directory,
refs_directory,
refs.into_iter().map(Into::into).collect(),
progress,
core::pack::receive::Context {
thread_limit,
format,
should_interrupt,
out,
object_hash,
},
);
#[cfg(feature = "gitoxide-core-async-client")]
let res = futures_lite::future::block_on(res);
res
},
),
free::pack::Subcommands::Explode {
check,
sink_compress,
delete_pack,
pack_path,
object_path,
verify,
} => prepare_and_run("pack-explode", auto_verbose, None, move |progress, _out, _err| {
core::pack::explode::pack_or_pack_index(
pack_path,
object_path,
check,
progress,
core::pack::explode::Context {
thread_limit,
delete_pack,
sink_compress,
verify,
should_interrupt,
object_hash,
},
)
}),
free::pack::Subcommands::Verify {
args:
free::pack::VerifyOptions {
algorithm,
decode,
re_encode,
statistics,
},
path,
} => prepare_and_run(
"pack-verify",
auto_verbose,
verify::PROGRESS_RANGE,
move |progress, out, err| {
let mode = verify_mode(decode, re_encode);
let output_statistics = if statistics { Some(format) } else { None };
verify::pack_or_pack_index(
path,
progress,
verify::Context {
output_statistics,
out,
err,
thread_limit,
mode,
algorithm,
should_interrupt: &should_interrupt,
object_hash,
},
)
},
)
.map(|_| ()),
free::pack::Subcommands::MultiIndex(free::pack::multi_index::Platform { multi_index_path, cmd }) => {
match cmd {
free::pack::multi_index::Subcommands::Entries => prepare_and_run(
"pack-multi-index-entries",
verbose,
core::pack::multi_index::PROGRESS_RANGE,
move |_progress, out, _err| core::pack::multi_index::entries(multi_index_path, format, out),
),
free::pack::multi_index::Subcommands::Info => prepare_and_run(
"pack-multi-index-info",
verbose,
core::pack::multi_index::PROGRESS_RANGE,
move |_progress, out, err| {
core::pack::multi_index::info(multi_index_path, format, out, err)
},
),
free::pack::multi_index::Subcommands::Verify => prepare_and_run(
"pack-multi-index-verify",
auto_verbose,
core::pack::multi_index::PROGRESS_RANGE,
move |progress, _out, _err| {
core::pack::multi_index::verify(multi_index_path, progress, &should_interrupt)
},
),
free::pack::multi_index::Subcommands::Create { index_paths } => prepare_and_run(
"pack-multi-index-create",
verbose,
core::pack::multi_index::PROGRESS_RANGE,
move |progress, _out, _err| {
core::pack::multi_index::create(
index_paths,
multi_index_path,
progress,
&should_interrupt,
object_hash,
)
},
),
}
}
free::pack::Subcommands::Index(subcommands) => match subcommands {
free::pack::index::Subcommands::Create {
iteration_mode,
pack_path,
directory,
} => prepare_and_run(
"pack-index-create",
verbose,
core::pack::index::PROGRESS_RANGE,
move |progress, out, _err| {
use gitoxide_core::pack::index::PathOrRead;
let input = if let Some(path) = pack_path {
PathOrRead::Path(path)
} else {
if std::io::stdin().is_terminal() {
bail!(
"Refusing to read from standard input as no path is given, but it's a terminal."
)
}
PathOrRead::Read(Box::new(stdin()))
};
core::pack::index::from_pack(
input,
directory,
progress,
core::pack::index::Context {
thread_limit,
iteration_mode,
format,
out,
object_hash,
should_interrupt: &gix::interrupt::IS_INTERRUPTED,
},
)
},
),
},
},
},
Subcommands::Verify {
args:
free::pack::VerifyOptions {
statistics,
algorithm,
decode,
re_encode,
},
} => prepare_and_run(
"verify",
auto_verbose,
core::repository::verify::PROGRESS_RANGE,
move |progress, out, _err| {
core::repository::verify::integrity(
repository(Mode::Strict)?,
out,
progress,
&should_interrupt,
core::repository::verify::Context {
output_statistics: statistics.then_some(format),
algorithm,
verify_mode: verify_mode(decode, re_encode),
thread_limit,
},
)
},
),
Subcommands::Revision(cmd) => match cmd {
revision::Subcommands::List {
spec,
svg,
limit,
long_hashes,
} => prepare_and_run(
"revision-list",
auto_verbose,
core::repository::revision::list::PROGRESS_RANGE,
move |progress, out, _err| {
core::repository::revision::list(
repository(Mode::Lenient)?,
progress,
out,
core::repository::revision::list::Context {
limit,
spec,
format,
long_hashes,
text: svg.map_or(core::repository::revision::list::Format::Text, |path| {
core::repository::revision::list::Format::Svg { path }
}),
},
)
},
),
revision::Subcommands::PreviousBranches => prepare_and_run(
"revision-previousbranches",
verbose,
None,
move |_progress, out, _err| {
core::repository::revision::previous_branches(repository(Mode::Lenient)?, out, format)
},
),
revision::Subcommands::Explain { spec } => {
prepare_and_run("revision-explain", verbose, None, move |_progress, out, _err| {
core::repository::revision::explain(spec, out)
})
}
revision::Subcommands::Resolve {
specs,
explain,
cat_file,
tree_mode,
reference,
blob_format,
} => prepare_and_run("revision-parse", verbose, None, move |_progress, out, _err| {
core::repository::revision::resolve(
repository(Mode::Strict)?,
specs,
out,
core::repository::revision::resolve::Options {
format,
explain,
cat_file,
show_reference: reference,
tree_mode: match tree_mode {
revision::resolve::TreeMode::Raw => core::repository::revision::resolve::TreeMode::Raw,
revision::resolve::TreeMode::Pretty => {
core::repository::revision::resolve::TreeMode::Pretty
}
},
blob_format: match blob_format {
revision::resolve::BlobFormat::Git => core::repository::revision::resolve::BlobFormat::Git,
revision::resolve::BlobFormat::Worktree => {
core::repository::revision::resolve::BlobFormat::Worktree
}
revision::resolve::BlobFormat::Diff => {
core::repository::revision::resolve::BlobFormat::Diff
}
revision::resolve::BlobFormat::DiffOrGit => {
core::repository::revision::resolve::BlobFormat::DiffOrGit
}
},
},
)
}),
},
Subcommands::Cat { revspec } => prepare_and_run("cat", verbose, None, move |_progress, out, _err| {
core::repository::cat(repository(Mode::Lenient)?, &revspec, out)
}),
Subcommands::Commit(cmd) => match cmd {
commit::Subcommands::Verify { rev_spec } => {
prepare_and_run("commit-verify", auto_verbose, None, move |_progress, _out, _err| {
core::repository::commit::verify(repository(Mode::Lenient)?, rev_spec.as_deref())
})
}
commit::Subcommands::Sign { rev_spec } => {
prepare_and_run("commit-sign", auto_verbose, None, move |_progress, out, _err| {
core::repository::commit::sign(repository(Mode::Lenient)?, rev_spec.as_deref(), out)
})
}
commit::Subcommands::Describe {
annotated_tags,
all_refs,
first_parent,
always,
long,
statistics,
max_candidates,
rev_spec,
dirty_suffix,
} => prepare_and_run("commit-describe", verbose, None, move |_progress, out, err| {
core::repository::commit::describe(
repository(Mode::Strict)?,
rev_spec.as_deref(),
out,
err,
core::repository::commit::describe::Options {
all_tags: !annotated_tags,
all_refs,
long_format: long,
first_parent,
statistics,
max_candidates,
always,
dirty_suffix: dirty_suffix.map(|suffix| suffix.unwrap_or_else(|| "dirty".to_string())),
},
)
}),
},
Subcommands::Tag(platform) => match platform.cmds {
Some(tag::Subcommands::List) | None => {
prepare_and_run("tag-list", auto_verbose, None, move |_progress, out, _err| {
core::repository::tag::list(repository(Mode::Lenient)?, out, format)
})
}
},
Subcommands::Tree(cmd) => match cmd {
tree::Subcommands::Entries {
treeish,
recursive,
extended,
} => prepare_and_run("tree-entries", verbose, None, move |_progress, out, _err| {
core::repository::tree::entries(
repository(Mode::Strict)?,
treeish.as_deref(),
recursive,
extended,
format,
out,
)
}),
tree::Subcommands::Info { treeish, extended } => {
prepare_and_run("tree-info", verbose, None, move |_progress, out, err| {
core::repository::tree::info(
repository(Mode::Strict)?,
treeish.as_deref(),
extended,
format,
out,
err,
)
})
}
},
Subcommands::Odb(cmd) => match cmd {
odb::Subcommands::Stats { extra_header_lookup } => prepare_and_run(
"odb-stats",
auto_verbose,
core::repository::odb::statistics::PROGRESS_RANGE,
move |progress, out, err| {
core::repository::odb::statistics(
repository(Mode::Strict)?,
progress,
out,
err,
core::repository::odb::statistics::Options {
format,
thread_limit,
extra_header_lookup,
},
)
},
),
odb::Subcommands::Entries => prepare_and_run("odb-entries", verbose, None, move |_progress, out, _err| {
core::repository::odb::entries(repository(Mode::Strict)?, format, out)
}),
odb::Subcommands::Info => prepare_and_run("odb-info", verbose, None, move |_progress, out, err| {
core::repository::odb::info(repository(Mode::Strict)?, format, out, err)
}),
},
Subcommands::Fsck(fsck::Platform { spec }) => {
prepare_and_run("fsck", auto_verbose, None, move |_progress, out, _err| {
core::repository::fsck(repository(Mode::Strict)?, spec, out)
})
}
Subcommands::Mailmap(cmd) => match cmd {
mailmap::Subcommands::Entries => {
prepare_and_run("mailmap-entries", verbose, None, move |_progress, out, err| {
core::repository::mailmap::entries(repository(Mode::Lenient)?, format, out, err)
})
}
mailmap::Subcommands::Check { contacts } => {
prepare_and_run("mailmap-check", verbose, None, move |_progress, out, err| {
core::repository::mailmap::check(repository(Mode::Lenient)?, format, contacts, out, err)
})
}
},
Subcommands::Attributes(cmd) => match cmd {
attributes::Subcommands::Query { statistics, pathspec } => {
prepare_and_run("attributes-query", verbose, None, move |_progress, out, err| {
let repo = repository(Mode::Strict)?;
let pathspecs = if pathspec.is_empty() {
PathsOrPatterns::Paths(Box::new(
stdin_or_bail()?
.byte_lines()
.filter_map(std::result::Result::ok)
.map(BString::from),
))
} else {
PathsOrPatterns::Patterns(pathspec)
};
core::repository::attributes::query(
repo,
pathspecs,
out,
err,
core::repository::attributes::query::Options { format, statistics },
)
})
}
attributes::Subcommands::ValidateBaseline { statistics, no_ignore } => prepare_and_run(
"attributes-validate-baseline",
auto_verbose,
None,
move |progress, out, err| {
core::repository::attributes::validate_baseline(
repository(Mode::StrictWithGitInstallConfig)?,
stdin_or_bail().ok().map(|stdin| {
stdin
.byte_lines()
.filter_map(std::result::Result::ok)
.map(gix::bstr::BString::from)
}),
progress,
out,
err,
core::repository::attributes::validate_baseline::Options {
format,
statistics,
ignore: !no_ignore,
},
)
},
),
},
Subcommands::Exclude(cmd) => match cmd {
exclude::Subcommands::Query {
statistics,
patterns,
paths,
show_ignore_patterns,
} => prepare_and_run("exclude-query", verbose, None, move |_progress, out, err| {
let repo = repository(Mode::Strict)?;
let paths = if paths.is_empty() {
PathsOrPatterns::Paths(Box::new(
stdin_or_bail()?
.byte_lines()
.filter_map(std::result::Result::ok)
.map(BString::from),
))
} else {
PathsOrPatterns::Patterns(paths)
};
core::repository::exclude::query(
repo,
paths,
out,
err,
core::repository::exclude::query::Options {
format,
show_ignore_patterns,
overrides: patterns,
statistics,
},
)
}),
},
Subcommands::Index(cmd) => match cmd {
index::Subcommands::Entries {
format: entry_format,
no_attributes,
attributes_from_index,
statistics,
recurse_submodules,
pathspec,
} => prepare_and_run("index-entries", verbose, None, move |_progress, out, err| {
core::repository::index::entries(
repository(Mode::Lenient)?,
pathspec,
out,
err,
core::repository::index::entries::Options {
format,
simple: match entry_format {
index::entries::Format::Simple => true,
index::entries::Format::Rich => false,
},
attributes: if no_attributes {
None
} else {
Some(if attributes_from_index {
core::repository::index::entries::Attributes::Index
} else {
core::repository::index::entries::Attributes::WorktreeAndIndex
})
},
recurse_submodules,
statistics,
},
)
}),
index::Subcommands::FromTree {
force,
index_output_path,
skip_hash,
spec,
} => prepare_and_run("index-from-tree", verbose, None, move |_progress, _out, _err| {
core::repository::index::from_tree(repository(Mode::Strict)?, spec, index_output_path, force, skip_hash)
}),
},
Subcommands::Blame {
statistics,
file,
ranges,
since,
} => prepare_and_run("blame", verbose, None, move |_progress, out, err| {
let repo = repository(Mode::Lenient)?;
let diff_algorithm = repo.diff_algorithm()?;
core::repository::blame::blame_file(
repo,
&file,
gix::blame::Options {
diff_algorithm,
ranges: gix::blame::BlameRanges::from_one_based_inclusive_ranges(ranges)?,
since,
rewrites: Some(gix::diff::Rewrites::default()),
debug_track_path: false,
},
out,
statistics.then_some(err),
)
}),
Subcommands::Completions { shell, out_dir } => {
prepare_and_run("completions", false, None, move |_progress, out, _err| {
let mut app = Args::command();
let shell = shell
.or_else(clap_complete::Shell::from_env)
.ok_or_raise(|| message("The shell could not be derived from the environment"))?;
let bin_name = app.get_name().to_owned();
if let Some(out_dir) = out_dir {
clap_complete::generate_to(shell, &mut app, bin_name, &out_dir).or_error()?;
} else {
clap_complete::generate(shell, &mut app, bin_name, out);
}
Ok(())
})
}
}?;
Ok(())
}
fn stdin_or_bail() -> Result<std::io::BufReader<std::io::Stdin>> {
if std::io::stdin().is_terminal() {
bail!("Refusing to read from standard input while a terminal is connected")
}
Ok(BufReader::new(stdin()))
}
fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode {
match (decode, re_encode) {
(true, false) => verify::Mode::HashCrc32Decode,
(_, true) => verify::Mode::HashCrc32DecodeEncode,
(false, false) => verify::Mode::HashCrc32,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clap() {
use clap::{CommandFactory, Parser};
Args::command().debug_assert();
for flag in ["--progress", "--progress-keep-open"] {
assert!(
Args::try_parse_from(["gix", flag, "env"]).is_err(),
"removed option {flag} stays unavailable"
);
}
}
#[test]
#[cfg(feature = "tix")]
fn tix_aliases_are_visible_and_route_to_tix() -> gix::error::TestResult {
use clap::{CommandFactory, Parser};
let command = Args::command();
let tix = command.find_subcommand("tix").expect("tix is registered");
assert_eq!(
tix.get_visible_aliases().collect::<Vec<_>>(),
["tui", "interactive", "i"],
"all aliases are shown in help"
);
for name in ["tix", "tui", "interactive", "i"] {
let args = Args::try_parse_from(["gix", name])?;
assert!(
matches!(args.cmd, Subcommands::Tix(_)),
"{name} routes to the tix command"
);
}
#[cfg(feature = "tracing")]
{
let args = Args::try_parse_from(["gix", "-tt", "tix"])?;
assert_eq!(args.trace, 2);
assert_eq!(
Args::try_parse_from(["gix", "tix", "-t"])
.expect_err("embedded tix does not repeat the trace flag")
.kind(),
clap::error::ErrorKind::UnknownArgument
);
}
for arguments in [
vec!["gix", "tix", "-x", "main", "--hide", "tag", "topic"],
vec!["gix", "tix", "amend"],
vec!["gix", "tix", "spill"],
] {
assert!(
matches!(Args::try_parse_from(arguments)?.cmd, Subcommands::Tix(_)),
"the complete tix command is delegated"
);
}
for (arguments, requires_repository) in [
(vec!["gix", "tix", "worktrunk"], true),
(vec!["gix", "tix", "worktrunk", "shell-init", "bash"], false),
] {
let Subcommands::Tix(command) = Args::try_parse_from(arguments)?.cmd else {
panic!("worktrunk routes to the tix command")
};
assert_eq!(
command.requires_repository(),
requires_repository,
"repository discovery is required exactly when the worktrunk command needs it"
);
}
assert_eq!(
Args::try_parse_from(["gix", "tix", "--screen", "half"])
.expect_err("screen selection is no longer supported")
.kind(),
clap::error::ErrorKind::UnknownArgument,
"alternate-screen operation has no command-line mode"
);
for help in ["-h", "--help"] {
assert_eq!(
Args::try_parse_from(["gix", "tix", help])
.expect_err("help exits through clap")
.kind(),
clap::error::ErrorKind::DisplayHelp,
"embedded tix supports {help}"
);
}
Ok(())
}
}