diff --git a/gix/src/revision/spec/parse/delegate/revision.rs b/gix/src/revision/spec/parse/delegate/revision.rs index 3df1c539c66..d603d9a9652 100644 --- a/gix/src/revision/spec/parse/delegate/revision.rs +++ b/gix/src/revision/spec/parse/delegate/revision.rs @@ -207,7 +207,19 @@ impl delegate::Revision for Delegate<'_> { self.refs[self.idx] = Some(r.detach()); id } - Err(_) => id, + Err(crate::reference::find::existing::Error::NotFound { .. }) => { + match ObjectId::from_hex(ref_name.as_ref()) { + Ok(id) if id.kind() == self.repo.object_hash() => id, + _ => { + return Err(message!( + "Previous checkout '{name}' does not resolve to an existing revision", + name = ref_name.as_bstr() + ) + .raise_erased()); + } + } + } + Err(err) => return Err(err.raise_erased()), }; let objs = self.objs[self.idx].get_or_insert_with(Vec::new); if !objs.contains(&id) { diff --git a/gix/tests/fixtures/generated-archives/make_rev_spec_parse_repos.tar b/gix/tests/fixtures/generated-archives/make_rev_spec_parse_repos.tar index 00c1a397922..b53ba5e45af 100644 Binary files a/gix/tests/fixtures/generated-archives/make_rev_spec_parse_repos.tar and b/gix/tests/fixtures/generated-archives/make_rev_spec_parse_repos.tar differ diff --git a/gix/tests/fixtures/make_rev_spec_parse_repos.sh b/gix/tests/fixtures/make_rev_spec_parse_repos.sh index e2477434320..8cfc0eb6198 100755 --- a/gix/tests/fixtures/make_rev_spec_parse_repos.sh +++ b/gix/tests/fixtures/make_rev_spec_parse_repos.sh @@ -450,6 +450,37 @@ EOF baseline @...@ ) +git init deleted_prior_checkout +( + cd deleted_prior_checkout + tick + + git commit --allow-empty -m c1 + git checkout -b prev-target + git checkout main + git branch -d prev-target + + # `@{-1}` replays the previous checkout name through regular revision + # parsing. Once that branch is deleted, Git rejects it even though HEAD's + # reflog still contains the previous object id. + baseline "@{-1}" +) + +git init deleted_prior_checkout_named_like_object +( + cd deleted_prior_checkout_named_like_object + tick + + git commit --allow-empty -m c1 + git checkout -b 0123456789012345678901234567890123456789 + git checkout main + git branch -d 0123456789012345678901234567890123456789 + + # Git interprets a previous checkout name that is a full object id as + # that object id, even when no such object exists in the database. + baseline "@{-1}" +) + git init new (cd new baseline '@{1}' diff --git a/gix/tests/gix/revision/spec/from_bytes/reflog.rs b/gix/tests/gix/revision/spec/from_bytes/reflog.rs index 2790641c005..f0eb111d688 100644 --- a/gix/tests/gix/revision/spec/from_bytes/reflog.rs +++ b/gix/tests/gix/revision/spec/from_bytes/reflog.rs @@ -27,6 +27,30 @@ fn nth_prior_checkout() { ); } +#[test] +fn nth_prior_checkout_to_deleted_branch_fails_like_git() -> crate::Result { + let repo = repo("deleted_prior_checkout")?; + let err = parse_spec("@{-1}", &repo).expect_err("deleted prior checkout branch must not resolve by object id"); + assert!( + err.probable_cause() + .to_string() + .contains("Previous checkout 'prev-target' does not resolve"), + "error should explain that the reflog name no longer resolves" + ); + Ok(()) +} + +#[test] +fn nth_prior_checkout_to_deleted_branch_named_like_object_matches_git() -> crate::Result { + let repo = repo("deleted_prior_checkout_named_like_object")?; + assert_eq!( + parse_spec("@{-1}", &repo)?, + Spec::from_id(hex_to_id_sha1_only("0123456789012345678901234567890123456789").attach(&repo)), + "full object ids are accepted as previous checkout names, even without matching objects" + ); + Ok(()) +} + #[test] fn by_index_unborn_head() { let repo = &repo("new").unwrap();