mod cli;
mod commands;
mod config;
mod discovery;
mod runner;
mod subprocess_utils;
use std::path::Path;
use anyhow::Result;
use clap::Parser;
use cli::{BranchWhat, BuildWhat, Cli, CleanWhat, Commands, CountWhat, ResetWhat, StashWhat, TagWhat};
use config::AppConfig;
fn main() -> Result<()> {
let cli = Cli::parse();
if let Commands::Complete { shell } = &cli.command {
cli::print_completions(*shell);
return Ok(());
}
if matches!(&cli.command, Commands::Version) {
println!("rsmultigit {} by {}", env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_AUTHORS"));
println!("GIT_DESCRIBE: {}", env!("GIT_DESCRIBE"));
println!("GIT_SHA: {}", env!("GIT_SHA"));
println!("GIT_BRANCH: {}", env!("GIT_BRANCH"));
println!("GIT_DIRTY: {}", env!("GIT_DIRTY"));
println!("RUSTC_SEMVER: {}", env!("RUSTC_SEMVER"));
println!("RUST_EDITION: {}", env!("RUST_EDITION"));
println!("BUILD_TIMESTAMP: {}", env!("BUILD_TIMESTAMP"));
return Ok(());
}
let config = AppConfig::from(&cli);
if let Commands::CheckSame { rule } = &cli.command {
warn_if_discovery_flags_set(&cli);
let exit_code = run_check_same(&config, rule.as_deref())?;
std::process::exit(exit_code);
}
let projects = discovery::discover_projects(&config)?;
if projects.is_empty() && !config.no_print_no_projects {
eprintln!("no projects found");
return Ok(());
}
match &cli.command {
Commands::Count { what } => {
let test_fn: fn(&Path) -> anyhow::Result<bool> = match what {
CountWhat::Dirty => commands::count::is_dirty,
CountWhat::Untracked => commands::count::has_untracked,
CountWhat::Synchronized => commands::count::non_synchronized,
};
runner::do_count(&config, &projects, test_fn)?;
}
Commands::Status => {
runner::print_if_data(&config, &projects, commands::status::do_status)?;
}
Commands::Dirty => {
runner::print_if_data(&config, &projects, commands::status::do_dirty)?;
}
Commands::ListProjects => {
runner::print_if_data(&config, &projects, |project: &Path| {
Ok(Some(project.display().to_string()))
})?;
}
Commands::Age => {
runner::print_if_data(&config, &projects, commands::age::do_age)?;
}
Commands::Authors => {
runner::print_if_data(&config, &projects, commands::authors::do_authors)?;
}
Commands::Config { key } => {
let key = key.clone();
runner::print_if_data(&config, &projects, move |project: &Path| {
commands::config::do_config(project, &key)
})?;
}
Commands::Size => {
runner::print_if_data(&config, &projects, commands::size::do_size)?;
}
Commands::LastTag => {
runner::print_if_data(&config, &projects, commands::last_tag::do_last_tag)?;
}
Commands::Branch { what } => {
let branch_fn: fn(&Path) -> anyhow::Result<bool> = match what {
BranchWhat::Local => commands::branch::branch_local,
BranchWhat::Remote => commands::branch::branch_remote,
BranchWhat::Github => commands::branch::branch_github,
};
runner::do_for_all_projects(&config, &projects, branch_fn)?;
}
Commands::Pull { quiet } => {
let quiet = *quiet;
runner::do_for_all_projects(&config, &projects, move |project: &Path| -> anyhow::Result<bool> {
commands::pull::do_pull(project, quiet)
})?;
}
Commands::Push => {
runner::do_for_all_projects(&config, &projects, commands::push::do_push)?;
}
Commands::Fetch => {
runner::do_for_all_projects(&config, &projects, commands::fetch::do_fetch)?;
}
Commands::Clean { what } => {
let clean_fn: fn(&Path) -> anyhow::Result<bool> = match what {
CleanWhat::Hard => commands::clean::clean_hard,
CleanWhat::Soft => commands::clean::clean_soft,
CleanWhat::Make => commands::clean::clean_make,
CleanWhat::Git => commands::clean::clean_git,
CleanWhat::Cargo => commands::clean::clean_cargo,
};
runner::do_for_all_projects(&config, &projects, clean_fn)?;
}
Commands::Stash { what } => {
let stash_fn: fn(&Path) -> anyhow::Result<bool> = match what {
StashWhat::Push => commands::stash::stash_push,
StashWhat::Pop => commands::stash::stash_pop,
};
runner::do_for_all_projects(&config, &projects, stash_fn)?;
}
Commands::Reset { what } => {
let reset_fn: fn(&Path) -> anyhow::Result<bool> = match what {
ResetWhat::Hard => commands::reset::reset_hard,
ResetWhat::Soft => commands::reset::reset_soft,
ResetWhat::Mixed => commands::reset::reset_mixed,
};
runner::do_for_all_projects(&config, &projects, reset_fn)?;
}
Commands::Diff => {
runner::do_for_all_projects(&config, &projects, commands::diff::do_diff)?;
}
Commands::Log { count } => {
let count = *count;
runner::do_for_all_projects(&config, &projects, move |project: &Path| -> anyhow::Result<bool> {
commands::log::do_log(project, count)
})?;
}
Commands::Tag { what } => match what {
TagWhat::Local | TagWhat::Remote => {
let tag_fn: fn(&Path) -> anyhow::Result<bool> = match what {
TagWhat::Local => commands::tag::tag_local,
TagWhat::Remote => commands::tag::tag_remote,
_ => unreachable!(),
};
runner::do_for_all_projects(&config, &projects, tag_fn)?;
}
TagWhat::HasLocal | TagWhat::HasRemote => {
let test_fn: fn(&Path) -> anyhow::Result<bool> = match what {
TagWhat::HasLocal => commands::tag::tag_has_local,
TagWhat::HasRemote => commands::tag::tag_has_remote,
_ => unreachable!(),
};
runner::do_count(&config, &projects, test_fn)?;
}
}
Commands::Remote => {
runner::do_for_all_projects(&config, &projects, commands::remote::do_remote)?;
}
Commands::Prune => {
runner::do_for_all_projects(&config, &projects, commands::prune::do_prune)?;
}
Commands::Gc => {
runner::do_for_all_projects(&config, &projects, commands::gc::do_gc)?;
}
Commands::Checkout { branch } => {
let branch = branch.clone();
runner::do_for_all_projects(&config, &projects, move |project: &Path| -> anyhow::Result<bool> {
commands::checkout::do_checkout(project, &branch)
})?;
}
Commands::Commit { message } => {
let message = message.clone();
runner::do_for_all_projects(&config, &projects, move |project: &Path| -> anyhow::Result<bool> {
commands::commit::do_commit(project, &message)
})?;
}
Commands::SubmoduleUpdate => {
runner::do_for_all_projects(&config, &projects, commands::submodule::submodule_update)?;
}
Commands::Blame { file } => {
let file = file.clone();
runner::do_for_all_projects(&config, &projects, move |project: &Path| -> anyhow::Result<bool> {
commands::blame::do_blame(project, &file)
})?;
}
Commands::Grep { regexp, files } => {
let regexp = regexp.clone();
let files = *files;
runner::do_for_all_projects(&config, &projects, move |project: &Path| -> anyhow::Result<bool> {
commands::grep::do_grep(project, ®exp, files)
})?;
}
Commands::Build { what } => {
type ProjectFn = fn(&Path) -> anyhow::Result<bool>;
let (check_fn, build_fn): (ProjectFn, ProjectFn) = match what {
BuildWhat::Bootstrap => (commands::build::check_not_disabled, commands::build::build_bootstrap),
BuildWhat::Pydmt => (commands::build::check_pydmt, commands::build::build_pydmt),
BuildWhat::Make => (commands::build::check_not_disabled, commands::build::build_make),
BuildWhat::VenvMake => (commands::build::check_not_disabled, commands::build::build_venv_make),
BuildWhat::VenvPydmt => (commands::build::check_pydmt, commands::build::build_venv_pydmt),
BuildWhat::PydmtBuildVenv => (commands::build::check_pydmt, commands::build::build_pydmt_build_venv),
BuildWhat::Rsconstruct => (commands::build::check_rsconstruct, commands::build::build_rsconstruct),
BuildWhat::Cargo => (commands::build::check_cargo, commands::build::build_cargo),
BuildWhat::CargoPublish => (commands::build::check_cargo, commands::build::build_cargo_publish),
};
runner::do_for_all_projects_with_check(&config, &projects, check_fn, build_fn)?;
}
Commands::CheckSame { .. } => unreachable!("handled above"),
Commands::Complete { .. } => unreachable!("handled above"),
Commands::Version => unreachable!("handled above"),
}
Ok(())
}
fn warn_if_discovery_flags_set(cli: &Cli) {
if !cli.folders.is_empty() {
eprintln!("warning: --folders is ignored by check-same (repos come from check.toml)");
}
if cli.no_glob {
eprintln!("warning: --no-glob is ignored by check-same (repos come from check.toml)");
}
if cli.glob != "*/*" {
eprintln!("warning: --glob is ignored by check-same (repos come from check.toml)");
}
}
fn run_check_same(app: &AppConfig, only_rule: Option<&str>) -> Result<i32> {
use commands::check;
let config_path = check::default_config_path()?;
let cfg = check::load_config(&config_path)?;
let projects = check::resolve_repos(&cfg)?;
let rules: Vec<&check::Rule> = cfg
.check
.iter()
.filter(|r| match only_rule {
Some(name) => r.name == name,
None => r.enabled,
})
.collect();
if rules.is_empty() {
if let Some(name) = only_rule {
eprintln!("no rule named {name:?} in {}", config_path.display());
return Ok(1);
}
if app.verbose {
println!("no rules to check");
}
return Ok(0);
}
let mut any_mismatch = false;
for rule in rules {
let result = check::evaluate_rule(rule, &projects)?;
if result.is_consistent() {
if app.verbose {
if !app.terse && !app.no_header {
println!("[{}]", result.name);
}
println!("ok ({} files)", result.total_files);
}
continue;
}
any_mismatch = true;
if app.terse {
println!("{}", result.name);
continue;
}
if !app.no_header {
println!("[{}]", result.name);
}
println!(
"{} files, {} groups{}",
result.total_files,
result.groups.len(),
if result.skipped.is_empty() {
String::new()
} else {
format!(" ({} skipped)", result.skipped.len())
},
);
if !app.no_output {
for (i, group) in result.groups.iter().enumerate() {
let label = group_label(i);
println!(" group {label} ({} files):", group.len());
for file in group {
println!(" {}", file.display());
}
}
}
}
Ok(if any_mismatch { 1 } else { 0 })
}
fn group_label(i: usize) -> String {
let mut n = i;
let mut s = String::new();
loop {
s.insert(0, (b'A' + (n % 26) as u8) as char);
n /= 26;
if n == 0 {
break;
}
n -= 1;
}
s
}