use std::path::Path;
use std::process::Command;
pub const TREE_CLEAN: &str = "clean";
pub const TREE_DIRTY: &str = "dirty";
pub const TREE_UNKNOWN: &str = "unknown";
const GIT_DIFF_NO_DIFFERENCES: i32 = 0;
const GIT_DIFF_DIFFERENCES: i32 = 1;
pub fn classify(manifest_dir: &Path) -> &'static str {
match tracked_diff_exit_code(manifest_dir) {
Some(GIT_DIFF_NO_DIFFERENCES) => TREE_CLEAN,
Some(GIT_DIFF_DIFFERENCES) => TREE_DIRTY,
_ => TREE_UNKNOWN,
}
}
fn tracked_diff_exit_code(manifest_dir: &Path) -> Option<i32> {
Command::new("git")
.args(["--no-optional-locks", "diff", "--quiet", "HEAD"])
.current_dir(manifest_dir)
.output()
.ok()?
.status
.code()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{git_add, git_command, git_init, write_file};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
const CARGO_OK: &str = ".cargo-ok";
const TRACKED_FILE: &str = "tracked.txt";
fn repo_with_one_commit() -> (TempDir, PathBuf) {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path().to_path_buf();
git_init(&root);
write_file(&root, TRACKED_FILE, "committed contents\n");
git_add(&root, &[TRACKED_FILE]);
let status = git_command(&root)
.args(["commit", "--quiet", "-m", "initial"])
.status()
.expect("git commit");
assert!(status.success(), "git commit must succeed for a provenance fixture");
(temp, root)
}
fn porcelain_status_is_nonempty(dir: &Path) -> bool {
let output = git_command(dir)
.args(["--no-optional-locks", "status", "--porcelain"])
.output()
.expect("git status");
!output.stdout.is_empty()
}
#[test]
fn pristine_checkout_reports_clean() {
let (_temp, root) = repo_with_one_commit();
assert_eq!(classify(&root), TREE_CLEAN);
}
#[test]
fn untracked_files_alone_report_clean() {
let (_temp, root) = repo_with_one_commit();
write_file(&root, CARGO_OK, "ok");
write_file(&root, "stray.log", "noise\n");
assert!(
porcelain_status_is_nonempty(&root),
"fixture is vacuous: git does not see the untracked files this test is about"
);
assert_eq!(
classify(&root),
TREE_CLEAN,
"untracked files must not make a checkout dirty"
);
}
#[test]
fn modified_tracked_file_still_reports_dirty() {
let (_temp, root) = repo_with_one_commit();
write_file(&root, TRACKED_FILE, "edited contents\n");
assert_eq!(classify(&root), TREE_DIRTY);
}
#[test]
fn deleted_tracked_file_still_reports_dirty() {
let (_temp, root) = repo_with_one_commit();
fs::remove_file(root.join(TRACKED_FILE)).expect("delete tracked file");
assert_eq!(classify(&root), TREE_DIRTY);
}
#[test]
fn staged_new_file_reports_dirty() {
let (_temp, root) = repo_with_one_commit();
write_file(&root, "added.rs", "fn added() {}\n");
git_add(&root, &["added.rs"]);
assert_eq!(classify(&root), TREE_DIRTY);
}
#[test]
fn untracked_files_do_not_mask_a_tracked_modification() {
let (_temp, root) = repo_with_one_commit();
write_file(&root, CARGO_OK, "ok");
write_file(&root, TRACKED_FILE, "edited contents\n");
assert_eq!(classify(&root), TREE_DIRTY);
}
#[test]
fn directory_outside_a_repository_reports_unknown() {
let temp = tempfile::tempdir().expect("tempdir");
assert_eq!(classify(temp.path()), TREE_UNKNOWN);
}
#[test]
fn repository_without_a_commit_reports_unknown() {
let temp = tempfile::tempdir().expect("tempdir");
git_init(temp.path());
assert_eq!(classify(temp.path()), TREE_UNKNOWN);
}
}