use std::path::Path;
use std::process::ExitCode;
use crate::impact;
use crate::telemetry;
#[derive(Clone, Copy, clap::Subcommand)]
pub enum ImpactCli {
Enable,
Disable,
Default {
#[arg(value_enum)]
state: ToggleState,
},
Reset {
#[arg(long)]
all: bool,
},
Status,
}
#[derive(Clone, Copy, clap::ValueEnum)]
pub enum ToggleState {
On,
Off,
}
#[derive(Clone, Copy, clap::ValueEnum)]
pub enum ImpactSortCli {
Recent,
Resolved,
Contained,
Name,
}
impl ImpactSortCli {
const fn to_impact(self) -> impact::CrossRepoSort {
match self {
Self::Recent => impact::CrossRepoSort::Recent,
Self::Resolved => impact::CrossRepoSort::Resolved,
Self::Contained => impact::CrossRepoSort::Contained,
Self::Name => impact::CrossRepoSort::Name,
}
}
}
#[derive(Clone, Copy)]
pub struct ImpactCrossRepoOpts {
pub all: bool,
pub sort: ImpactSortCli,
pub limit: Option<usize>,
}
pub fn dispatch_impact(
root: &Path,
quiet: bool,
output: fallow_config::OutputFormat,
subcommand: Option<ImpactCli>,
cross_repo: ImpactCrossRepoOpts,
) -> ExitCode {
let ImpactCrossRepoOpts { all, sort, limit } = cross_repo;
if all {
if subcommand.is_some() {
return crate::emit_known_failure(
"`fallow impact --all` is a read-only cross-repo view and cannot be combined \
with a subcommand (enable/disable/default/reset)",
2,
output,
telemetry::FailureReason::Validation,
);
}
return render_impact_all(quiet, output, sort, limit);
}
match subcommand {
Some(ImpactCli::Enable) => impact_enable(root, quiet),
Some(ImpactCli::Disable) => impact_disable(root, quiet),
Some(ImpactCli::Default { state }) => impact_set_default(state, quiet),
Some(ImpactCli::Reset { all }) => impact_reset(root, all, quiet),
Some(ImpactCli::Status) | None => render_impact_status(root, quiet, output),
}
}
fn impact_enable(root: &Path, quiet: bool) -> ExitCode {
let newly = impact::enable(root);
if !quiet {
if newly {
println!(
"Fallow Impact enabled for this project. Each `fallow audit` / pre-commit \
gate run is recorded in your user config dir (never written into the \
repo, never uploaded)."
);
println!(
"Tip: run `fallow init --hooks` (or add `--gate-marker pre-commit` to \
your existing hook's `fallow audit` line) so blocked-then-fixed \
commits are recorded as contained."
);
} else {
println!("Fallow Impact is already enabled.");
}
}
ExitCode::SUCCESS
}
fn impact_disable(root: &Path, quiet: bool) -> ExitCode {
let was_enabled = impact::disable(root);
if !quiet {
println!(
"{}",
if was_enabled {
"Fallow Impact disabled. Existing history is retained."
} else {
"Fallow Impact was already disabled."
}
);
}
ExitCode::SUCCESS
}
fn impact_set_default(state: ToggleState, quiet: bool) -> ExitCode {
let on = matches!(state, ToggleState::On);
let changed = impact::set_global_default(on);
if !quiet {
let verb = if on { "on" } else { "off" };
let body = if on {
"New projects now record Impact by default. A per-project `fallow impact \
disable` still opts that repo out."
} else {
"New projects no longer record by default; run `fallow impact enable` per \
project to opt in."
};
if changed {
println!("Fallow Impact default set to {verb}. {body}");
} else {
println!("Fallow Impact default was already {verb}.");
}
}
ExitCode::SUCCESS
}
fn impact_reset(root: &Path, all: bool, quiet: bool) -> ExitCode {
if all {
let removed = impact::reset_all();
if !quiet {
println!(
"{}",
if removed {
"Removed all Fallow Impact history."
} else {
"No Fallow Impact history to remove."
}
);
}
} else {
let removed = impact::reset(root);
if !quiet {
println!(
"{}",
if removed {
"Removed this project's Fallow Impact history."
} else {
"No Fallow Impact history for this project."
}
);
}
}
ExitCode::SUCCESS
}
fn render_impact_status(root: &Path, quiet: bool, output: fallow_config::OutputFormat) -> ExitCode {
let store = impact::load(root);
let report = impact::build_report(&store);
let is_human = matches!(output, fallow_config::OutputFormat::Human);
let rendered = match output {
fallow_config::OutputFormat::Json => impact::render_json(&report),
fallow_config::OutputFormat::Markdown => impact::render_markdown(&report),
fallow_config::OutputFormat::Human => impact::render_human(&report),
fallow_config::OutputFormat::Sarif
| fallow_config::OutputFormat::Compact
| fallow_config::OutputFormat::CodeClimate
| fallow_config::OutputFormat::PrCommentGithub
| fallow_config::OutputFormat::PrCommentGitlab
| fallow_config::OutputFormat::ReviewGithub
| fallow_config::OutputFormat::ReviewGitlab
| fallow_config::OutputFormat::Badge
| fallow_config::OutputFormat::GithubAnnotations
| fallow_config::OutputFormat::GithubSummary => {
return crate::emit_known_failure(
"impact supports human, json, and markdown output",
2,
output,
telemetry::FailureReason::UnsupportedFormat,
);
}
};
println!("{rendered}");
if is_human && !quiet {
println!(" Store key: {}", impact::resolved_project_key(root));
match impact::resolved_store_path(root) {
Some(path) => println!(" Store file: {}", path.display()),
None => println!(" Store file: (no user config dir resolved; not persisted)"),
}
}
ExitCode::SUCCESS
}
fn render_impact_all(
quiet: bool,
output: fallow_config::OutputFormat,
sort: ImpactSortCli,
limit: Option<usize>,
) -> ExitCode {
let report = impact::aggregate(sort.to_impact());
let is_human = matches!(output, fallow_config::OutputFormat::Human);
let rendered = match output {
fallow_config::OutputFormat::Json => impact::render_cross_repo_json(&report),
fallow_config::OutputFormat::Markdown => impact::render_cross_repo_markdown(&report),
fallow_config::OutputFormat::Human => impact::render_cross_repo_human(&report, limit),
fallow_config::OutputFormat::Sarif
| fallow_config::OutputFormat::Compact
| fallow_config::OutputFormat::CodeClimate
| fallow_config::OutputFormat::PrCommentGithub
| fallow_config::OutputFormat::PrCommentGitlab
| fallow_config::OutputFormat::ReviewGithub
| fallow_config::OutputFormat::ReviewGitlab
| fallow_config::OutputFormat::Badge
| fallow_config::OutputFormat::GithubAnnotations
| fallow_config::OutputFormat::GithubSummary => {
return crate::emit_known_failure(
"impact --all supports human, json, and markdown output",
2,
output,
telemetry::FailureReason::UnsupportedFormat,
);
}
};
println!("{rendered}");
if is_human
&& !quiet
&& let Some(dir) = impact::store_dir()
{
println!(" Stores: {}", dir.display());
}
ExitCode::SUCCESS
}