#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
#![allow(clippy::panic)]
use serde_json::Value;
use std::{
path::Path,
process::{Command, Stdio},
};
use tempfile::TempDir;
const AHEAD: &str = "A";
const BEHIND: &str = "B";
fn git(dir: &Path, args: &[&str]) {
let output = Command::new("git")
.current_dir(dir)
.args(["-c", "user.name=slick test"])
.args(["-c", "user.email=test@example.com"])
.args(["-c", "commit.gpgsign=false"])
.args(["-c", "init.defaultBranch=main"])
.args(args)
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.unwrap_or_else(|error| panic!("failed to run git {args:?}: {error}"));
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn git_is_available() -> bool {
Command::new("git")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
fn repo_ahead_1_behind_2() -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let root = dir.path();
let work = root.join("work");
let other = root.join("other");
git(root, &["init", "--quiet", "--bare", "remote.git"]);
git(root, &["init", "--quiet", "work"]);
std::fs::write(work.join("a.txt"), "a\n").unwrap();
git(&work, &["add", "a.txt"]);
git(&work, &["commit", "--quiet", "-m", "initial"]);
git(&work, &["remote", "add", "origin", "../remote.git"]);
git(
&work,
&["push", "--quiet", "-u", "origin", "HEAD:refs/heads/main"],
);
git(root, &["clone", "--quiet", "remote.git", "other"]);
for message in ["remote-1", "remote-2"] {
std::fs::write(other.join("a.txt"), format!("{message}\n")).unwrap();
git(&other, &["commit", "--quiet", "-am", message]);
}
git(&other, &["push", "--quiet"]);
std::fs::write(work.join("z.txt"), "z\n").unwrap();
git(&work, &["add", "z.txt"]);
git(&work, &["commit", "--quiet", "-m", "local-1"]);
(dir, work)
}
fn precmd_phases(work: &Path, cache_dir: &Path, fetch: &str) -> Vec<Vec<String>> {
let output = Command::new(env!("CARGO_BIN_EXE_slick"))
.arg("precmd")
.current_dir(work)
.env("SLICK_PROMPT_GIT_REMOTE_AHEAD", AHEAD)
.env("SLICK_PROMPT_GIT_REMOTE_BEHIND", BEHIND)
.env("SLICK_PROMPT_GIT_FETCH", fetch)
.env("SLICK_PROMPT_GIT_FETCH_TIMEOUT", "60")
.env("SLICK_TEST_AUTH_CACHE_DIR", cache_dir)
.output()
.expect("slick precmd should run");
assert!(output.status.success(), "slick precmd should succeed");
String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
let value: Value = serde_json::from_str(line)
.unwrap_or_else(|error| panic!("bad JSON {line}: {error}"));
value["remote"]
.as_array()
.expect("remote should be an array")
.iter()
.map(|entry| entry.as_str().unwrap_or_default().to_string())
.collect()
})
.collect()
}
#[test]
fn test_precmd_reports_behind_commits_discovered_by_its_own_fetch() {
if !git_is_available() {
return;
}
let (_dir, work) = repo_ahead_1_behind_2();
let cache = TempDir::new().unwrap();
let phases = precmd_phases(&work, cache.path(), "1");
assert!(
phases.len() >= 2,
"precmd should emit at least the fast and status phases, got {phases:?}"
);
assert_eq!(
phases[0],
vec![format!("{AHEAD}1")],
"phase 1 should show the pre-fetch state"
);
let last = phases.last().expect("at least one phase");
assert_eq!(
*last,
vec![format!("{BEHIND}2"), format!("{AHEAD}1")],
"final phase must report 2 commits to pull and 1 to push after the fetch"
);
}
#[test]
fn test_precmd_does_not_re_emit_when_fetch_changes_nothing() {
if !git_is_available() {
return;
}
let (_dir, work) = repo_ahead_1_behind_2();
let cache = TempDir::new().unwrap();
precmd_phases(&work, cache.path(), "1");
git(&work, &["fetch", "--quiet"]);
let phases = precmd_phases(&work, cache.path(), "1");
assert_eq!(
phases.len(),
2,
"an unchanged fetch must not trigger a third phase, got {phases:?}"
);
assert_eq!(
*phases.last().unwrap(),
vec![format!("{BEHIND}2"), format!("{AHEAD}1")]
);
}
#[test]
fn test_precmd_with_fetch_disabled_stays_on_local_refs() {
if !git_is_available() {
return;
}
let (_dir, work) = repo_ahead_1_behind_2();
let cache = TempDir::new().unwrap();
let phases = precmd_phases(&work, cache.path(), "0");
assert_eq!(
phases.len(),
2,
"fetch is disabled, so there is nothing to refresh, got {phases:?}"
);
for phase in &phases {
assert_eq!(
*phase,
vec![format!("{AHEAD}1")],
"without fetching, only the stale local ahead count is known"
);
}
}