use std::path::PathBuf;
use std::process::ExitCode;
use clap::builder::styling::{AnsiColor, Styles};
use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand};
use color_eyre::eyre::eyre;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
mod color;
mod common;
mod config;
mod diff;
mod history;
mod maintenance;
mod object;
mod progress;
mod refs;
mod repo;
mod transfer;
mod tree;
mod worktree;
pub use common::Context;
#[derive(Debug, Parser)]
#[command(name = "girt", version, about, propagate_version = true, styles = STYLES)]
struct Cli {
#[arg(short, long, global = true, action = ArgAction::Count)]
verbose: u8,
#[arg(short = 'C', global = true, value_name = "PATH")]
chdir: Option<PathBuf>,
#[arg(long, global = true)]
git: bool,
#[arg(long, global = true, value_enum, default_value = "auto")]
color: color::Color,
#[arg(short, long, global = true)]
quiet: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
#[command(subcommand)]
Repo(repo::Command),
Init(repo::InitArgs),
Clone(transfer::CloneArgs),
#[command(subcommand)]
Object(object::Command),
#[command(subcommand)]
Tree(tree::Command),
#[command(subcommand, name = "ref")]
Ref(refs::Command),
Log(history::LogArgs),
#[command(subcommand)]
History(history::Command),
Diff(diff::DiffArgs),
#[command(subcommand)]
Index(worktree::IndexCommand),
Status(worktree::StatusArgs),
#[command(subcommand)]
Ignore(worktree::IgnoreCommand),
Checkout(worktree::CheckoutArgs),
#[command(subcommand)]
Worktree(worktree::WorktreeCommand),
#[command(subcommand)]
Config(config::Command),
#[command(subcommand)]
Remote(config::RemoteCommand),
Fetch(transfer::FetchArgs),
Push(transfer::PushArgs),
#[command(subcommand)]
Pack(maintenance::PackCommand),
#[command(subcommand)]
Maintenance(maintenance::Command),
}
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().bold())
.usage(AnsiColor::Green.on_default().bold())
.literal(AnsiColor::Cyan.on_default().bold())
.placeholder(AnsiColor::Cyan.on_default())
.error(AnsiColor::Red.on_default().bold())
.valid(AnsiColor::Cyan.on_default().bold())
.invalid(AnsiColor::Yellow.on_default().bold());
fn main() -> ExitCode {
let args: Vec<_> = std::env::args_os().collect();
let early_color = color::Color::early(&args);
let mut command = Cli::command();
let matches = match command.try_get_matches_from_mut(args) {
Ok(matches) => matches,
Err(error) => {
let enabled = if error.use_stderr() {
early_color.stderr()
} else {
early_color.stdout()
};
let choice = if enabled {
anstream::ColorChoice::Always
} else {
anstream::ColorChoice::Never
};
choice.write_global();
let command = command.color(if enabled {
clap::ColorChoice::Always
} else {
clap::ColorChoice::Never
});
error.with_cmd(&command).exit();
}
};
let cli = Cli::from_arg_matches(&matches).unwrap_or_else(|error| error.exit());
let stderr_color = cli.color.stderr() && !cli.git;
if stderr_color {
anstream::ColorChoice::Always
} else {
anstream::ColorChoice::Never
}
.write_global();
if let Err(error) = install_reporting(cli.verbose, stderr_color) {
eprintln!("error: {error}");
return ExitCode::FAILURE;
}
if let Some(directory) = &cli.chdir
&& let Err(error) = std::env::set_current_dir(directory)
{
eprintln!("error: cannot change to {}: {error}", directory.display());
return ExitCode::FAILURE;
}
let mut context = Context::new(cli.git);
context.color = cli.color.stdout() && !cli.git;
context.progress = !cli.quiet && !cli.git;
match run(cli.command, &context) {
Ok(code) => code,
Err(error) if is_broken_pipe(&error) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Error: {error:?}");
ExitCode::FAILURE
}
}
}
fn install_reporting(verbose: u8, color: bool) -> color_eyre::Result<()> {
let theme = if color {
color_eyre::config::Theme::dark()
} else {
color_eyre::config::Theme::new()
};
color_eyre::config::HookBuilder::default()
.theme(theme)
.display_env_section(false)
.display_location_section(false)
.install()?;
let default = match verbose {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
let filter = match std::env::var("GIRT_LOG") {
Ok(directives) => EnvFilter::try_new(&directives)
.map_err(|error| eyre!("invalid GIRT_LOG {directives:?}: {error}"))?,
Err(std::env::VarError::NotPresent) => EnvFilter::new(default),
Err(error) => return Err(eyre!("invalid GIRT_LOG: {error}")),
};
tracing_subscriber::registry()
.with(filter)
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_ansi(color),
)
.with(tracing_error::ErrorLayer::default())
.try_init()?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(context))]
fn run(command: Command, context: &Context) -> color_eyre::Result<ExitCode> {
match command {
Command::Repo(command) => repo::run(command, context),
Command::Init(args) => repo::init(args, context),
Command::Clone(args) => transfer::clone(args, context),
Command::Object(command) => object::run(command, context),
Command::Tree(command) => tree::run(command, context),
Command::Ref(command) => refs::run(command, context),
Command::Log(args) => history::log(args, context),
Command::History(command) => history::run(command, context),
Command::Diff(args) => diff::run(args, context),
Command::Index(command) => worktree::index(command, context),
Command::Status(args) => worktree::status(args, context),
Command::Ignore(command) => worktree::ignore(command, context),
Command::Checkout(args) => worktree::checkout(args, context),
Command::Worktree(command) => worktree::worktree(command, context),
Command::Config(command) => config::run(command, context),
Command::Remote(command) => config::remote(command, context),
Command::Fetch(args) => transfer::fetch(args, context),
Command::Push(args) => transfer::push(args, context),
Command::Pack(command) => maintenance::pack(command, context),
Command::Maintenance(command) => maintenance::run(command, context),
}
}
fn is_broken_pipe(error: &color_eyre::Report) -> bool {
error.chain().any(|cause| {
cause
.downcast_ref::<std::io::Error>()
.is_some_and(|error| error.kind() == std::io::ErrorKind::BrokenPipe)
})
}