use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use super::*;
struct LocalRepo {
root: PathBuf,
repo: Repo,
}
impl LocalRepo {
fn new(name: &str) -> Self {
let root = local_workspace(name);
let repo = init(&root).expect("initialize local git repository");
repo.config_set("user.name", "Test User")
.expect("set user.name");
repo.config_set("user.email", "test@example.com")
.expect("set user.email");
Self { root, repo }
}
fn write(&self, path: &str, content: &str) {
let full_path = self.root.join(path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).expect("create parent directories");
}
fs::write(full_path, content).expect("write repository file");
}
fn commit_all(&self, message: &str) -> Oid {
let paths = self
.repo
.status()
.expect("read status")
.into_iter()
.map(|entry| entry.path)
.collect::<Vec<_>>();
let refs = paths.iter().map(String::as_str).collect::<Vec<_>>();
if !refs.is_empty() {
self.repo.stage(&refs).expect("stage status paths");
}
self.repo
.commit(message, None)
.expect("commit staged changes")
}
}
impl Drop for LocalRepo {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn local_workspace(name: &str) -> PathBuf {
let base = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("target")
.join("rskit-git-tests");
fs::create_dir_all(&base).expect("create local test workspace");
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time after epoch")
.as_nanos();
let root = base.join(format!("{name}-{}-{unique}", std::process::id()));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).expect("create test repo directory");
root
}
#[test]
fn repository_facade_manages_refs_config_and_remotes_locally() {
let local = LocalRepo::new("manage");
local.write(
"README.md",
"# local
",
);
let initial = local.commit_all("initial commit");
local
.repo
.create_branch("feature", "HEAD")
.expect("create branch");
local
.repo
.create_tag("v1.0.0", "HEAD", Some("release notes"))
.expect("create annotated tag");
local
.repo
.create_tag("v1.0.1", "HEAD", None)
.expect("create lightweight tag");
local
.repo
.exec(&[
"remote",
"add",
"origin",
&format!("file://{}", local.root.display()),
])
.expect("add local remote");
let branches = local
.repo
.list_branches(BranchFilter::All)
.expect("list branches");
assert!(branches.iter().any(|branch| branch.name == "feature"));
let tags = local.repo.list_tags().expect("list tags");
let annotated = tags
.iter()
.find(|tag| tag.name == "v1.0.0")
.expect("annotated tag exists");
assert_eq!(annotated.message, "release notes");
assert_eq!(annotated.target, initial);
assert!(
tags.iter()
.any(|tag| tag.name == "v1.0.1" && tag.message.is_empty())
);
let remotes = local.repo.list_remotes().expect("list remotes");
assert_eq!(remotes[0].name, "origin");
assert!(remotes[0].url.starts_with("file://"));
assert_eq!(
local.repo.config_get("user.email").expect("get config"),
"test@example.com"
);
let missing = local
.repo
.config_get("rskit.missing")
.expect_err("missing config maps to error");
assert_eq!(missing.code(), rskit_errors::ErrorCode::NotFound);
let duplicate = local
.repo
.create_branch("feature", "HEAD")
.expect_err("duplicate branch maps to conflict");
assert_eq!(duplicate.code(), rskit_errors::ErrorCode::AlreadyExists);
}
#[test]
fn repository_facade_handles_index_checkout_stash_and_clean() {
let local = LocalRepo::new("write");
local.write(
"tracked.txt",
"one
",
);
local.commit_all("initial commit");
local.write(
"tracked.txt",
"two
",
);
local
.repo
.stage(&["tracked.txt"])
.expect("stage tracked file");
assert_eq!(
local.repo.staged_entries().expect("list staged entries")[0].state,
EntryState::Staged
);
local
.repo
.reset("HEAD", ResetMode::Mixed)
.expect("mixed reset");
assert!(
local
.repo
.staged_entries()
.expect("list staged entries after reset")
.is_empty()
);
local
.repo
.checkout_files(&["tracked.txt"])
.expect("restore tracked file");
assert_eq!(
fs::read_to_string(local.root.join("tracked.txt")).expect("read restored file"),
"one
"
);
local.write(
"tracked.txt",
"stashed
",
);
let stash_oid = local.repo.stash("work in progress").expect("create stash");
assert!(!stash_oid.is_zero());
let stashes = local.repo.stash_list().expect("list stashes");
assert_eq!(stashes[0].index, 0);
assert!(stashes[0].message.contains("work in progress"));
local.repo.stash_pop().expect("pop stash");
assert_eq!(
fs::read_to_string(local.root.join("tracked.txt")).expect("read popped file"),
"stashed
"
);
local.write(
"scratch/file.txt",
"remove me
",
);
let dry_run = local
.repo
.clean(Some(&CleanOptions {
directories: true,
..Default::default()
}))
.expect("dry-run clean");
assert!(dry_run.iter().any(|path| path == "scratch/"));
local
.repo
.clean(Some(&CleanOptions {
directories: true,
force: true,
..Default::default()
}))
.expect("force clean");
assert!(!local.root.join("scratch").exists());
}
#[test]
fn commits_support_explicit_signatures_and_amend_without_signing() {
let local = LocalRepo::new("commit-options");
local.write(
"file.txt", "one
",
);
local.repo.stage(&["file.txt"]).expect("stage file");
let signature = Signature {
name: "Before Epoch".to_string(),
email: "before@example.com".to_string(),
when: UNIX_EPOCH - std::time::Duration::from_secs(60),
};
let first = local
.repo
.commit(
"initial",
Some(&CommitOptions {
author: Some(signature.clone()),
committer: Some(signature),
..Default::default()
}),
)
.expect("commit with explicit signature");
assert!(!first.is_zero());
local.write(
"file.txt", "two
",
);
local.repo.stage(&["file.txt"]).expect("stage amendment");
let amended = local
.repo
.commit(
"amended",
Some(&CommitOptions {
amend: true,
..Default::default()
}),
)
.expect("amend commit");
assert_ne!(first, amended);
let signing = local
.repo
.commit(
"signed",
Some(&CommitOptions {
sign: true,
..Default::default()
}),
)
.expect_err("signing is unsupported");
assert_eq!(signing.code(), rskit_errors::ErrorCode::InvalidInput);
}