#![cfg(feature = "git")]
mod common;
use anyhow::Result;
use assert_cmd::prelude::*;
use common::dircat_cmd;
use predicates::prelude::*;
use std::fs;
use tempfile::{tempdir, TempDir};
struct TestRemote {
_temp_dir: TempDir,
repo: git2::Repository,
url: String,
}
impl TestRemote {
fn new() -> Result<Self> {
let temp_dir = tempdir()?;
let repo_path = temp_dir.path();
let repo = git2::Repository::init_bare(repo_path)?;
#[cfg(windows)]
let url = format!("file:///{}", repo_path.to_str().unwrap().replace('\\', "/"));
#[cfg(not(windows))]
let url = format!("file://{}", repo_path.to_str().unwrap());
Ok(Self {
_temp_dir: temp_dir,
repo,
url,
})
}
fn commit_file(&self, branch: &str, path: &str, content: &str, msg: &str) -> Result<()> {
let signature = git2::Signature::now("Test User", "test@example.com")?;
let parent_commit = self
.repo
.find_branch(branch, git2::BranchType::Local)
.ok()
.and_then(|b| b.get().peel_to_commit().ok());
let parent_tree = parent_commit.as_ref().and_then(|c| c.tree().ok());
let mut tree_builder = if let Some(tree) = &parent_tree {
self.repo.treebuilder(Some(tree))?
} else {
self.repo.treebuilder(None)?
};
let blob_oid = self.repo.blob(content.as_bytes())?;
tree_builder.insert(path, blob_oid, 0o100644)?;
let tree_oid = tree_builder.write()?;
let tree = self.repo.find_tree(tree_oid)?;
let parents: Vec<&git2::Commit> = parent_commit.iter().collect();
let commit_oid = self.repo.commit(
None, &signature, &signature, msg, &tree, &parents,
)?;
let commit = self.repo.find_commit(commit_oid)?;
self.repo.branch(branch, &commit, true)?;
Ok(())
}
fn set_default_branch(&self, branch: &str) -> Result<()> {
self.repo.set_head(&format!("refs/heads/{}", branch))?;
Ok(())
}
fn delete_branch(&self, branch: &str) -> Result<()> {
let mut branch_ref = self.repo.find_branch(branch, git2::BranchType::Local)?;
branch_ref.delete()?;
Ok(())
}
}
#[test]
fn test_git_update_with_different_default_branch() -> Result<()> {
let remote = TestRemote::new()?;
remote.commit_file("master", "file.txt", "v1", "Initial commit")?;
remote.set_default_branch("master")?;
let cache_dir = tempdir()?;
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", cache_dir.path())
.arg(&remote.url)
.assert()
.success()
.stdout(predicate::str::contains("v1"));
remote.commit_file("master", "file.txt", "v2", "Update file")?;
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", cache_dir.path())
.arg(&remote.url)
.assert()
.success()
.stdout(predicate::str::contains("v2"))
.stdout(predicate::str::contains("v1").not());
Ok(())
}
#[test]
fn test_git_update_with_specific_branch() -> Result<()> {
let remote = TestRemote::new()?;
remote.commit_file("main", "main.txt", "main v1", "main commit")?;
remote.commit_file("develop", "dev.txt", "dev v1", "develop commit")?;
remote.set_default_branch("main")?;
let cache_dir = tempdir()?;
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", cache_dir.path())
.arg(&remote.url)
.arg("--git-branch")
.arg("develop")
.assert()
.success()
.stdout(predicate::str::contains("dev v1"))
.stdout(predicate::str::contains("main v1").not());
remote.commit_file("develop", "dev.txt", "dev v2", "Update develop")?;
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", cache_dir.path())
.arg(&remote.url)
.arg("--git-branch")
.arg("develop")
.assert()
.success()
.stdout(predicate::str::contains("dev v2"))
.stdout(predicate::str::contains("dev v1").not());
Ok(())
}
#[test]
fn test_git_update_prunes_deleted_branch() -> Result<()> {
let remote = TestRemote::new()?;
remote.commit_file("main", "main.txt", "v1", "main commit")?;
remote.commit_file("feature", "feature.txt", "feature content", "feat commit")?;
remote.set_default_branch("main")?;
let cache_dir = tempdir()?;
let repos_base_dir = cache_dir.path();
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", repos_base_dir)
.arg(&remote.url)
.assert()
.success();
let cached_repo_path = fs::read_dir(repos_base_dir)?.next().unwrap()?.path();
let cached_repo = git2::Repository::open(&cached_repo_path)?;
assert!(cached_repo
.find_reference("refs/remotes/origin/feature")
.is_ok());
remote.delete_branch("feature")?;
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", repos_base_dir)
.arg(&remote.url)
.assert()
.success();
let cached_repo_reopened = git2::Repository::open(&cached_repo_path)?;
assert!(cached_repo_reopened
.find_reference("refs/remotes/origin/feature")
.is_err());
Ok(())
}
#[test]
fn test_git_update_non_existent_branch() -> Result<()> {
let remote = TestRemote::new()?;
remote.commit_file("main", "main.txt", "v1", "main commit")?;
remote.set_default_branch("main")?;
let cache_dir = tempdir()?;
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", cache_dir.path())
.arg(&remote.url)
.assert()
.success();
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", cache_dir.path())
.arg(&remote.url)
.arg("--git-branch")
.arg("ghost-branch")
.assert()
.failure()
.stderr(predicate::str::contains(
"Could not find remote branch or tag named 'ghost-branch' after fetch.",
));
Ok(())
}
#[test]
fn test_git_update_to_specific_tag() -> Result<()> {
let remote = TestRemote::new()?;
remote.commit_file("main", "file.txt", "v1 content", "Commit v1")?;
let commit_v1 = remote
.repo
.find_branch("main", git2::BranchType::Local)?
.get()
.peel_to_commit()?;
remote.repo.tag(
"v1.0",
commit_v1.as_object(),
&git2::Signature::now("Test User", "test@example.com")?,
"Tag v1.0",
false,
)?;
remote.commit_file("main", "file.txt", "v2 content", "Commit v2")?;
remote.set_default_branch("main")?;
let cache_dir = tempdir()?;
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", cache_dir.path())
.arg(&remote.url)
.assert()
.success()
.stdout(predicate::str::contains("v2 content"));
dircat_cmd()
.env("DIRCAT_TEST_CACHE_DIR", cache_dir.path())
.arg(&remote.url)
.arg("--git-branch")
.arg("v1.0")
.assert()
.success()
.stdout(predicate::str::contains("v1 content"))
.stdout(predicate::str::contains("v2 content").not());
Ok(())
}