diff options
Diffstat (limited to 'vendor/gix/src/remote/connection/fetch/update_refs/tests.rs')
-rw-r--r-- | vendor/gix/src/remote/connection/fetch/update_refs/tests.rs | 427 |
1 files changed, 396 insertions, 31 deletions
diff --git a/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs b/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs index 47ab5d1a5..0b29f14f4 100644 --- a/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs +++ b/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs @@ -31,6 +31,10 @@ mod update { gix_testtools::scripted_fixture_read_only_with_args("make_fetch_repos.sh", [base_repo_path()]).unwrap(); gix::open_opts(dir.join(name), restricted()).unwrap() } + fn named_repo(name: &str) -> gix::Repository { + let dir = gix_testtools::scripted_fixture_read_only("make_remote_repos.sh").unwrap(); + gix::open_opts(dir.join(name), restricted()).unwrap() + } fn repo_rw(name: &str) -> (gix::Repository, gix_testtools::tempfile::TempDir) { let dir = gix_testtools::scripted_fixture_writable_with_args( "make_fetch_repos.sh", @@ -41,13 +45,19 @@ mod update { let repo = gix::open_opts(dir.path().join(name), restricted()).unwrap(); (repo, dir) } - use gix_ref::{transaction::Change, TargetRef}; + use gix_ref::{ + transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, + Target, TargetRef, + }; use crate::{ bstr::BString, remote::{ fetch, - fetch::{refs::tests::restricted, Mapping, RefLogMessage, Source, SpecIndex}, + fetch::{ + refs::{tests::restricted, update::TypeChange}, + Mapping, RefLogMessage, Source, SpecIndex, + }, }, }; @@ -112,7 +122,7 @@ mod update { ( "+refs/remotes/origin/g:refs/heads/main", fetch::refs::update::Mode::RejectedCurrentlyCheckedOut { - worktree_dir: repo.work_dir().expect("present").to_owned(), + worktree_dirs: vec![repo.work_dir().expect("present").to_owned()], }, None, "checked out branches cannot be written, as it requires a merge of sorts which isn't done here", @@ -148,6 +158,7 @@ mod update { assert_eq!( out.updates, vec![fetch::refs::Update { + type_change: None, mode: expected_mode.clone(), edit_index: reflog_message.map(|_| 0), }], @@ -180,7 +191,7 @@ mod update { #[test] fn checked_out_branches_in_worktrees_are_rejected_with_additional_information() -> Result { - let root = gix_path::realpath(gix_testtools::scripted_fixture_read_only_with_args( + let root = gix_path::realpath(&gix_testtools::scripted_fixture_read_only_with_args( "make_fetch_repos.sh", [base_repo_path()], )?)?; @@ -211,8 +222,9 @@ mod update { out.updates, vec![fetch::refs::Update { mode: fetch::refs::update::Mode::RejectedCurrentlyCheckedOut { - worktree_dir: root.join(path_from_root), + worktree_dirs: vec![root.join(path_from_root)], }, + type_change: None, edit_index: None, }], "{spec}: checked-out checks are done before checking if a change would actually be required (here it isn't)" @@ -223,10 +235,350 @@ mod update { } #[test] - fn local_symbolic_refs_are_never_written() { + fn unborn_remote_branches_can_be_created_locally_if_they_are_new() -> Result { + let repo = named_repo("unborn"); + let (mappings, specs) = mapping_from_spec("HEAD:refs/remotes/origin/HEAD", &repo); + assert_eq!(mappings.len(), 1); + let out = fetch::refs::update( + &repo, + prefixed("action"), + &mappings, + &specs, + &[], + fetch::Tags::None, + fetch::DryRun::Yes, + fetch::WritePackedRefs::Never, + )?; + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::New, + type_change: None, + edit_index: Some(0) + }] + ); + assert_eq!(out.edits.len(), 1, "we are OK with creating unborn refs"); + Ok(()) + } + + #[test] + fn unborn_remote_branches_can_update_local_unborn_branches() -> Result { + let repo = named_repo("unborn"); + let (mappings, specs) = mapping_from_spec("HEAD:refs/heads/existing-unborn-symbolic", &repo); + assert_eq!(mappings.len(), 1); + let out = fetch::refs::update( + &repo, + prefixed("action"), + &mappings, + &specs, + &[], + fetch::Tags::None, + fetch::DryRun::Yes, + fetch::WritePackedRefs::Never, + )?; + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::NoChangeNeeded, + type_change: None, + edit_index: Some(0) + }] + ); + assert_eq!(out.edits.len(), 1, "we are OK with updating unborn refs"); + assert_eq!( + out.edits[0], + RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: "action: change unborn ref".into(), + }, + expected: PreviousValue::MustExistAndMatch(Target::Symbolic( + "refs/heads/main".try_into().expect("valid"), + )), + new: Target::Symbolic("refs/heads/main".try_into().expect("valid")), + }, + name: "refs/heads/existing-unborn-symbolic".try_into().expect("valid"), + deref: false, + } + ); + + let (mappings, specs) = mapping_from_spec("HEAD:refs/heads/existing-unborn-symbolic-other", &repo); + assert_eq!(mappings.len(), 1); + let out = fetch::refs::update( + &repo, + prefixed("action"), + &mappings, + &specs, + &[], + fetch::Tags::None, + fetch::DryRun::Yes, + fetch::WritePackedRefs::Never, + )?; + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::Forced, + type_change: None, + edit_index: Some(0) + }] + ); + assert_eq!( + out.edits.len(), + 1, + "we are OK with creating unborn refs even without actually forcing it" + ); + assert_eq!( + out.edits[0], + RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: "action: change unborn ref".into(), + }, + expected: PreviousValue::MustExistAndMatch(Target::Symbolic( + "refs/heads/other".try_into().expect("valid"), + )), + new: Target::Symbolic("refs/heads/main".try_into().expect("valid")), + }, + name: "refs/heads/existing-unborn-symbolic-other".try_into().expect("valid"), + deref: false, + } + ); + Ok(()) + } + + #[test] + fn remote_symbolic_refs_with_locally_unavailable_target_result_in_valid_peeled_branches() -> Result { + let remote_repo = named_repo("one-commit-with-symref"); + let local_repo = named_repo("unborn"); + let (mappings, specs) = mapping_from_spec("refs/heads/symbolic:refs/heads/new", &remote_repo); + assert_eq!(mappings.len(), 1); + + let out = fetch::refs::update( + &local_repo, + prefixed("action"), + &mappings, + &specs, + &[], + fetch::Tags::None, + fetch::DryRun::Yes, + fetch::WritePackedRefs::Never, + )?; + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::New, + type_change: None, + edit_index: Some(0) + }] + ); + assert_eq!(out.edits.len(), 1); + let target = Target::Peeled(hex_to_id("66f16e4e8baf5c77bb6d0484495bebea80e916ce")); + assert_eq!( + out.edits[0], + RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: "action: storing head".into(), + }, + expected: PreviousValue::ExistingMustMatch(target.clone()), + new: target, + }, + name: "refs/heads/new".try_into().expect("valid"), + deref: false, + }, + "we create local-refs whose targets aren't present yet, even though the remote knows them.\ + This leaves the caller with assuring all refs are mentioned in mappings." + ); + Ok(()) + } + + #[test] + fn remote_symbolic_refs_with_locally_unavailable_target_dont_overwrite_valid_local_branches() -> Result { + let remote_repo = named_repo("one-commit-with-symref"); + let local_repo = named_repo("one-commit-with-symref-missing-branch"); + let (mappings, specs) = mapping_from_spec("refs/heads/unborn:refs/heads/valid-locally", &remote_repo); + assert_eq!(mappings.len(), 1); + + let out = fetch::refs::update( + &local_repo, + prefixed("action"), + &mappings, + &specs, + &[], + fetch::Tags::None, + fetch::DryRun::Yes, + fetch::WritePackedRefs::Never, + )?; + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::RejectedToReplaceWithUnborn, + type_change: None, + edit_index: None + }] + ); + assert_eq!(out.edits.len(), 0); + Ok(()) + } + + #[test] + fn unborn_remote_refs_dont_overwrite_valid_local_refs() -> Result { + let remote_repo = named_repo("unborn"); + let local_repo = named_repo("one-commit-with-symref"); + let (mappings, specs) = + mapping_from_spec("refs/heads/existing-unborn-symbolic:refs/heads/branch", &remote_repo); + assert_eq!(mappings.len(), 1); + + let out = fetch::refs::update( + &local_repo, + prefixed("action"), + &mappings, + &specs, + &[], + fetch::Tags::None, + fetch::DryRun::Yes, + fetch::WritePackedRefs::Never, + )?; + assert_eq!( + out.updates, + vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::RejectedToReplaceWithUnborn, + type_change: None, + edit_index: None + }], + "we don't overwrite locally present refs with unborn ones for safety" + ); + assert_eq!(out.edits.len(), 0); + Ok(()) + } + + #[test] + fn local_symbolic_refs_can_be_overwritten() { let repo = repo("two-origins"); - for source in ["refs/heads/main", "refs/heads/symbolic", "HEAD"] { - let (mappings, specs) = mapping_from_spec(&format!("{source}:refs/heads/symbolic"), &repo); + for (source, destination, expected_update, expected_edit) in [ + ( + // attempt to overwrite HEAD isn't possible as the matching engine will normalize the path. That way, `HEAD` + // can never be set. This is by design (of git) and we follow it. + "refs/heads/symbolic", + "HEAD", + fetch::refs::Update { + mode: fetch::refs::update::Mode::New, + type_change: None, + edit_index: Some(0), + }, + Some(RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: "action: storing head".into(), + }, + expected: PreviousValue::ExistingMustMatch(Target::Symbolic( + "refs/heads/main".try_into().expect("valid"), + )), + new: Target::Symbolic("refs/heads/main".try_into().expect("valid")), + }, + name: "refs/heads/HEAD".try_into().expect("valid"), + deref: false, + }), + ), + ( + // attempt to overwrite checked out branch fails + "refs/remotes/origin/b", // strange, but the remote-refs are simulated and based on local refs + "refs/heads/main", + fetch::refs::Update { + mode: fetch::refs::update::Mode::RejectedCurrentlyCheckedOut { + worktree_dirs: vec![repo.work_dir().expect("present").to_owned()], + }, + type_change: None, + edit_index: None, + }, + None, + ), + ( + // symbolic becomes direct + "refs/heads/main", + "refs/heads/symbolic", + fetch::refs::Update { + mode: fetch::refs::update::Mode::NoChangeNeeded, + type_change: Some(TypeChange::SymbolicToDirect), + edit_index: Some(0), + }, + Some(RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: "action: no update will be performed".into(), + }, + expected: PreviousValue::MustExistAndMatch(Target::Symbolic( + "refs/heads/main".try_into().expect("valid"), + )), + new: Target::Peeled(hex_to_id("f99771fe6a1b535783af3163eba95a927aae21d5")), + }, + name: "refs/heads/symbolic".try_into().expect("valid"), + deref: false, + }), + ), + ( + // direct becomes symbolic + "refs/heads/symbolic", + "refs/remotes/origin/a", + fetch::refs::Update { + mode: fetch::refs::update::Mode::NoChangeNeeded, + type_change: Some(TypeChange::DirectToSymbolic), + edit_index: Some(0), + }, + Some(RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: "action: no update will be performed".into(), + }, + expected: PreviousValue::MustExistAndMatch(Target::Peeled(hex_to_id( + "f99771fe6a1b535783af3163eba95a927aae21d5", + ))), + new: Target::Symbolic("refs/heads/main".try_into().expect("valid")), + }, + name: "refs/remotes/origin/a".try_into().expect("valid"), + deref: false, + }), + ), + ( + // symbolic to symbolic (same) + "refs/heads/symbolic", + "refs/heads/symbolic", + fetch::refs::Update { + mode: fetch::refs::update::Mode::NoChangeNeeded, + type_change: None, + edit_index: Some(0), + }, + Some(RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: "action: no update will be performed".into(), + }, + expected: PreviousValue::MustExistAndMatch(Target::Symbolic( + "refs/heads/main".try_into().expect("valid"), + )), + new: Target::Symbolic("refs/heads/main".try_into().expect("valid")), + }, + name: "refs/heads/symbolic".try_into().expect("valid"), + deref: false, + }), + ), + ] { + let (mappings, specs) = mapping_from_spec(&format!("{source}:{destination}"), &repo); + assert_eq!(mappings.len(), 1); let out = fetch::refs::update( &repo, prefixed("action"), @@ -239,15 +591,11 @@ mod update { ) .unwrap(); - assert_eq!(out.edits.len(), 0); - assert_eq!( - out.updates, - vec![fetch::refs::Update { - mode: fetch::refs::update::Mode::RejectedSymbolic, - edit_index: None - }], - "we don't overwrite these as the checked-out check needs to consider much more than it currently does, we are playing it safe" - ); + assert_eq!(out.edits.len(), usize::from(expected_edit.is_some())); + assert_eq!(out.updates, vec![expected_update]); + if let Some(expected) = expected_edit { + assert_eq!(out.edits, vec![expected]); + } } } @@ -275,17 +623,19 @@ mod update { ) .unwrap(); - assert_eq!(out.edits.len(), 1); + assert_eq!(out.edits.len(), 2, "symbolic refs are handled just like any other ref"); assert_eq!( out.updates, vec![ fetch::refs::Update { mode: fetch::refs::update::Mode::New, + type_change: None, edit_index: Some(0) }, fetch::refs::Update { - mode: fetch::refs::update::Mode::RejectedSymbolic, - edit_index: None + mode: fetch::refs::update::Mode::NoChangeNeeded, + type_change: Some(TypeChange::SymbolicToDirect), + edit_index: Some(1) } ], ); @@ -303,7 +653,7 @@ mod update { } #[test] - fn local_direct_refs_are_never_written_with_symbolic_ones_but_see_only_the_destination() { + fn local_direct_refs_are_written_with_symbolic_ones() { let repo = repo("two-origins"); let (mappings, specs) = mapping_from_spec("refs/heads/symbolic:refs/heads/not-currently-checked-out", &repo); let out = fetch::refs::update( @@ -323,6 +673,7 @@ mod update { out.updates, vec![fetch::refs::Update { mode: fetch::refs::update::Mode::NoChangeNeeded, + type_change: Some(fetch::refs::update::TypeChange::DirectToSymbolic), edit_index: Some(0) }], ); @@ -349,6 +700,7 @@ mod update { out.updates, vec![fetch::refs::Update { mode: fetch::refs::update::Mode::New, + type_change: None, edit_index: Some(0), }], ); @@ -399,10 +751,12 @@ mod update { vec![ fetch::refs::Update { mode: fetch::refs::update::Mode::New, + type_change: None, edit_index: Some(0), }, fetch::refs::Update { mode: fetch::refs::update::Mode::NoChangeNeeded, + type_change: None, edit_index: Some(1), } ], @@ -446,6 +800,7 @@ mod update { out.updates, vec![fetch::refs::Update { mode: fetch::refs::update::Mode::FastForward, + type_change: None, edit_index: Some(0), }], "The caller has to be aware and note that dry-runs can't know about fast-forwards as they don't have remote objects" @@ -480,6 +835,7 @@ mod update { out.updates, vec![fetch::refs::Update { mode: fetch::refs::update::Mode::RejectedNonFastForward, + type_change: None, edit_index: None, }] ); @@ -502,6 +858,7 @@ mod update { out.updates, vec![fetch::refs::Update { mode: fetch::refs::update::Mode::FastForward, + type_change: None, edit_index: Some(0), }] ); @@ -535,6 +892,7 @@ mod update { out.updates, vec![fetch::refs::Update { mode: fetch::refs::update::Mode::FastForward, + type_change: None, edit_index: Some(0), }] ); @@ -548,12 +906,15 @@ mod update { } } - fn mapping_from_spec(spec: &str, repo: &gix::Repository) -> (Vec<fetch::Mapping>, Vec<gix::refspec::RefSpec>) { + fn mapping_from_spec( + spec: &str, + remote_repo: &gix::Repository, + ) -> (Vec<fetch::Mapping>, Vec<gix::refspec::RefSpec>) { let spec = gix_refspec::parse(spec.into(), gix_refspec::parse::Operation::Fetch).unwrap(); let group = gix_refspec::MatchGroup::from_fetch_specs(Some(spec)); - let references = repo.references().unwrap(); + let references = remote_repo.references().unwrap(); let mut references: Vec<_> = references.all().unwrap().map(|r| into_remote_ref(r.unwrap())).collect(); - references.push(into_remote_ref(repo.find_reference("HEAD").unwrap())); + references.push(into_remote_ref(remote_repo.find_reference("HEAD").unwrap())); let mappings = group .match_remotes(references.iter().map(remote_ref_to_item)) .mappings @@ -566,7 +927,7 @@ mod update { }, |idx| fetch::Source::Ref(references[idx].clone()), ), - local: m.rhs.map(|r| r.into_owned()), + local: m.rhs.map(std::borrow::Cow::into_owned), spec_index: SpecIndex::ExplicitInRemote(m.spec_index), }) .collect(); @@ -582,11 +943,14 @@ mod update { }, TargetRef::Symbolic(name) => { let target = name.as_bstr().into(); - let id = r.peel_to_id_in_place().unwrap(); - gix_protocol::handshake::Ref::Symbolic { - full_ref_name, - target, - object: id.detach(), + match r.peel_to_id_in_place() { + Ok(id) => gix_protocol::handshake::Ref::Symbolic { + full_ref_name, + target, + tag: None, + object: id.detach(), + }, + Err(_) => gix_protocol::handshake::Ref::Unborn { full_ref_name, target }, } } } @@ -594,9 +958,10 @@ mod update { fn remote_ref_to_item(r: &gix_protocol::handshake::Ref) -> gix_refspec::match_group::Item<'_> { let (full_ref_name, target, object) = r.unpack(); + static NULL: gix_hash::ObjectId = gix_hash::Kind::Sha1.null(); gix_refspec::match_group::Item { full_ref_name, - target: target.expect("no unborn HEAD"), + target: target.unwrap_or(NULL.as_ref()), object, } } |