use std::path::Path;
use std::process::Command;
use super::opts::PostInstallScanOpts;
use super::outcome::SkipReason;
pub const SKIP_ENV_VAR: &str = "DIFFLORE_SKIP_POST_INSTALL_SCAN";
const CI_ENV_VARS: &[&str] = &["CI", "GITHUB_ACTIONS", "GITLAB_CI"];
#[derive(Debug, Clone, Copy)]
pub struct GuardSignals {
pub stdin_is_tty: bool,
pub stdout_is_tty: bool,
pub gh_on_path: bool,
pub is_git_repo: bool,
pub has_github_remote: bool,
pub in_ci: bool,
pub explicit_skip: bool,
}
pub const fn run_guards_with(
signals: GuardSignals,
non_interactive: bool,
) -> Result<(), SkipReason> {
if signals.explicit_skip {
return Err(SkipReason::ExplicitlySkipped);
}
if signals.in_ci {
return Err(SkipReason::RunningInCi);
}
if non_interactive || !signals.stdin_is_tty || !signals.stdout_is_tty {
return Err(SkipReason::NonInteractive);
}
if !signals.is_git_repo {
return Err(SkipReason::NotAGitRepo);
}
if !signals.has_github_remote {
return Err(SkipReason::NoGitHubRemote);
}
if !signals.gh_on_path {
return Err(SkipReason::GhNotInstalled);
}
Ok(())
}
pub fn run_guards(opts: &PostInstallScanOpts) -> Result<(), SkipReason> {
use std::io::IsTerminal;
let signals = GuardSignals {
stdin_is_tty: std::io::stdin().is_terminal(),
stdout_is_tty: std::io::stdout().is_terminal(),
gh_on_path: which::which("gh").is_ok(),
is_git_repo: is_git_repo(&opts.cwd),
has_github_remote: has_github_remote(&opts.cwd),
in_ci: detect_ci(),
explicit_skip: detect_explicit_skip(),
};
run_guards_with(signals, opts.non_interactive)
}
fn detect_explicit_skip() -> bool {
match std::env::var(SKIP_ENV_VAR) {
Ok(v) => is_truthy(&v),
Err(_) => false,
}
}
fn detect_ci() -> bool {
CI_ENV_VARS
.iter()
.any(|name| std::env::var(name).is_ok_and(|v| is_truthy(&v)))
}
fn is_truthy(v: &str) -> bool {
matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes")
}
fn is_git_repo(cwd: &Path) -> bool {
let Ok(output) = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.current_dir(cwd)
.output()
else {
return false;
};
output.status.success()
&& String::from_utf8_lossy(&output.stdout).trim() == "true"
}
fn has_github_remote(cwd: &Path) -> bool {
let path = cwd.to_string_lossy().into_owned();
difflore_core::github_import::detect_repo_from_remote(&path).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn base_signals() -> GuardSignals {
GuardSignals {
stdin_is_tty: true,
stdout_is_tty: true,
gh_on_path: true,
is_git_repo: true,
has_github_remote: true,
in_ci: false,
explicit_skip: false,
}
}
#[test]
fn happy_path_passes_all_guards() {
assert_eq!(run_guards_with(base_signals(), false), Ok(()));
}
#[test]
fn explicit_skip_short_circuits_everything() {
let mut s = base_signals();
s.explicit_skip = true;
s.in_ci = true;
s.stdin_is_tty = false;
assert_eq!(
run_guards_with(s, false),
Err(SkipReason::ExplicitlySkipped)
);
}
#[test]
fn ci_env_var_blocks_offer_even_on_interactive_terminal() {
let mut s = base_signals();
s.in_ci = true;
assert_eq!(run_guards_with(s, false), Err(SkipReason::RunningInCi));
}
#[test]
fn non_interactive_flag_or_pipe_skips_with_non_interactive_reason() {
assert_eq!(
run_guards_with(base_signals(), true),
Err(SkipReason::NonInteractive)
);
let mut s = base_signals();
s.stdin_is_tty = false;
assert_eq!(
run_guards_with(s, false),
Err(SkipReason::NonInteractive)
);
let mut s = base_signals();
s.stdout_is_tty = false;
assert_eq!(
run_guards_with(s, false),
Err(SkipReason::NonInteractive)
);
}
#[test]
fn missing_git_repo_skips_before_gh_check() {
let mut s = base_signals();
s.is_git_repo = false;
s.gh_on_path = false; assert_eq!(run_guards_with(s, false), Err(SkipReason::NotAGitRepo));
}
#[test]
fn missing_github_remote_skips_after_repo_check() {
let mut s = base_signals();
s.has_github_remote = false;
assert_eq!(run_guards_with(s, false), Err(SkipReason::NoGitHubRemote));
}
#[test]
fn missing_gh_cli_is_lowest_priority_skip_reason() {
let mut s = base_signals();
s.gh_on_path = false;
assert_eq!(run_guards_with(s, false), Err(SkipReason::GhNotInstalled));
}
#[test]
fn truthy_helper_matches_common_ci_values() {
for v in ["1", "true", "TRUE", "yes", "Yes", " true "] {
assert!(is_truthy(v), "expected truthy: {v:?}");
}
for v in ["", "0", "false", "no", "off", "FALSE "] {
assert!(!is_truthy(v), "expected falsy: {v:?}");
}
}
}