michal/tit
Browse tree · Show commit · Download archive
Blob: tests/git_repository.rs
#[allow(dead_code, reason = "the repository test exercises selected Git APIs")]
#[path = "../src/git/repository.rs"]
mod repository;
use std::fs;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use gix::hash::ObjectId;
use repository::{GitRepository, GitRepositoryError};
use tempfile::TempDir;
#[test]
fn opens_empty_sha1_and_sha256_bare_repositories() {
let directory = TempDir::new().expect("create a repository directory");
for format in ["sha1", "sha256"] {
let path = directory.path().join(format);
init_bare(&path, format);
let repository = GitRepository::open(&path).expect("open the bare repository");
assert_eq!(repository.object_format().to_string(), format);
assert!(
repository
.references()
.expect("read empty references")
.is_empty()
);
}
let non_bare = directory.path().join("worktree");
run(Command::new("git").args(["init", "-q"]).arg(&non_bare));
assert!(matches!(
GitRepository::open(&non_bare),
Err(GitRepositoryError::NotBare(_))
));
}
#[test]
fn creates_main_bare_repositories_and_copies_without_git() {
let directory = TempDir::new().expect("create a repository directory");
for kind in [gix::hash::Kind::Sha1, gix::hash::Kind::Sha256] {
let source = directory.path().join(format!("source-{kind}"));
GitRepository::create_bare(&source, kind).expect("create a bare repository");
assert_eq!(
fs::read_to_string(source.join("HEAD")).expect("read HEAD"),
"ref: refs/heads/main\n"
);
let destination = directory.path().join(format!("copy-{kind}"));
assert_eq!(
GitRepository::copy_bare(&source, &destination).expect("copy a bare repository"),
kind
);
assert_eq!(
GitRepository::open(&destination)
.expect("open the copy")
.object_format(),
kind
);
}
let unsafe_source = directory.path().join("unsafe");
GitRepository::create_bare(&unsafe_source, gix::hash::Kind::Sha1)
.expect("create an unsafe-source repository");
std::os::unix::fs::symlink("HEAD", unsafe_source.join("unsafe-link"))
.expect("create a repository symlink");
assert!(matches!(
GitRepository::copy_bare(&unsafe_source, &directory.path().join("unsafe-copy")),
Err(GitRepositoryError::UnsafeFile(_))
));
}
#[test]
fn reads_sorted_refs_and_generates_complete_packs_for_both_hashes() {
let directory = TempDir::new().expect("create a repository directory");
for format in ["sha1", "sha256"] {
let source_path = directory.path().join(format!("source-{format}"));
let first = make_fixture(&source_path, format);
let source = GitRepository::open(&source_path).expect("open the source repository");
let references = source.references().expect("read references");
assert_eq!(references[0].name, b"HEAD");
assert_eq!(
references[0].symbolic_target.as_deref(),
Some(b"refs/heads/main".as_slice())
);
assert!(
references
.windows(2)
.skip(1)
.all(|pair| pair[0].name < pair[1].name)
);
assert!(references.iter().any(|reference| {
reference.name == b"refs/tags/v1" && reference.peeled == Some(first)
}));
let pack = source
.make_pack(&[first], &[])
.expect("generate a complete pack");
assert!(pack.starts_with(b"PACK"));
let destination = directory.path().join(format!("destination-{format}"));
init_bare(&destination, format);
index_pack(&destination, &pack);
assert_object(&destination, first, "commit");
let second = append_commit(&source_path, first, b"second payload", "second.txt");
let source = GitRepository::open(&source_path).expect("open the updated repository");
let incremental = source
.make_pack(&[second], &[first])
.expect("generate an incremental pack");
index_pack(&destination, &incremental);
assert_object(&destination, second, "commit");
}
}
#[test]
fn rejects_unadvertised_wants_and_damaged_reachable_objects() {
let directory = TempDir::new().expect("create a repository directory");
let source_path = directory.path().join("source");
let commit = make_fixture(&source_path, "sha1");
let source = GitRepository::open(&source_path).expect("open the source repository");
let unadvertised = ObjectId::from_hex(b"1111111111111111111111111111111111111111")
.expect("parse an unadvertised ID");
assert!(matches!(
source.make_pack(&[unadvertised], &[]),
Err(GitRepositoryError::UnadvertisedWant)
));
let wrong_format =
ObjectId::from_hex(b"1111111111111111111111111111111111111111111111111111111111111111")
.expect("parse a SHA-256 ID");
assert!(matches!(
source.make_pack(&[commit], &[wrong_format]),
Err(GitRepositoryError::WrongObjectFormat)
));
drop(source);
let object_path = loose_object_path(&source_path, commit);
fs::set_permissions(&object_path, fs::Permissions::from_mode(0o600))
.expect("make the commit object writable");
fs::write(&object_path, b"damaged object").expect("damage the commit object");
let damaged = GitRepository::open(&source_path).expect("open the damaged repository");
assert!(matches!(
damaged.make_pack(&[commit], &[]),
Err(GitRepositoryError::References(_)
| GitRepositoryError::Object { .. }
| GitRepositoryError::DamagedObject { .. })
));
}
fn make_fixture(path: &Path, format: &str) -> ObjectId {
init_bare(path, format);
run(git(path).args(["config", "user.name", "Tit Test"]));
run(git(path).args(["config", "user.email", "tit@example.invalid"]));
run(git(path).args(["config", "tag.gpgsign", "false"]));
let blob = write_blob(path, b"hello from tit\n");
let tree = write_tree(path, blob, "non-ascii-\u{00e5}.txt");
let commit = write_commit(path, tree, None, "first commit");
update_ref(path, "refs/heads/main", commit);
run(git(path).args(["symbolic-ref", "HEAD", "refs/heads/main"]));
run(git(path).args(["tag", "-a", "v1", "-m", "version one", &commit.to_string()]));
commit
}
fn append_commit(path: &Path, parent: ObjectId, contents: &[u8], filename: &str) -> ObjectId {
let blob = write_blob(path, contents);
let tree = write_tree(path, blob, filename);
let commit = write_commit(path, tree, Some(parent), "second commit");
update_ref(path, "refs/heads/main", commit);
commit
}
fn init_bare(path: &Path, format: &str) {
run(Command::new("git")
.args(["init", "-q", "--bare", "--object-format", format])
.arg(path));
}
fn write_blob(path: &Path, contents: &[u8]) -> ObjectId {
let output = run_with_input(git(path).args(["hash-object", "-w", "--stdin"]), contents);
parse_id(&output)
}
fn write_tree(path: &Path, blob: ObjectId, filename: &str) -> ObjectId {
let record = format!("100644 blob {blob}\t{filename}\n");
let output = run_with_input(git(path).arg("mktree"), record.as_bytes());
parse_id(&output)
}
fn write_commit(path: &Path, tree: ObjectId, parent: Option<ObjectId>, message: &str) -> ObjectId {
let mut command = git(path);
command.arg("commit-tree").arg(tree.to_string());
if let Some(parent) = parent {
command.args(["-p", &parent.to_string()]);
}
command.args(["-m", message]);
command.envs([
("GIT_AUTHOR_NAME", "Tit Test"),
("GIT_AUTHOR_EMAIL", "tit@example.invalid"),
("GIT_AUTHOR_DATE", "1700000000 +0000"),
("GIT_COMMITTER_NAME", "Tit Test"),
("GIT_COMMITTER_EMAIL", "tit@example.invalid"),
("GIT_COMMITTER_DATE", "1700000000 +0000"),
]);
parse_id(&run(&mut command).stdout)
}
fn update_ref(path: &Path, name: &str, id: ObjectId) {
run(git(path).args(["update-ref", name, &id.to_string()]));
}
fn index_pack(path: &Path, pack: &[u8]) {
let output = run_with_input(git(path).args(["index-pack", "--stdin"]), pack);
assert!(!output.is_empty());
}
fn assert_object(path: &Path, id: ObjectId, kind: &str) {
let output = run(git(path).args(["cat-file", "-t", &id.to_string()]));
assert_eq!(
String::from_utf8(output.stdout)
.expect("read object type")
.trim(),
kind
);
}
fn loose_object_path(repository: &Path, id: ObjectId) -> PathBuf {
let id = id.to_string();
repository.join("objects").join(&id[..2]).join(&id[2..])
}
fn git(path: &Path) -> Command {
let mut command = Command::new("git");
command.arg("--git-dir").arg(path);
command
}
fn run(command: &mut Command) -> std::process::Output {
let output = command.output().expect("run stock Git");
assert!(
output.status.success(),
"stock Git failed: {}",
String::from_utf8_lossy(&output.stderr)
);
output
}
fn run_with_input(command: &mut Command, input: &[u8]) -> Vec<u8> {
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("start stock Git");
child
.stdin
.take()
.expect("open stock Git input")
.write_all(input)
.expect("write stock Git input");
let output = child.wait_with_output().expect("wait for stock Git");
assert!(
output.status.success(),
"stock Git failed: {}",
String::from_utf8_lossy(&output.stderr)
);
output.stdout
}
fn parse_id(input: &[u8]) -> ObjectId {
ObjectId::from_hex(String::from_utf8_lossy(input).trim().as_bytes()).expect("parse object ID")
}