use std::ops::ControlFlow;
use std::process::Command;
use std::sync::atomic::AtomicBool;
use girt::config::{ConfigFile, ConfigInputs, ConfigScope};
use girt::fetch::{FetchError, FetchLimits, discover_local, receive_local};
use girt::push::{
ForcePolicy, PreparedPush, PushCommand, PushError, PushFailure, PushLimits, RejectionOrigin,
send_local,
};
use girt::refs::{Backend, Expected, RefName, Target};
use girt::transport::TransportControl;
use girt::{
Commit, CommitFields, Config, InitKind, ObjectFormat, PackLimits, Repository, Signature, Tree,
};
use rstest::rstest;
fn repository(
root: &tempfile::TempDir,
name: &str,
format: ObjectFormat,
backend: Backend,
) -> Repository {
Repository::init_with_backend(format, root.path().join(name), InitKind::Bare, backend).unwrap()
}
fn commit(repository: &Repository) -> girt::ObjectId {
let store = repository.loose_objects();
let tree = Tree::new(repository.object_format(), vec![]).unwrap();
store.write_tree(&tree).unwrap();
let person = Signature {
name: b"Local Test".to_vec(),
email: b"local@example.invalid".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let commit = Commit::new(CommitFields {
tree: tree.id(),
parents: vec![],
author: person.clone(),
committer: person,
extra_headers: vec![],
message: b"Local transfer\n".to_vec(),
})
.unwrap();
let id = store.write_commit(&commit).unwrap();
let name = RefName::new("refs/heads/main").unwrap();
repository
.references()
.unwrap()
.update_without_reflog(&name, Target::Direct(id), Expected::Absent)
.unwrap();
id
}
fn git_cat_file(repository: &Repository, id: girt::ObjectId) -> Vec<u8> {
let output = Command::new("git")
.arg("--git-dir")
.arg(plain_local_path(repository.git_dir()))
.args(["cat-file", "commit", &id.to_string()])
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
output.stdout
}
fn plain_local_path(path: &std::path::Path) -> String {
let value = path.to_string_lossy();
#[cfg(windows)]
{
value.strip_prefix(r"\\?\").unwrap_or(&value).to_owned()
}
#[cfg(not(windows))]
{
value.into_owned()
}
}
#[rstest]
#[case::sha1(ObjectFormat::Sha1)]
#[case::sha256(ObjectFormat::Sha256)]
fn native_push_creates_and_deletes_git_reflog_with_explicit_identity(#[case] format: ObjectFormat) {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", format, Backend::Files);
let dest = repository(&root, "dest", format, Backend::Files);
let id = commit(&source);
use std::io::Write;
std::fs::OpenOptions::new()
.append(true)
.open(dest.git_dir().join("config"))
.unwrap()
.write_all(b"\n[core]\n logAllRefUpdates = true\n")
.unwrap();
let command = PushCommand::new(RefName::new("refs/heads/main").unwrap(), None, id)
.with_force(ForcePolicy::FastForwardOnly);
let cancel = AtomicBool::new(false);
let objects = source.objects_with(PackLimits::default()).unwrap();
let prepared =
PreparedPush::new(&objects, vec![command], &[], PushLimits::default(), &cancel).unwrap();
assert!(matches!(
send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&cancel),
|_| {}
),
Err(PushError::NotSent(PushFailure::Identity))
));
let identity = Signature {
name: b"Pusher".to_vec(),
email: b"push@example.invalid".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let report = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared).with_identity(Some(&identity)),
TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(report.all_succeeded());
assert!(report.refs[0].effects.is_some());
let log = dest
.references()
.unwrap()
.reflog(&RefName::new("refs/heads/main").unwrap())
.unwrap()
.unwrap();
assert_eq!(log.len(), 1);
assert_eq!(log[0].committer, identity);
assert_eq!(log[0].new, id);
let git_log = Command::new("git")
.arg("--git-dir")
.arg(plain_local_path(dest.git_dir()))
.args(["reflog", "show", "--format=%H", "refs/heads/main"])
.output()
.unwrap();
assert!(git_log.status.success());
assert!(git_log.stdout.starts_with(id.to_string().as_bytes()));
let deletion = {
let input = PushCommand::new(
RefName::new("refs/heads/main").unwrap(),
Some(id),
girt::ObjectId::null(format),
);
input.with_force(ForcePolicy::FastForwardOnly)
};
let prepared = PreparedPush::new(
&objects,
vec![deletion],
&[],
PushLimits::default(),
&cancel,
)
.unwrap();
let report = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared).with_identity(Some(&identity)),
TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(report.all_succeeded());
assert!(
dest.references()
.unwrap()
.reflog(&RefName::new("refs/heads/main").unwrap())
.unwrap()
.is_none()
);
let git_log = Command::new("git")
.arg("--git-dir")
.arg(plain_local_path(dest.git_dir()))
.args(["reflog", "exists", "refs/heads/main"])
.status()
.unwrap();
assert!(!git_log.success());
}
#[test]
fn native_current_branch_policies_distinguish_updates_and_deletes() {
use std::io::Write;
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha1, Backend::Files);
let dest = Repository::init(
ObjectFormat::Sha1,
root.path().join("worktree"),
InitKind::Worktree,
)
.unwrap();
let id = commit(&source);
let cancel = AtomicBool::new(false);
let objects = source.objects_with(PackLimits::default()).unwrap();
let update = PushCommand::new(RefName::new("refs/heads/main").unwrap(), None, id)
.with_force(ForcePolicy::FastForwardOnly);
let prepared =
PreparedPush::new(&objects, vec![update], &[], PushLimits::default(), &cancel).unwrap();
let refused = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(matches!(
refused.refs[0].status,
Some(girt::push::Status::Rejected(_))
));
std::fs::OpenOptions::new()
.append(true)
.open(dest.git_dir().join("config"))
.unwrap()
.write_all(b"\n[receive]\n denyCurrentBranch = ignore\n")
.unwrap();
let identity = Signature {
name: b"Pusher".to_vec(),
email: b"push@example.invalid".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let accepted = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared).with_identity(Some(&identity)),
TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(accepted.all_succeeded());
let deletion = {
let input = PushCommand::new(
RefName::new("refs/heads/main").unwrap(),
Some(id),
girt::ObjectId::null(ObjectFormat::Sha1),
);
input.with_force(ForcePolicy::FastForwardOnly)
};
let prepared = PreparedPush::new(
&objects,
vec![deletion],
&[],
PushLimits::default(),
&cancel,
)
.unwrap();
let refused = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(matches!(
refused.refs[0].status,
Some(girt::push::Status::Rejected(_))
));
std::fs::OpenOptions::new()
.append(true)
.open(dest.git_dir().join("config"))
.unwrap()
.write_all(b" denyDeleteCurrent = false\n")
.unwrap();
let accepted = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared).with_identity(Some(&identity)),
TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(accepted.all_succeeded());
assert!(
dest.references()
.unwrap()
.read(&RefName::new("refs/heads/main").unwrap())
.unwrap()
.is_none()
);
}
#[rstest]
#[case::sha1(ObjectFormat::Sha1)]
#[case::sha256(ObjectFormat::Sha256)]
fn native_push_uses_explicit_inherited_receive_config(#[case] format: ObjectFormat) {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", format, Backend::Files);
let dest = repository(&root, "dest", format, Backend::Files);
let id = commit(&source);
let name = RefName::new("refs/heads/topic").unwrap();
let cancel = AtomicBool::new(false);
let objects = source.objects_with(PackLimits::default()).unwrap();
let create = PreparedPush::new(
&objects,
vec![PushCommand::new(name.clone(), None, id).with_force(ForcePolicy::FastForwardOnly)],
&[],
PushLimits::default(),
&cancel,
)
.unwrap();
assert!(
send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&create),
girt::transport::TransportControl::new(&cancel),
|_| {}
)
.unwrap()
.all_succeeded()
);
let global_config = root.path().join("global-config");
std::fs::write(&global_config, b"[receive]\n denyDeletes = true\n").unwrap();
let config_inputs = ConfigInputs::default().with_files(vec![
ConfigFile::new(global_config, ConfigScope::Global).with_optional(false),
]);
let delete = PreparedPush::new(
&objects,
vec![
PushCommand::new(name.clone(), Some(id), girt::ObjectId::null(format))
.with_force(ForcePolicy::FastForwardOnly),
],
&[],
PushLimits::default(),
&cancel,
)
.unwrap();
let report = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&delete).with_config_inputs(&config_inputs),
TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(matches!(
report.refs[0].status,
Some(girt::push::Status::Rejected(_))
));
assert_eq!(
report.refs[0].rejection_origin,
Some(RejectionOrigin::Receiver)
);
assert_eq!(
dest.references().unwrap().read(&name).unwrap(),
Some(Target::Direct(id))
);
assert!(
send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&delete),
girt::transport::TransportControl::new(&cancel),
|_| {}
)
.unwrap()
.all_succeeded()
);
}
#[test]
fn native_hidden_ref_rejects_independently_of_visible_ref() {
use std::io::Write;
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha1, Backend::Files);
let dest = repository(&root, "dest", ObjectFormat::Sha1, Backend::Files);
let id = commit(&source);
std::fs::OpenOptions::new()
.append(true)
.open(dest.git_dir().join("config"))
.unwrap()
.write_all(b"\n[receive]\n hideRefs = refs/heads/private\n hideRefs = !refs/heads/private/allowed\n")
.unwrap();
let commands = ["refs/heads/private/blocked", "refs/heads/private/allowed"].map(|name| {
PushCommand::new(RefName::new(name).unwrap(), None, id)
.with_force(ForcePolicy::FastForwardOnly)
});
let cancel = AtomicBool::new(false);
let objects = source.objects_with(PackLimits::default()).unwrap();
let prepared = PreparedPush::new(
&objects,
commands.to_vec(),
&[],
PushLimits::default(),
&cancel,
)
.unwrap();
let report = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(matches!(
report.refs[0].status,
Some(girt::push::Status::Rejected(_))
));
assert_eq!(
report.refs[0].rejection_origin,
Some(RejectionOrigin::Receiver)
);
assert_eq!(report.refs[1].status, Some(girt::push::Status::Ok));
let refs = dest.references().unwrap();
assert_eq!(refs.read(&commands[0].name).unwrap(), None);
assert_eq!(
refs.read(&commands[1].name).unwrap(),
Some(Target::Direct(id))
);
}
#[rstest]
#[case::sha1_files(ObjectFormat::Sha1, Backend::Files)]
#[case::sha1_reftable(ObjectFormat::Sha1, Backend::Reftable)]
#[case::sha256_files(ObjectFormat::Sha256, Backend::Files)]
#[case::sha256_reftable(ObjectFormat::Sha256, Backend::Reftable)]
fn native_stale_expectation_is_distinct_from_receiver_rejection(
#[case] format: ObjectFormat,
#[case] backend: Backend,
) {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", format, backend);
let dest = repository(&root, "dest", format, backend);
let id = commit(&source);
let stale = RefName::new("refs/heads/stale").unwrap();
let accepted = RefName::new("refs/heads/accepted").unwrap();
let commands = vec![
PushCommand::new(stale.clone(), Some(id), id).with_force(ForcePolicy::FastForwardOnly),
PushCommand::new(accepted.clone(), None, id).with_force(ForcePolicy::FastForwardOnly),
];
let cancel = AtomicBool::new(false);
let prepared = PreparedPush::new(
&source.objects_with(PackLimits::default()).unwrap(),
commands,
&[],
PushLimits::default(),
&cancel,
)
.unwrap();
let report = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(matches!(
report.refs[0].status,
Some(girt::push::Status::Rejected(_))
));
assert_eq!(
report.refs[0].rejection_origin,
Some(RejectionOrigin::ExpectedValue)
);
assert_eq!(report.refs[1].status, Some(girt::push::Status::Ok));
assert_eq!(report.refs[1].rejection_origin, None);
let refs = dest.references().unwrap();
assert_eq!(refs.read(&stale).unwrap(), None);
assert_eq!(refs.read(&accepted).unwrap(), Some(Target::Direct(id)));
}
#[test]
fn native_warn_mode_reports_accepted_current_branch_update() {
use std::io::Write;
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha1, Backend::Files);
let dest = Repository::init(
ObjectFormat::Sha1,
root.path().join("worktree"),
InitKind::Worktree,
)
.unwrap();
let id = commit(&source);
std::fs::OpenOptions::new()
.append(true)
.open(dest.git_dir().join("config"))
.unwrap()
.write_all(b"\n[receive]\n denyCurrentBranch = warn\n")
.unwrap();
let cancel = AtomicBool::new(false);
let prepared = PreparedPush::new(
&source.objects_with(PackLimits::default()).unwrap(),
vec![
PushCommand::new(RefName::new("refs/heads/main").unwrap(), None, id)
.with_force(ForcePolicy::FastForwardOnly),
],
&[],
PushLimits::default(),
&cancel,
)
.unwrap();
let identity = Signature {
name: b"Pusher".to_vec(),
email: b"push@example.invalid".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let report = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared).with_identity(Some(&identity)),
TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert!(report.all_succeeded());
assert_eq!(report.warnings.len(), 1);
assert_eq!(
dest.references()
.unwrap()
.read(&RefName::new("refs/heads/main").unwrap())
.unwrap(),
Some(Target::Direct(id))
);
}
#[rstest]
#[case::deletes("denyDeletes")]
#[case::non_fast_forward("denyNonFastForwards")]
fn invalid_receive_boolean_refuses_before_native_installation(#[case] key: &str) {
use std::io::Write;
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha1, Backend::Files);
let dest = repository(&root, "dest", ObjectFormat::Sha1, Backend::Files);
let id = commit(&source);
std::fs::OpenOptions::new()
.append(true)
.open(dest.git_dir().join("config"))
.unwrap()
.write_all(format!("\n[receive]\n {key} = invalid\n").as_bytes())
.unwrap();
let cancel = AtomicBool::new(false);
let prepared = PreparedPush::new(
&source.objects_with(PackLimits::default()).unwrap(),
vec![
PushCommand::new(RefName::new("refs/heads/main").unwrap(), None, id)
.with_force(ForcePolicy::FastForwardOnly),
],
&[],
PushLimits::default(),
&cancel,
)
.unwrap();
assert!(matches!(
send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&cancel),
|_| {}
),
Err(PushError::NotSent(PushFailure::Unsupported(_)))
));
assert!(
dest.objects_with(PackLimits::default())
.unwrap()
.read_with(id, Default::default())
.unwrap()
.is_none()
);
}
#[rstest]
#[case::sha1_files(ObjectFormat::Sha1, Backend::Files)]
#[case::sha1_reftable(ObjectFormat::Sha1, Backend::Reftable)]
#[case::sha256_files(ObjectFormat::Sha256, Backend::Files)]
#[case::sha256_reftable(ObjectFormat::Sha256, Backend::Reftable)]
fn native_fetch_and_push_preserve_git_objects_and_refs(
#[case] format: ObjectFormat,
#[case] backend: Backend,
) {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", format, backend);
let fetched = repository(&root, "fetched", format, backend);
let pushed = repository(&root, "pushed", format, backend);
let id = commit(&source);
let received = receive_local(
source.git_dir(),
|_| vec![id],
girt::fetch::LocalFetchRequest::new(FetchLimits::default()),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
girt::fetch::LocalFetchObserver::default(),
)
.unwrap();
assert_eq!(received.wants(), &[id]);
received
.install(&fetched, PackLimits::default(), &AtomicBool::new(false))
.unwrap();
assert_eq!(git_cat_file(&fetched, id), git_cat_file(&source, id));
let command = PushCommand::new(RefName::new("refs/heads/main").unwrap(), None, id)
.with_force(ForcePolicy::FastForwardOnly);
let objects = source.objects_with(PackLimits::default()).unwrap();
let prepared = PreparedPush::new(
&objects,
vec![command],
&[],
PushLimits::default(),
&AtomicBool::new(false),
)
.unwrap();
let report = send_local(
pushed.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
|_| {},
)
.unwrap();
assert!(report.all_succeeded());
assert_eq!(
pushed
.references()
.unwrap()
.resolve_with(&RefName::new("refs/heads/main").unwrap(), 8)
.unwrap()
.id,
Some(id)
);
assert_eq!(git_cat_file(&pushed, id), git_cat_file(&source, id));
}
#[rstest]
#[case::sha1_files(ObjectFormat::Sha1, Backend::Files)]
#[case::sha1_reftable(ObjectFormat::Sha1, Backend::Reftable)]
#[case::sha256_files(ObjectFormat::Sha256, Backend::Files)]
#[case::sha256_reftable(ObjectFormat::Sha256, Backend::Reftable)]
fn native_push_deletes_one_ref_and_creates_another(
#[case] format: ObjectFormat,
#[case] backend: Backend,
) {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", format, backend);
let dest = repository(&root, "dest", format, backend);
let id = commit(&source);
let objects = source.objects_with(PackLimits::default()).unwrap();
let cancel = AtomicBool::new(false);
let old = PushCommand::new(RefName::new("refs/for/main").unwrap(), None, id)
.with_force(ForcePolicy::FastForwardOnly);
let prepared =
PreparedPush::new(&objects, vec![old], &[], PushLimits::default(), &cancel).unwrap();
assert!(
send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&cancel),
|_| {}
)
.unwrap()
.all_succeeded()
);
let commands = vec![
PushCommand::new(
RefName::new("refs/for/main").unwrap(),
Some(id),
girt::ObjectId::null(format),
)
.with_force(ForcePolicy::FastForwardOnly),
PushCommand::new(RefName::new("refs/tags/main").unwrap(), None, id)
.with_force(ForcePolicy::FastForwardOnly),
];
let prepared =
PreparedPush::new(&objects, commands, &[], PushLimits::default(), &cancel).unwrap();
assert!(
send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&cancel),
|_| {}
)
.unwrap()
.all_succeeded()
);
let refs = dest.references().unwrap();
assert_eq!(
refs.read(&RefName::new("refs/for/main").unwrap()).unwrap(),
None
);
assert_eq!(
refs.read(&RefName::new("refs/tags/main").unwrap()).unwrap(),
Some(Target::Direct(id))
);
}
#[test]
fn native_fetch_rejects_missing_source_object_without_installation() {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha1, Backend::Files);
let id = girt::ObjectId::for_blob(ObjectFormat::Sha1, b"missing");
source
.references()
.unwrap()
.update_without_reflog(
&RefName::new("refs/heads/main").unwrap(),
Target::Direct(id),
Expected::Absent,
)
.unwrap();
let result = receive_local(
source.git_dir(),
|_| vec![id],
girt::fetch::LocalFetchRequest::new(FetchLimits::default()),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
girt::fetch::LocalFetchObserver::default(),
);
assert!(matches!(result, Err(FetchError::Missing(missing)) if missing == id));
}
fn commit_tree(repository: &Repository, entries: Vec<girt::TreeEntry>) -> girt::ObjectId {
let store = repository.loose_objects();
let tree = Tree::new(repository.object_format(), entries).unwrap();
store.write_tree(&tree).unwrap();
let person = Signature {
name: b"Local Test".to_vec(),
email: b"local@example.invalid".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let id = store
.write_commit(
&Commit::new(CommitFields {
tree: tree.id(),
parents: vec![],
author: person.clone(),
committer: person,
extra_headers: vec![],
message: b"Repeated entries\n".to_vec(),
})
.unwrap(),
)
.unwrap();
repository
.references()
.unwrap()
.update_without_reflog(
&RefName::new("refs/heads/main").unwrap(),
Target::Direct(id),
Expected::Any,
)
.unwrap();
id
}
#[test]
fn native_fetch_checks_every_kind_of_a_repeated_entry() {
use girt::EntryMode;
let entry = |mode, name: &str, id| girt::TreeEntry {
mode,
name: name.as_bytes().to_vec(),
id,
};
let fetch = |source: &Repository, tip| {
receive_local(
source.git_dir(),
|_| vec![tip],
girt::fetch::LocalFetchRequest::new(FetchLimits::default()),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
girt::fetch::LocalFetchObserver::default(),
)
};
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha1, Backend::Files);
let store = source.loose_objects();
let blob = store.write_blob(b"shared\n").unwrap();
let nested = Tree::new(ObjectFormat::Sha1, vec![entry(EntryMode::Blob, "x", blob)]).unwrap();
store.write_tree(&nested).unwrap();
let tip = commit_tree(
&source,
vec![
entry(EntryMode::Blob, "a", blob),
entry(EntryMode::Executable, "b", blob),
entry(EntryMode::Tree, "c", nested.id()),
entry(EntryMode::Tree, "d", nested.id()),
],
);
let received = fetch(&source, tip).unwrap();
assert_eq!(received.wants(), [tip]);
let tip = commit_tree(
&source,
vec![
entry(EntryMode::Blob, "a", blob),
entry(EntryMode::Tree, "b", blob),
],
);
assert!(matches!(fetch(&source, tip), Err(FetchError::Kind(id)) if id == blob));
}
#[rstest]
#[case::sha1(ObjectFormat::Sha1)]
#[case::sha256(ObjectFormat::Sha256)]
fn native_fetch_materializes_absent_canonical_empty_tree(#[case] format: ObjectFormat) {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", format, Backend::Files);
let destination = repository(&root, "destination", format, Backend::Files);
let id = commit(&source);
let empty_tree = Tree::new(format, vec![]).unwrap().id();
let hex = empty_tree.to_string();
std::fs::remove_file(source.object_dir().join(&hex[..2]).join(&hex[2..])).unwrap();
let cancel = AtomicBool::new(false);
let received = receive_local(
source.git_dir(),
|_| vec![id],
girt::fetch::LocalFetchRequest::new(FetchLimits::default()),
girt::transport::TransportControl::new(&cancel),
girt::fetch::LocalFetchObserver::default(),
)
.unwrap();
received
.install(&destination, PackLimits::default(), &cancel)
.unwrap();
let tree = destination
.objects_with(PackLimits::default())
.unwrap()
.read_with(empty_tree, Default::default())
.unwrap()
.unwrap();
assert_eq!(tree.kind(), girt::ObjectKind::Tree);
assert!(tree.data().is_empty());
}
#[test]
fn resolved_file_url_fetches_from_local_storage() {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source path", ObjectFormat::Sha1, Backend::Files);
let id = commit(&source);
let path = plain_local_path(source.git_dir())
.replace('\\', "/")
.replace(' ', "%20");
let url = if cfg!(windows) {
format!("file:///{path}")
} else {
format!("file://{path}")
};
let config = Config::parse(format!("[remote \"origin\"]\nurl = {url}\n").as_bytes()).unwrap();
let remote = girt::remote::Remote::find(&config, b"origin")
.unwrap()
.unwrap();
let destination = remote
.fetch_destination(&config, &Default::default())
.unwrap();
let received = receive_local(
destination.local_path().unwrap(),
|_| vec![id],
girt::fetch::LocalFetchRequest::new(FetchLimits::default()),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
girt::fetch::LocalFetchObserver::default(),
)
.unwrap();
assert_eq!(received.wants(), &[id]);
}
#[test]
fn native_fetch_rejects_corrupt_source_object() {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha1, Backend::Files);
let id = commit(&source);
let hex = id.to_string();
let path = source.object_dir().join(&hex[..2]).join(&hex[2..]);
std::fs::write(path, b"corrupt loose object").unwrap();
let result = receive_local(
source.git_dir(),
|_| vec![id],
girt::fetch::LocalFetchRequest::new(FetchLimits::default()),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
girt::fetch::LocalFetchObserver::default(),
);
assert!(matches!(result, Err(FetchError::LocalRead { id: failed, .. }) if failed == id));
}
#[test]
fn native_fetch_callback_interruption_returns_no_pack() {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha256, Backend::Reftable);
let id = commit(&source);
let result = receive_local(
source.git_dir(),
|_| vec![id],
girt::fetch::LocalFetchRequest::new(FetchLimits::default()),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
girt::fetch::LocalFetchObserver::default().with_messages(&mut |_| ControlFlow::Break(())),
);
assert!(matches!(result, Err(FetchError::Cancelled)));
}
#[rstest]
#[case::sha1(ObjectFormat::Sha1)]
#[case::sha256(ObjectFormat::Sha256)]
fn native_push_accepts_history_reaching_unstored_empty_tree(#[case] format: ObjectFormat) {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", format, Backend::Files);
let dest = repository(&root, "dest", format, Backend::Files);
let empty_tree = Tree::new(format, vec![]).unwrap().id();
let person = Signature {
name: b"Local Test".to_vec(),
email: b"local@example.invalid".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let write = |repository: &Repository, parents, message: &[u8]| {
let commit = Commit::new(CommitFields {
tree: empty_tree,
parents,
author: person.clone(),
committer: person.clone(),
extra_headers: vec![],
message: message.to_vec(),
})
.unwrap();
repository.loose_objects().write_commit(&commit).unwrap()
};
let main = RefName::new("refs/heads/main").unwrap();
let mut parent = None;
let mut child = None;
for repository in [&source, &dest] {
let first = write(repository, vec![], b"Parent\n");
let second = write(repository, vec![first], b"Child\n");
repository
.references()
.unwrap()
.update_without_reflog(&main, Target::Direct(second), Expected::Absent)
.unwrap();
(parent, child) = (Some(first), Some(second));
}
let (parent, child) = (parent.unwrap(), child.unwrap());
let copy = RefName::new("refs/heads/copy").unwrap();
let commands = vec![
PushCommand::new(main.clone(), None, parent).with_force(ForcePolicy::FastForwardOnly),
PushCommand::new(copy.clone(), None, parent).with_force(ForcePolicy::FastForwardOnly),
];
let cancel = AtomicBool::new(false);
let prepared = PreparedPush::new(
&source.objects_with(PackLimits::default()).unwrap(),
commands,
&[child],
PushLimits::default(),
&cancel,
)
.unwrap();
let report = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&cancel),
|_| {},
)
.unwrap();
assert_eq!(
report.refs[0].rejection_origin,
Some(RejectionOrigin::ExpectedValue)
);
assert_eq!(report.refs[1].status, Some(girt::push::Status::Ok));
let refs = dest.references().unwrap();
assert_eq!(refs.read(&main).unwrap(), Some(Target::Direct(child)));
assert_eq!(refs.read(©).unwrap(), Some(Target::Direct(parent)));
let hex = empty_tree.to_string();
assert!(dest.object_dir().join(&hex[..2]).join(&hex[2..]).exists());
let output = Command::new("git")
.arg("--git-dir")
.arg(plain_local_path(dest.git_dir()))
.args(["fsck", "--connectivity-only"])
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
#[test]
fn receive_hooks_on_unsupported_platform_refuse_before_mutation() {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha1, Backend::Files);
let destination = repository(&root, "destination", ObjectFormat::Sha1, Backend::Files);
let id = commit(&source);
let path = destination.git_dir().join("hooks/pre-receive");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, b"hook must not execute").unwrap();
let prepared = PreparedPush::new(
&source.objects_with(PackLimits::default()).unwrap(),
vec![
PushCommand::new(RefName::new(b"refs/heads/main").unwrap(), None, id)
.with_force(ForcePolicy::FastForwardOnly),
],
&[],
PushLimits::default(),
&AtomicBool::new(false),
)
.unwrap();
let error = send_local(
destination.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
|_| {},
)
.unwrap_err();
assert!(matches!(
error,
PushError::NotSent(PushFailure::Unsupported(
"receive hook execution on this platform"
))
));
assert!(
destination
.objects_with(PackLimits::default())
.unwrap()
.read_with(id, Default::default())
.unwrap()
.is_none()
);
assert!(
destination
.references()
.unwrap()
.read(&RefName::new(b"refs/heads/main").unwrap())
.unwrap()
.is_none()
);
}
#[cfg(unix)]
fn git_unstored_empty_tree_commit(
repository: &Repository,
tree: girt::ObjectId,
parent: Option<girt::ObjectId>,
) -> girt::ObjectId {
let mut command = Command::new("git");
for (key, _) in std::env::vars_os() {
if key.to_string_lossy().starts_with("GIT_") {
command.env_remove(key);
}
}
command
.arg("--git-dir")
.arg(repository.git_dir())
.args([
"-c",
"commit.gpgsign=false",
"commit-tree",
&tree.to_string(),
])
.env("GIT_CONFIG_NOSYSTEM", "1")
.env(
"GIT_CONFIG_GLOBAL",
repository.git_dir().join("absent-config"),
)
.env("GIT_AUTHOR_NAME", "Local Test")
.env("GIT_AUTHOR_EMAIL", "local@example.invalid")
.env("GIT_COMMITTER_NAME", "Local Test")
.env("GIT_COMMITTER_EMAIL", "local@example.invalid")
.env("GIT_AUTHOR_DATE", "@1700000000 +0000")
.env("GIT_COMMITTER_DATE", "@1700000000 +0000")
.stdin(std::process::Stdio::null());
if let Some(parent) = parent {
command.args(["-p", &parent.to_string()]);
}
let output = command.output().unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
std::str::from_utf8(&output.stdout)
.unwrap()
.trim()
.parse()
.unwrap()
}
#[cfg(unix)]
#[rstest]
#[case::sha1(ObjectFormat::Sha1)]
#[case::sha256(ObjectFormat::Sha256)]
fn pre_receive_rejection_does_not_materialize_unstored_empty_tree(#[case] format: ObjectFormat) {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", format, Backend::Files);
let dest = repository(&root, "destination", format, Backend::Files);
let tree = Tree::new(format, vec![]).unwrap().id();
let parent = git_unstored_empty_tree_commit(&source, tree, None);
let child = git_unstored_empty_tree_commit(&source, tree, Some(parent));
let parent_commit = Commit::parse(format, &git_cat_file(&source, parent)).unwrap();
let child_commit = Commit::parse(format, &git_cat_file(&source, child)).unwrap();
assert_eq!(
dest.loose_objects().write_commit(&parent_commit).unwrap(),
parent
);
assert_eq!(
dest.loose_objects().write_commit(&child_commit).unwrap(),
child
);
dest.references()
.unwrap()
.update_without_reflog(
&RefName::new(b"refs/heads/main").unwrap(),
Target::Direct(child),
Expected::Absent,
)
.unwrap();
let hex = tree.to_string();
let relative_tree = format!("{}/{}", &hex[..2], &hex[2..]);
assert!(!dest.object_dir().join(&relative_tree).exists());
let path = dest.git_dir().join("hooks/pre-receive");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, format!("#!/bin/sh\ntest -f \"$GIT_OBJECT_DIRECTORY/{relative_tree}\" || exit 9\ngit cat-file -e {tree} || exit 9\nprintf visible >hook-observed\ncat >/dev/null\nexit 1\n")).unwrap();
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
let prepared = PreparedPush::new(
&source.objects_with(PackLimits::default()).unwrap(),
vec![
PushCommand::new(RefName::new(b"refs/heads/copy").unwrap(), None, parent)
.with_force(ForcePolicy::FastForwardOnly),
],
&[child],
PushLimits::default(),
&AtomicBool::new(false),
)
.unwrap();
let report = send_local(
dest.git_dir(),
girt::push::LocalPushRequest::new(&prepared),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
|_| {},
)
.unwrap();
assert_eq!(
report.refs[0].status,
Some(girt::push::Status::Rejected(
b"pre-receive hook declined".to_vec()
))
);
assert_eq!(
std::fs::read(dest.git_dir().join("hook-observed")).unwrap(),
b"visible"
);
assert!(!dest.object_dir().join(relative_tree).exists());
assert!(
dest.references()
.unwrap()
.read(&RefName::new(b"refs/heads/copy").unwrap())
.unwrap()
.is_none()
);
assert_eq!(
dest.references()
.unwrap()
.read(&RefName::new(b"refs/heads/main").unwrap())
.unwrap(),
Some(Target::Direct(child))
);
}
fn git_in(directory: &std::path::Path, args: &[&str]) -> String {
let output = Command::new("git")
.current_dir(directory)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", directory.join("absent-config"))
.args([
"-c",
"user.name=Local Test",
"-c",
"user.email=local@example.invalid",
])
.args(args)
.output()
.unwrap();
assert!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap().trim().to_owned()
}
fn packed_git_source(root: &tempfile::TempDir) -> (std::path::PathBuf, girt::ObjectId, usize) {
let path = root.path().join("source");
std::fs::create_dir(&path).unwrap();
git_in(&path, &["init", "-q", "--template=", "-b", "main"]);
std::fs::write(path.join("file"), b"packed source\n").unwrap();
git_in(&path, &["add", "file"]);
git_in(&path, &["commit", "-q", "-m", "packed"]);
git_in(&path, &["repack", "-adq"]);
let tip = git_in(&path, &["rev-parse", "HEAD"]).parse().unwrap();
let pack_dir = path.join(".git/objects/pack");
let index = std::fs::read_dir(&pack_dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.find(|path| path.extension().is_some_and(|ext| ext == "idx"))
.unwrap();
let index_len = std::fs::metadata(index).unwrap().len().try_into().unwrap();
(path, tip, index_len)
}
#[rstest]
#[case::index_over_limit(1, false)]
#[case::index_at_limit(0, true)]
fn native_fetch_opens_source_within_source_pack_limits(
#[case] below_index: usize,
#[case] fetched: bool,
) {
let root = tempfile::tempdir().unwrap();
let (source, tip, index_len) = packed_git_source(&root);
let limits = FetchLimits::default()
.with_source_packs(PackLimits::default().with_max_index_bytes(index_len - below_index));
let received = receive_local(
&source,
|_| vec![tip],
girt::fetch::LocalFetchRequest::new(limits),
TransportControl::new(&AtomicBool::new(false)),
girt::fetch::LocalFetchObserver::default(),
);
let discovered = discover_local(
&source,
limits,
TransportControl::new(&AtomicBool::new(false)),
);
assert_eq!(
received.as_ref().err().map(ToString::to_string),
(!fetched).then(|| "opening source object store".to_owned())
);
assert_eq!(
matches!(received, Err(FetchError::SourceObjects(_))),
!fetched
);
assert_eq!(
matches!(discovered, Err(FetchError::SourceObjects(_))),
!fetched
);
assert_eq!(discovered.is_ok(), fetched);
let destination = repository(&root, "destination", ObjectFormat::Sha1, Backend::Files);
let installed = received.map(|received| {
received
.install(&destination, PackLimits::default(), &AtomicBool::new(false))
.unwrap();
git_in(
destination.git_dir(),
&["cat-file", "-p", &format!("{tip}^{{tree}}:file")],
)
});
assert_eq!(installed.ok(), fetched.then(|| "packed source".to_owned()));
}
#[rstest]
#[case::index_over_limit(Some(1), None)]
#[case::default_limits(None, Some(()))]
fn native_push_opens_destination_within_destination_pack_limits(
#[case] below_index: Option<usize>,
#[case] pushed: Option<()>,
) {
let root = tempfile::tempdir().unwrap();
let (destination, _, index_len) = packed_git_source(&root);
let source = repository(&root, "pusher", ObjectFormat::Sha1, Backend::Files);
let id = commit(&source);
let packs = below_index.map_or_else(PackLimits::default, |below| {
PackLimits::default().with_max_index_bytes(index_len - below)
});
let command = PushCommand::new(RefName::new("refs/heads/pushed").unwrap(), None, id);
let identity = Signature {
name: b"Local Test".to_vec(),
email: b"local@example.invalid".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
let cancel = AtomicBool::new(false);
let objects = source.objects_with(PackLimits::default()).unwrap();
let prepared = PreparedPush::new(
&objects,
vec![command],
&[],
PushLimits::default().with_destination_packs(packs),
&cancel,
)
.unwrap();
let result = send_local(
&destination,
girt::push::LocalPushRequest::new(&prepared).with_identity(Some(&identity)),
TransportControl::new(&cancel),
|_| {},
);
assert_eq!(
result.as_ref().map(|report| report.all_succeeded()).ok(),
pushed.map(|()| true)
);
let limit_cause = match &result {
Err(PushError::NotSent(PushFailure::Install(error))) => match error.as_ref() {
FetchError::Destination(cause) => Some(cause.to_string()),
_ => None,
},
_ => None,
};
assert_eq!(
limit_cause,
pushed
.is_none()
.then(|| "object storage limit exceeded: pack index bytes".to_owned())
);
let observed = git_in(
&destination,
&[
"for-each-ref",
"--format=%(objectname)",
"refs/heads/pushed",
],
);
assert_eq!(
(!observed.is_empty()).then(|| observed.parse::<girt::ObjectId>().unwrap()),
pushed.map(|()| id)
);
git_in(&destination, &["fsck", "--strict"]);
}
#[rstest]
#[case(ObjectFormat::Sha1)]
#[case(ObjectFormat::Sha256)]
fn configured_clone_no_checkout_is_portable_and_checkout_preserves_platform_scope(
#[case] format: ObjectFormat,
) {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", format, Backend::Files);
let id = commit(&source);
let url = source.git_dir().to_str().unwrap();
let environment = girt::workflow::Environment::default();
let inputs = ConfigInputs::default();
let options = girt::workflow::CloneOptions::default().with_checkout(false);
let outcome = girt::workflow::clone_with(
url,
root.path().join("no-checkout"),
&options,
&inputs,
&environment,
&mut girt::workflow::NoCallbacks,
)
.unwrap();
let cloned = outcome.clone.repository.unwrap();
assert_eq!(cloned.head().unwrap().id, Some(id));
assert!(!cloned.git_dir().join("index").exists());
let configured = girt::remote::Remote::find(cloned.config(), b"origin")
.unwrap()
.unwrap();
girt::workflow::fetch(
&cloned,
"origin",
&girt::workflow::FetchOptions::new(
configured
.fetch_refspecs()
.specs()
.iter()
.map(|spec| spec.as_bytes()),
),
&environment,
&mut girt::workflow::NoCallbacks,
)
.unwrap();
let checked_out = girt::workflow::clone_with(
url,
root.path().join("checkout"),
&Default::default(),
&inputs,
&environment,
&mut girt::workflow::NoCallbacks,
);
if cfg!(any(target_os = "linux", target_os = "macos")) {
assert!(checked_out.unwrap().checkout.unwrap().index_published);
} else {
let girt::workflow::CloneError::UnsupportedCheckout = checked_out.unwrap_err() else {
panic!("expected unsupported checkout");
};
assert!(!root.path().join("checkout").exists());
}
}
fn reflog_names(git_dir: &std::path::Path) -> std::collections::BTreeSet<String> {
fn walk(
root: &std::path::Path,
directory: &std::path::Path,
names: &mut std::collections::BTreeSet<String>,
) {
let Ok(entries) = std::fs::read_dir(directory) else {
return;
};
for entry in entries {
let path = entry.unwrap().path();
if path.is_dir() {
walk(root, &path, names);
} else {
let name = path.strip_prefix(root).unwrap().to_string_lossy();
names.insert(name.replace('\\', "/"));
}
}
}
let mut names = std::collections::BTreeSet::new();
let root = git_dir.join("logs");
walk(&root, &root, &mut names);
names
}
#[rstest]
#[case::default(None, false)]
#[case::always(Some("always"), false)]
#[case::disabled(Some("false"), false)]
#[case::existing_tag_log(None, true)]
fn workflow_fetch_reflogs_follow_core_log_all_ref_updates_like_git(
#[case] setting: Option<&str>,
#[case] existing_tag_log: bool,
) {
let root = tempfile::tempdir().unwrap();
let source = repository(&root, "source", ObjectFormat::Sha1, Backend::Files);
commit(&source);
git_in(source.git_dir(), &["tag", "v1", "main"]);
let url = plain_local_path(source.git_dir());
let refspecs = [
"+refs/heads/*:refs/remotes/origin/*",
"+refs/tags/*:refs/tags/*",
"+refs/tags/*:refs/remote-tags/origin/*",
];
let prepare = |name: &str| {
let path = root.path().join(name);
git_in(root.path(), &["init", "-q", name]);
git_in(&path, &["remote", "add", "origin", &url]);
if let Some(setting) = setting {
git_in(&path, &["config", "core.logAllRefUpdates", setting]);
}
if existing_tag_log {
let log = path.join(".git/logs/refs/tags/v1");
std::fs::create_dir_all(log.parent().unwrap()).unwrap();
std::fs::write(log, b"").unwrap();
}
path
};
let with_git = prepare("git");
let mut fetch = vec!["fetch", "-q", "--no-auto-maintenance", "origin"];
fetch.extend(refspecs);
git_in(&with_git, &fetch);
let with_girt = Repository::open(prepare("girt")).unwrap();
let person = Signature {
name: b"Local Test".to_vec(),
email: b"local@example.invalid".to_vec(),
seconds: 1_700_000_000,
offset_minutes: 0,
};
girt::workflow::fetch(
&with_girt,
"origin",
&girt::workflow::FetchOptions::new(refspecs)
.with_tag_namespace(RefName::new("refs/remote-tags/origin").unwrap())
.with_reflog(person, "fetch origin"),
&girt::workflow::Environment::default(),
&mut girt::workflow::NoCallbacks,
)
.unwrap();
let expected = reflog_names(&with_git.join(".git"));
assert!(expected.contains("refs/remotes/origin/main") == (setting != Some("false")));
assert_eq!(reflog_names(with_girt.git_dir()), expected);
}
fn git(dir: &std::path::Path, args: &[&str]) -> String {
let output = Command::new("git")
.arg("-C")
.arg(plain_local_path(dir))
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.args(["-c", "user.name=T", "-c", "user.email=t@example.invalid"])
.args(args)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
fn object_ids(dir: &std::path::Path) -> std::collections::BTreeSet<String> {
git(
dir,
&[
"cat-file",
"--batch-all-objects",
"--batch-check=%(objectname)",
],
)
.lines()
.map(str::to_owned)
.collect()
}
#[rstest]
#[case::sha1_bitmap(ObjectFormat::Sha1, true, false)]
#[case::sha1_walk(ObjectFormat::Sha1, false, false)]
#[case::sha1_bitmap_stored(ObjectFormat::Sha1, true, true)]
#[case::sha256_bitmap(ObjectFormat::Sha256, true, false)]
fn native_fetch_with_source_bitmap_receives_every_reachable_object(
#[case] format: ObjectFormat,
#[case] bitmap: bool,
#[case] stored: bool,
) {
let root = tempfile::tempdir().unwrap();
let work = root.path().join("work");
let format_name = match format {
ObjectFormat::Sha1 => "sha1",
_ => "sha256",
};
std::fs::create_dir(&work).unwrap();
git(
&work,
&["init", "-q", "-b", "main", "--object-format", format_name],
);
for round in 0..4 {
std::fs::create_dir_all(work.join("dir")).unwrap();
std::fs::write(work.join("shared"), b"shared\n").unwrap();
std::fs::write(work.join("dir/file"), format!("round {round}\n")).unwrap();
git(&work, &["add", "-A"]);
git(&work, &["commit", "-q", "-m", &format!("round {round}")]);
if round == 1 {
git(&work, &["tag", "-a", "-m", "tag", "v1"]);
git(&work, &["branch", "old"]);
}
}
git(&work, &["repack", "-adq", "--write-bitmap-index"]);
let source = work.join(".git");
let packs = source.join("objects/pack");
let has_bitmap = std::fs::read_dir(&packs)
.unwrap()
.any(|entry| entry.unwrap().path().extension() == Some("bitmap".as_ref()));
assert!(has_bitmap);
if !bitmap {
for entry in std::fs::read_dir(&packs).unwrap() {
let path = entry.unwrap().path();
if path.extension() == Some("bitmap".as_ref()) {
std::fs::remove_file(path).unwrap();
}
}
}
let destination = repository(&root, "destination", format, Backend::Files);
if stored {
destination.loose_objects().write_blob(b"shared\n").unwrap();
}
let known = girt::fetch::KnownHistory::from_store(
destination.objects().unwrap(),
&[],
FetchLimits::default(),
&AtomicBool::new(false),
)
.unwrap();
let received = receive_local(
&source,
|advertisement| {
advertisement
.refs
.iter()
.map(|reference| reference.id)
.collect()
},
girt::fetch::LocalFetchRequest::new(FetchLimits::default()).with_known(&known),
girt::transport::TransportControl::new(&AtomicBool::new(false)),
girt::fetch::LocalFetchObserver::default(),
)
.unwrap();
received
.install(&destination, PackLimits::default(), &AtomicBool::new(false))
.unwrap();
assert_eq!(object_ids(destination.git_dir()), object_ids(&source));
}