pub fn restricted() -> crate::open::Options { crate::open::Options::isolated().config_overrides(["user.name=gitoxide", "user.email=gitoxide@localhost"])} /// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_.fn hex_to_id(hex: &str) -> gix_hash::ObjectId { gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("valid hex object id")} mod update { use gix_testtools::Result; use super::hex_to_id; use crate as gix; fn base_repo_path() -> String { gix::path::realpath( gix_testtools::scripted_fixture_read_only("make_remote_repos.sh") .unwrap() .join("base"), ) .unwrap() .to_string_lossy() .into_owned() } fn repo(name: &str) -> gix::Repository { let dir = gix_testtools::scripted_fixture_read_only_with_args_single_archive( "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_single_archive( "make_fetch_repos.sh", [base_repo_path()], gix_testtools::Creation::Execute, ) .unwrap(); let repo = gix::open_opts(dir.path().join(name), restricted()).unwrap(); (repo, dir) } /// Resolve `name` to its peeled object id, so expected ids track the fixture's hash. fn peeled_id(repo: &gix::Repository, name: &str) -> gix_hash::ObjectId { repo.find_reference(name) .expect("reference exists") .peel_to_id() .expect("ref peels to an object") .detach() } use gix_ref::{ Target, TargetRef, transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, }; use crate::{ bstr::BString, remote::{ fetch, fetch::{ RefLogMessage, refmap::{Mapping, Source, SpecIndex}, refs::{tests::restricted, update::TypeChange}, }, }, }; #[test] fn various_valid_updates() { let repo = repo("two-origins"); for (spec, expected_mode, reflog_message, detail) in [ ( "refs/heads/main:refs/remotes/origin/main", fetch::refs::update::Mode::NoChangeNeeded, Some("no update will be performed"), "these refs are en-par since the initial clone", ), ( "refs/heads/main", fetch::refs::update::Mode::NoChangeNeeded, None, "without local destination ref there is nothing to do for us, ever (except for FETCH_HEADs) later", ), ( "refs/heads/main:refs/remotes/origin/new-main", fetch::refs::update::Mode::New, Some("storing ref"), "the destination branch doesn't exist and needs to be created", ), ( "refs/heads/main:refs/heads/feature", fetch::refs::update::Mode::New, Some("storing head"), "reflog messages are specific to the type of branch stored, to some limited extend", ), ( "refs/heads/main:refs/tags/new-tag", fetch::refs::update::Mode::New, Some("storing tag"), "reflog messages are specific to the type of branch stored, to some limited extend", ), ( "+refs/heads/main:refs/remotes/origin/new-main", fetch::refs::update::Mode::New, Some("storing ref"), "just to validate that we really are in dry-run mode, or else this ref would be present now", ), ( "+refs/heads/main:refs/remotes/origin/g", fetch::refs::update::Mode::FastForward, Some("fast-forward (guessed in dry-run)"), "a forced non-fastforward (main goes backwards), but dry-run calls it fast-forward", ), ( "+refs/heads/main:refs/tags/b-tag", fetch::refs::update::Mode::Forced, Some("updating tag"), "tags can only be forced", ), ( "refs/heads/main:refs/tags/b-tag", fetch::refs::update::Mode::RejectedTagUpdate, None, "otherwise a tag is always refusing itself to be overwritten (no-clobber)", ), ( "+refs/remotes/origin/g:refs/heads/main", fetch::refs::update::Mode::RejectedCurrentlyCheckedOut { worktree_dirs: vec![repo.workdir().expect("present").to_owned()], }, None, "checked out branches cannot be written, as it requires a merge of sorts which isn't done here", ), ( "refs/remotes/origin/g:refs/heads/not-currently-checked-out", fetch::refs::update::Mode::FastForward, Some("fast-forward (guessed in dry-run)"), "a fast-forward only fast-forward situation, all good", ), ] { let (mapping, specs) = mapping_from_spec(spec, &repo); let out = fetch::refs::update( &repo, prefixed("action"), &mapping, &specs, &[], fetch::Tags::None, reflog_message.map_or(fetch::DryRun::No, |_| fetch::DryRun::Yes), fetch::WritePackedRefs::Never, ) .unwrap(); assert_eq!( out.updates, vec![fetch::refs::Update { type_change: None, mode: expected_mode.clone(), edit_index: reflog_message.map(|_| 0), }], "{spec:?}: {detail}" ); assert_eq!(out.edits.len(), reflog_message.map_or(0, |_| 1)); if let Some(reflog_message) = reflog_message { let edit = &out.edits[0]; match &edit.change { Change::Update { log, new, .. } => { assert_eq!( log.message, format!("action: {reflog_message}"), "{spec}: reflog messages are specific and we emulate git word for word" ); let remote_ref = repo .find_reference(specs[0].to_ref().source().expect("always present")) .unwrap(); assert_eq!( new.id(), remote_ref.target().id(), "remote ref provides the id to set in the local reference" ); } _ => unreachable!("only updates"), } } } let missing = "f".repeat(repo.object_hash().len_in_hex()); let (mapping, specs) = mapping_from_spec(&format!("{missing}:refs/heads/invalid-source-object"), &repo); let out = fetch::refs::update( &repo, prefixed("action"), &mapping, &specs, &[], fetch::Tags::None, fetch::DryRun::No, fetch::WritePackedRefs::Never, ) .unwrap(); assert_eq!( out.updates, vec![fetch::refs::Update { type_change: None, mode: fetch::refs::update::Mode::RejectedSourceObjectNotFound { id: hex_to_id(&missing), }, edit_index: None, }], "a source object that isn't present is rejected" ); assert_eq!(out.edits.len(), 0); } #[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_single_archive( "make_fetch_repos.sh", [base_repo_path()], )?)?; let repo = root.join("worktree-root"); let repo = gix::open_opts(repo, restricted())?; for (branch, path_from_root) in [ ("main", "worktree-root"), ("wt-a-nested", "prev/wt-a-nested"), ("wt-a", "wt-a"), ("nested-wt-b", "wt-a/nested-wt-b"), ("wt-c-locked", "wt-c-locked"), ("wt-deleted", "wt-deleted"), ] { let spec = format!("refs/heads/main:refs/heads/{branch}"); let (mappings, specs) = mapping_from_spec(&spec, &repo); 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::RejectedCurrentlyCheckedOut { 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)" ); assert_eq!(out.edits.len(), 0); } Ok(()) } #[test] 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::Object(peeled_id(&remote_repo, "refs/heads/symbolic")); 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"); let main_id = peeled_id(&repo, "refs/heads/main"); 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,Lines 1–500 of 1012. Load more
pub fn restricted() -> crate::open::Options { crate::open::Options::isolated().config_overrides(["user.name=gitoxide", "user.email=gitoxide@localhost"]) } /// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_. fn hex_to_id(hex: &str) -> gix_hash::ObjectId { gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("valid hex object id") } mod update { use gix_testtools::Result; use super::hex_to_id; use crate as gix; fn base_repo_path() -> String { gix::path::realpath( gix_testtools::scripted_fixture_read_only("make_remote_repos.sh") .unwrap() .join("base"), ) .unwrap() .to_string_lossy() .into_owned() } fn repo(name: &str) -> gix::Repository { let dir = gix_testtools::scripted_fixture_read_only_with_args_single_archive( "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_single_archive( "make_fetch_repos.sh", [base_repo_path()], gix_testtools::Creation::Execute, ) .unwrap(); let repo = gix::open_opts(dir.path().join(name), restricted()).unwrap(); (repo, dir) } /// Resolve `name` to its peeled object id, so expected ids track the fixture's hash. fn peeled_id(repo: &gix::Repository, name: &str) -> gix_hash::ObjectId { repo.find_reference(name) .expect("reference exists") .peel_to_id() .expect("ref peels to an object") .detach() } use gix_ref::{ Target, TargetRef, transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, }; use crate::{ bstr::BString, remote::{ fetch, fetch::{ RefLogMessage, refmap::{Mapping, Source, SpecIndex}, refs::{tests::restricted, update::TypeChange}, }, }, }; #[test] fn various_valid_updates() { let repo = repo("two-origins"); for (spec, expected_mode, reflog_message, detail) in [ ( "refs/heads/main:refs/remotes/origin/main", fetch::refs::update::Mode::NoChangeNeeded, Some("no update will be performed"), "these refs are en-par since the initial clone", ), ( "refs/heads/main", fetch::refs::update::Mode::NoChangeNeeded, None, "without local destination ref there is nothing to do for us, ever (except for FETCH_HEADs) later", ), ( "refs/heads/main:refs/remotes/origin/new-main", fetch::refs::update::Mode::New, Some("storing ref"), "the destination branch doesn't exist and needs to be created", ), ( "refs/heads/main:refs/heads/feature", fetch::refs::update::Mode::New, Some("storing head"), "reflog messages are specific to the type of branch stored, to some limited extend", ), ( "refs/heads/main:refs/tags/new-tag", fetch::refs::update::Mode::New, Some("storing tag"), "reflog messages are specific to the type of branch stored, to some limited extend", ), ( "+refs/heads/main:refs/remotes/origin/new-main", fetch::refs::update::Mode::New, Some("storing ref"), "just to validate that we really are in dry-run mode, or else this ref would be present now", ), ( "+refs/heads/main:refs/remotes/origin/g", fetch::refs::update::Mode::FastForward, Some("fast-forward (guessed in dry-run)"), "a forced non-fastforward (main goes backwards), but dry-run calls it fast-forward", ), ( "+refs/heads/main:refs/tags/b-tag", fetch::refs::update::Mode::Forced, Some("updating tag"), "tags can only be forced", ), ( "refs/heads/main:refs/tags/b-tag", fetch::refs::update::Mode::RejectedTagUpdate, None, "otherwise a tag is always refusing itself to be overwritten (no-clobber)", ), ( "+refs/remotes/origin/g:refs/heads/main", fetch::refs::update::Mode::RejectedCurrentlyCheckedOut { worktree_dirs: vec![repo.workdir().expect("present").to_owned()], }, None, "checked out branches cannot be written, as it requires a merge of sorts which isn't done here", ), ( "refs/remotes/origin/g:refs/heads/not-currently-checked-out", fetch::refs::update::Mode::FastForward, Some("fast-forward (guessed in dry-run)"), "a fast-forward only fast-forward situation, all good", ), ] { let (mapping, specs) = mapping_from_spec(spec, &repo); let out = fetch::refs::update( &repo, prefixed("action"), &mapping, &specs, &[], fetch::Tags::None, reflog_message.map_or(fetch::DryRun::No, |_| fetch::DryRun::Yes), fetch::WritePackedRefs::Never, ) .unwrap(); assert_eq!( out.updates, vec![fetch::refs::Update { type_change: None, mode: expected_mode.clone(), edit_index: reflog_message.map(|_| 0), }], "{spec:?}: {detail}" ); assert_eq!(out.edits.len(), reflog_message.map_or(0, |_| 1)); if let Some(reflog_message) = reflog_message { let edit = &out.edits[0]; match &edit.change { Change::Update { log, new, .. } => { assert_eq!( log.message, format!("action: {reflog_message}"), "{spec}: reflog messages are specific and we emulate git word for word" ); let remote_ref = repo .find_reference(specs[0].to_ref().source().expect("always present")) .unwrap(); assert_eq!( new.id(), remote_ref.target().id(), "remote ref provides the id to set in the local reference" ); } _ => unreachable!("only updates"), } } } let missing = "f".repeat(repo.object_hash().len_in_hex()); let (mapping, specs) = mapping_from_spec(&format!("{missing}:refs/heads/invalid-source-object"), &repo); let out = fetch::refs::update( &repo, prefixed("action"), &mapping, &specs, &[], fetch::Tags::None, fetch::DryRun::No, fetch::WritePackedRefs::Never, ) .unwrap(); assert_eq!( out.updates, vec![fetch::refs::Update { type_change: None, mode: fetch::refs::update::Mode::RejectedSourceObjectNotFound { id: hex_to_id(&missing), }, edit_index: None, }], "a source object that isn't present is rejected" ); assert_eq!(out.edits.len(), 0); } #[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_single_archive( "make_fetch_repos.sh", [base_repo_path()], )?)?; let repo = root.join("worktree-root"); let repo = gix::open_opts(repo, restricted())?; for (branch, path_from_root) in [ ("main", "worktree-root"), ("wt-a-nested", "prev/wt-a-nested"), ("wt-a", "wt-a"), ("nested-wt-b", "wt-a/nested-wt-b"), ("wt-c-locked", "wt-c-locked"), ("wt-deleted", "wt-deleted"), ] { let spec = format!("refs/heads/main:refs/heads/{branch}"); let (mappings, specs) = mapping_from_spec(&spec, &repo); 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::RejectedCurrentlyCheckedOut { 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)" ); assert_eq!(out.edits.len(), 0); } Ok(()) } #[test] 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::Object(peeled_id(&remote_repo, "refs/heads/symbolic")); 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"); let main_id = peeled_id(&repo, "refs/heads/main"); 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,