use crate::repo_read_config_scan::ScopeEnvironment;
use crate::repo_read_query::tests::{answered, declined, mine, scope};
use crate::repo_read_query::{Query, answer_with};
use crate::test_support::{REAL_GIT, fixture, git, git_output, remove};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
fn referenced_repository(root: &Path, name: &str, format: &str) -> PathBuf {
let repo = root.join(name);
std::fs::create_dir(&repo).expect("dir");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main", &format!("--object-format={format}")]);
git(repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=first"]);
git(repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=second"]);
git(repo.as_path(), &["remote", "add", "origin", "https://example.invalid/a.git"]);
git(repo.as_path(), &["update-ref", "refs/remotes/origin/main", "HEAD~1"]);
git(repo.as_path(), &["symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"]);
git(repo.as_path(), &["tag", "light"]);
git(repo.as_path(), &["tag", "-a", "-m", "annotated", "v1"]);
git(repo.as_path(), &["branch", "feature/deep/name"]);
return repo;
}
fn pairs(items: &[(&str, &str)]) -> Vec<(OsString, OsString)> {
return items.iter().map(|(name, value)| { return (OsString::from(*name), OsString::from(*value)); }).collect();
}
#[test]
fn configuration_listings_equal_git() {
let root = fixture("repo-read-refs-config");
let repo = referenced_repository(root.as_path(), "repo", "sha1");
let extra = root.join("extra.config");
let global = root.join("global.config");
std::fs::write(&extra, "[extra]\n\tvalue = from include\n[Mixed \"Sub Section\"]\n\tKey = V\n").expect("extra");
std::fs::write(&global, format!("[user]\n\tname = Someone\n[include]\n\tpath = {}\n[flag]\n\tbare\n", extra.display())).expect("global");
git(repo.as_path(), &["config", "--local", "include.path", extra.to_str().expect("UTF-8")]);
let scope = ScopeEnvironment { home: Some(root.join("no-home")), no_system: true, global: Some(global.clone()), ..ScopeEnvironment::default() };
let fixed = [("GIT_CONFIG_NOSYSTEM", "1"), ("GIT_CONFIG_GLOBAL", global.to_str().expect("UTF-8"))];
let injected = [("GIT_CONFIG_COUNT", "2"), ("GIT_CONFIG_KEY_0", "core.hooksPath"), ("GIT_CONFIG_VALUE_0", "/dev/null/no-hooks"), ("GIT_CONFIG_KEY_1", "Hook.Pre-Commit.Enabled"), ("GIT_CONFIG_VALUE_1", "false")];
let expected = std::process::Command::new(REAL_GIT)
.current_dir(repo.as_path())
.env_clear()
.envs(fixed)
.envs(injected)
.args(["config", "--list", "-z", "--show-scope"])
.output()
.expect("config listing");
let given: Vec<OsString> = ["config", "--list", "-z", "--show-scope"].iter().map(OsString::from).collect();
let added = pairs(&injected);
let found = answer_with(&Query { arguments: given.as_slice(), directory: Some(repo.as_path()), variables: added.as_slice(), input: None }, pairs(&fixed), &scope).expect("answered");
assert_eq!(found.stdout, expected.stdout);
assert!(expected.stdout.windows(5).any(|window| { return window == b"extra"; }), "the included file is listed");
assert!(expected.stdout.windows(7).any(|window| { return window == b"command"; }), "the injected entries are listed");
git(repo.as_path(), &["config", "--local", "includeIf.gitdir:/nowhere/.path", "extra.config"]);
assert!(mine(root.as_path(), repo.as_path(), &["config", "--list", "-z", "--show-scope"]).is_none());
remove(root.as_path());
}
#[test]
fn worktree_configuration_listings_equal_git() {
let root = fixture("repo-read-refs-worktree-config");
let repo = referenced_repository(root.as_path(), "repo", "sha1");
let linked = root.join("linked");
git(repo.as_path(), &["worktree", "add", "--quiet", "-b", "side", linked.to_str().expect("UTF-8"), "HEAD"]);
git(repo.as_path(), &["config", "--local", "extensions.worktreeConfig", "true"]);
std::fs::write(repo.join(".git/config.worktree"), "[user]\n\tname = Only Here\n[include]\n\tpath = wt-extra\n[flag]\n\tbare\n").expect("main file");
std::fs::write(repo.join(".git/wt-extra"), "[extra]\n\tvalue = from include\n").expect("included");
let linked_git = repo.join(".git/worktrees/linked");
std::fs::write(linked_git.join("config.worktree"), "[user]\n\tname = Linked\n").expect("linked file");
let listing = ["config", "--list", "-z", "--show-scope"];
let expected = git_output(repo.as_path(), &listing);
assert!(expected.stdout.windows(15).any(|window| { return window == b"worktree\0user.n"; }), "Git lists the per-worktree file under its own scope");
answered(root.as_path(), repo.as_path(), &listing);
answered(root.as_path(), linked.as_path(), &listing);
git(repo.as_path(), &["config", "--local", "extensions.worktreeConfig", "false"]);
assert!(!git_output(repo.as_path(), &listing).stdout.windows(9).any(|window| { return window == b"worktree\0"; }));
answered(root.as_path(), repo.as_path(), &listing);
git(repo.as_path(), &["config", "--local", "extensions.worktreeConfig", "true"]);
std::fs::write(repo.join(".git/config.worktree"), "[includeIf \"gitdir:/nowhere/\"]\n\tpath = wt-extra\n").expect("conditional");
declined(root.as_path(), repo.as_path(), &listing);
std::fs::write(repo.join(".git/config.worktree"), "[user]\n\tname = Main only\n").expect("main file");
std::fs::remove_file(linked_git.join("config.worktree")).expect("no linked file");
answered(root.as_path(), linked.as_path(), &listing);
remove(root.as_path());
}
#[test]
fn local_listings_and_remote_urls_equal_git() {
let root = fixture("repo-read-refs-remote");
let repo = referenced_repository(root.as_path(), "repo", "sha1");
answered(root.as_path(), repo.as_path(), &["config", "--list", "-z", "--show-scope"]);
answered(root.as_path(), repo.as_path(), &["remote", "get-url", "origin"]);
answered(root.as_path(), repo.as_path(), &["remote", "get-url", "missing"]);
std::fs::create_dir_all(repo.join(".git/remotes")).expect("legacy directory");
std::fs::write(repo.join(".git/remotes/legacy"), "URL: https://example.invalid/legacy.git\n").expect("legacy remote");
declined(root.as_path(), repo.as_path(), &["remote", "get-url", "legacy"]);
git(repo.as_path(), &["remote", "set-url", "--add", "origin", "https://example.invalid/b.git"]);
declined(root.as_path(), repo.as_path(), &["remote", "get-url", "origin"]);
let _ = git_output(repo.as_path(), &["config", "--unset-all", "remote.origin.url"]);
git(repo.as_path(), &["config", "remote.origin.url", "https://example.invalid/a.git"]);
answered(root.as_path(), repo.as_path(), &["remote", "get-url", "origin"]);
git(repo.as_path(), &["config", "url.https://mirror.invalid/.insteadOf", "https://example.invalid/"]);
declined(root.as_path(), repo.as_path(), &["remote", "get-url", "origin"]);
remove(root.as_path());
}
#[test]
fn references_equal_git() {
let root = fixture("repo-read-refs-all");
for format in ["sha1", "sha256"] {
let repo = referenced_repository(root.as_path(), format, format);
git(repo.as_path(), &["config", "branch.main.remote", "origin"]);
let shapes = |target: &Path| {
for name in ["refs/heads/main", "refs/heads/feature/deep/name", "refs/tags/light", "refs/tags/v1", "refs/remotes/origin/main", "refs/remotes/origin/HEAD"] {
answered(root.as_path(), target, &["rev-parse", "--verify", "--quiet", &format!("{name}^{{commit}}")]);
}
declined(root.as_path(), target, &["rev-parse", "--verify", "--quiet", "refs/heads/missing^{commit}"]);
declined(root.as_path(), target, &["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
answered(root.as_path(), target, &["for-each-ref", "--format=%(refname)%00%(objectname)%00%(symref)"]);
};
shapes(repo.as_path());
declined(root.as_path(), repo.as_path(), &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
git(repo.as_path(), &["config", "--unset", "branch.main.remote"]);
answered(root.as_path(), repo.as_path(), &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
git(repo.as_path(), &["config", "branch.main.remote", "origin"]);
git(repo.as_path(), &["config", "branch.main.merge", "refs/heads/main"]);
answered(root.as_path(), repo.as_path(), &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
git(repo.as_path(), &["pack-refs", "--all", "--prune"]);
shapes(repo.as_path());
answered(root.as_path(), repo.as_path(), &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
git(repo.as_path(), &["branch", "origin/main"]);
declined(root.as_path(), repo.as_path(), &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
git(repo.as_path(), &["branch", "--delete", "origin/main"]);
git(repo.as_path(), &["checkout", "--quiet", "--detach"]);
declined(root.as_path(), repo.as_path(), &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
}
remove(root.as_path());
}
#[test]
fn the_symbolic_head_equals_git() {
let root = fixture("repo-read-refs-symbolic");
let repo = referenced_repository(root.as_path(), "repo", "sha1");
answered(root.as_path(), repo.as_path(), &["symbolic-ref", "--quiet", "HEAD"]);
git(repo.as_path(), &["checkout", "--quiet", "--detach"]);
answered(root.as_path(), repo.as_path(), &["symbolic-ref", "--quiet", "HEAD"]);
git(repo.as_path(), &["symbolic-ref", "HEAD", "refs/heads/unborn"]);
answered(root.as_path(), repo.as_path(), &["symbolic-ref", "--quiet", "HEAD"]);
declined(root.as_path(), repo.as_path(), &["symbolic-ref", "HEAD"]);
remove(root.as_path());
}
#[test]
fn chains_repeats_and_shadows_are_declined() {
let root = fixture("repo-read-refs-chains");
let repo = referenced_repository(root.as_path(), "repo", "sha1");
git(repo.as_path(), &["config", "branch.main.remote", "origin"]);
git(repo.as_path(), &["config", "branch.main.merge", "refs/heads/main"]);
let upstream = ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"];
answered(root.as_path(), repo.as_path(), &upstream);
git(repo.as_path(), &["config", "--add", "branch.main.merge", "refs/heads/feature/deep/name"]);
declined(root.as_path(), repo.as_path(), &upstream);
git(repo.as_path(), &["config", "--unset-all", "branch.main.merge"]);
git(repo.as_path(), &["config", "branch.main.merge", "refs/heads/main"]);
answered(root.as_path(), repo.as_path(), &upstream);
std::fs::create_dir_all(repo.join(".git/origin")).expect("directory");
std::fs::write(repo.join(".git/origin/main"), format!("{}\n", "1".repeat(40))).expect("file");
declined(root.as_path(), repo.as_path(), &upstream);
std::fs::remove_dir_all(repo.join(".git/origin")).expect("remove");
git(repo.as_path(), &["symbolic-ref", "refs/remotes/origin/main", "refs/remotes/origin/gone"]);
declined(root.as_path(), repo.as_path(), &upstream);
git(repo.as_path(), &["update-ref", "--no-deref", "-d", "refs/remotes/origin/main"]);
git(repo.as_path(), &["update-ref", "refs/remotes/origin/main", "HEAD"]);
answered(root.as_path(), repo.as_path(), &upstream);
git(repo.as_path(), &["symbolic-ref", "refs/heads/alias", "refs/heads/main"]);
git(repo.as_path(), &["symbolic-ref", "HEAD", "refs/heads/alias"]);
declined(root.as_path(), repo.as_path(), &["symbolic-ref", "--quiet", "HEAD"]);
declined(root.as_path(), repo.as_path(), &upstream);
remove(root.as_path());
}
#[test]
fn injected_content_keys_are_declined() {
let root = fixture("repo-read-refs-injected");
let repo = referenced_repository(root.as_path(), "repo", "sha1");
let given: Vec<OsString> = ["ls-files", "--stage", "-z", "--full-name"].iter().map(OsString::from).collect();
let host = scope(root.as_path());
let ask = |key: &str| -> bool {
let injected = pairs(&[("GIT_CONFIG_COUNT", "1"), ("GIT_CONFIG_KEY_0", key), ("GIT_CONFIG_VALUE_0", "x")]);
return answer_with(&Query { arguments: given.as_slice(), directory: Some(repo.as_path()), variables: injected.as_slice(), input: None }, Vec::new(), &host).is_some();
};
assert!(ask("core.hooksPath"), "a hook setting is plain");
assert!(!ask("diff.orderFile"), "an injected order file");
assert!(!ask("Core.IgnoreCase"), "an injected case rule, spelled with capitals");
remove(root.as_path());
}
#[test]
fn broken_refs_are_declined() {
let root = fixture("repo-read-refs-broken");
let repo = referenced_repository(root.as_path(), "repo", "sha1");
answered(root.as_path(), repo.as_path(), &["for-each-ref", "--format=%(refname)%00%(objectname)%00%(symref)"]);
let missing = "1".repeat(40);
std::fs::write(repo.join(".git/refs/heads/broken"), format!("{missing}\n")).expect("broken");
declined(root.as_path(), repo.as_path(), &["for-each-ref", "--format=%(refname)%00%(objectname)%00%(symref)"]);
std::fs::remove_file(repo.join(".git/refs/heads/broken")).expect("remove");
std::fs::write(repo.join(".git/refs/heads/spaced"), format!("{} \n", "2".repeat(40))).expect("odd");
declined(root.as_path(), repo.as_path(), &["for-each-ref", "--format=%(refname)%00%(objectname)%00%(symref)"]);
std::fs::remove_file(repo.join(".git/refs/heads/spaced")).expect("remove");
let _ = scope(root.as_path());
remove(root.as_path());
}