mod common;
use gix_hash::Kind;
use gunnar_wire::receive_pack::AllowAll;
use gunnar_wire::{ReceivePack, RefStatus};
use common::{empty_pack, push_bytes, validating_ingest, write_object, Fixture};
fn tag_object(target: &gix_hash::oid, tag: &str, kind: gix_object::Kind) -> Vec<u8> {
format!(
"object {}\ntype {kind}\ntag {tag}\n\
tagger Gunnar Test <gunnar@example.invalid> 1700000006 +0000\n\nrelease\n",
target.to_hex()
)
.into_bytes()
}
fn push(fixture: &Fixture, commands: &[(&str, &str, &str)], pack: &[u8]) -> Vec<RefStatus> {
let receive = ReceivePack::new(&fixture.git, fixture.kind);
let input = push_bytes(commands, "report-status object-format=sha1", pack);
let mut input = std::io::Cursor::new(input);
let mut out = Vec::new();
receive
.serve(
&mut input,
&mut out,
&validating_ingest(Kind::Sha1),
&AllowAll,
)
.expect("the session itself completes")
.expect("a push outcome")
.statuses
}
fn rejection(status: &RefStatus) -> &str {
match status {
RefStatus::Rejected(reason) => reason,
RefStatus::Ok => panic!("expected a refusal, got `ok`"),
}
}
#[test]
fn a_force_rewind_is_served_while_an_annotated_tag_names_a_newer_commit() {
let fixture = Fixture::build(Kind::Sha1);
let statuses = push(
&fixture,
&[(
&fixture.four.to_string(),
&fixture.two.to_string(),
"refs/heads/main",
)],
&empty_pack(Kind::Sha1),
);
assert_eq!(
statuses[0],
RefStatus::Ok,
"a rewind onto an object the repository already holds was refused: {:?}",
statuses[0]
);
assert_eq!(
fixture.read_ref("refs/heads/main"),
Some(fixture.two),
"the push reported ok and the reference did not move"
);
}
#[test]
fn a_force_update_of_an_annotated_tag_is_served() {
let fixture = Fixture::build(Kind::Sha1);
let moved = write_object(
&fixture.git,
Kind::Sha1,
gix_object::Kind::Tag,
&tag_object(&fixture.four, "v1", gix_object::Kind::Commit),
);
let statuses = push(
&fixture,
&[(&fixture.tag.to_string(), &moved.to_string(), "refs/tags/v1")],
&empty_pack(Kind::Sha1),
);
assert_eq!(
statuses[0],
RefStatus::Ok,
"moving a tag onto a tag object whose target the repository holds was refused: {:?}",
statuses[0]
);
assert_eq!(fixture.read_ref("refs/tags/v1"), Some(moved));
}
#[test]
fn a_tag_chain_in_the_frontier_peels_all_the_way_to_its_commit() {
let fixture = Fixture::build(Kind::Sha1);
let inner = write_object(
&fixture.git,
Kind::Sha1,
gix_object::Kind::Tag,
&tag_object(&fixture.four, "inner", gix_object::Kind::Commit),
);
let outer = write_object(
&fixture.git,
Kind::Sha1,
gix_object::Kind::Tag,
&tag_object(&inner, "outer", gix_object::Kind::Tag),
);
fixture.force_ref("refs/tags/chained", outer);
let statuses = push(
&fixture,
&[(
&Kind::Sha1.null().to_string(),
&fixture.four.to_string(),
"refs/heads/onto-chain",
)],
&empty_pack(Kind::Sha1),
);
assert_eq!(
statuses[0],
RefStatus::Ok,
"a two-deep tag chain in the frontier refused an unrelated push: {:?}",
statuses[0]
);
}
#[test]
fn a_tag_whose_target_is_absent_does_not_refuse_an_unrelated_push() {
let fixture = Fixture::build(Kind::Sha1);
let absent =
gix_object::compute_hash(Kind::Sha1, gix_object::Kind::Blob, b"never stored").unwrap();
let dangling = write_object(
&fixture.git,
Kind::Sha1,
gix_object::Kind::Tag,
&tag_object(&absent, "ghost", gix_object::Kind::Commit),
);
fixture.force_ref("refs/tags/ghost", dangling);
let statuses = push(
&fixture,
&[(
&fixture.four.to_string(),
&fixture.two.to_string(),
"refs/heads/main",
)],
&empty_pack(Kind::Sha1),
);
assert_eq!(
statuses[0],
RefStatus::Ok,
"one dangling tag elsewhere in the repository refused a push that has nothing to do \
with it: {:?}",
statuses[0]
);
}
#[test]
fn an_absent_tip_is_refused_with_a_reason_that_does_not_invent_a_tag() {
let fixture = Fixture::build(Kind::Sha1);
let absent =
gix_object::compute_hash(Kind::Sha1, gix_object::Kind::Commit, b"never stored").unwrap();
let statuses = push(
&fixture,
&[(
&Kind::Sha1.null().to_string(),
&absent.to_string(),
"refs/heads/nowhere",
)],
&empty_pack(Kind::Sha1),
);
let reason = rejection(&statuses[0]);
assert!(
reason.contains(&absent.to_string()),
"the refusal does not say which object is missing: {reason}"
);
assert!(
!reason.contains("peeling a tag"),
"no tag was pushed, named, or stored, and the refusal blames tag peeling: {reason}"
);
assert!(
fixture.read_ref("refs/heads/nowhere").is_none(),
"the reference was created for an object the repository does not have"
);
}
#[test]
fn a_tag_whose_target_was_not_pushed_is_refused_as_a_tag() {
let fixture = Fixture::build(Kind::Sha1);
let absent =
gix_object::compute_hash(Kind::Sha1, gix_object::Kind::Commit, b"never stored").unwrap();
let orphan = write_object(
&fixture.git,
Kind::Sha1,
gix_object::Kind::Tag,
&tag_object(&absent, "orphan", gix_object::Kind::Commit),
);
let statuses = push(
&fixture,
&[(
&Kind::Sha1.null().to_string(),
&orphan.to_string(),
"refs/tags/orphan",
)],
&empty_pack(Kind::Sha1),
);
let reason = rejection(&statuses[0]);
assert!(
reason.contains("tag"),
"a tag was pushed without the commit it names and the refusal does not mention the \
tag: {reason}"
);
assert!(
reason.contains(&absent.to_string()),
"the refusal does not say which object the tag names: {reason}"
);
}