use crate::file::{ store_writable, transaction::prepare_and_commit::{committer, empty_store}, }; use git_lock::acquire::Fail; use git_ref::file::ReferenceExt; use git_ref::transaction::PreviousValue; use git_ref::{ transaction::{Change, RefEdit, RefLog}, Reference, Target, }; use git_testtools::hex_to_id; use std::convert::TryInto; #[test] fn delete_a_ref_which_is_gone_succeeds() -> crate::Result { let (_keep, store) = empty_store()?; let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "DOES_NOT_EXIST".try_into()?, deref: false, }), Fail::Immediately, )? .commit(&committer())?; assert_eq!(edits.len(), 1); Ok(()) } #[test] fn delete_a_ref_which_is_gone_but_must_exist_fails() -> crate::Result { let (_keep, store) = empty_store()?; let res = store.transaction().prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::MustExist, log: RefLog::AndReference, }, name: "DOES_NOT_EXIST".try_into()?, deref: false, }), Fail::Immediately, ); match res { Ok(_) => unreachable!("must exist, but it doesn't actually exist"), Err(err) => assert_eq!( err.to_string(), "The reference 'DOES_NOT_EXIST' for deletion did not exist or could not be parsed" ), } Ok(()) } #[test] fn delete_ref_and_reflog_on_symbolic_no_deref() -> crate::Result { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.find_loose("HEAD")?; assert!(head.log_exists(&store)); let _main = store.find_loose("main")?; let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::MustExist, log: RefLog::AndReference, }, name: head.name.clone(), deref: false, }), Fail::Immediately, )? .commit(&committer())?; assert_eq!( edits, vec![RefEdit { change: Change::Delete { expected: PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/main".try_into()?)), log: RefLog::AndReference, }, name: head.name, deref: false }], "the previous value was updated with the actual one" ); assert!( store.reflog_iter_rev("HEAD", &mut [0u8; 128])?.is_none(), "reflog was deleted" ); assert!(store.try_find_loose("HEAD")?.is_none(), "ref was deleted"); assert!(store.try_find_loose("main")?.is_some(), "referent still exists"); Ok(()) } #[test] fn delete_ref_with_incorrect_previous_value_fails() -> crate::Result { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.find_loose("HEAD")?; assert!(head.log_exists(&store)); let res = store.transaction().prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/main".try_into()?)), log: RefLog::Only, }, name: head.name, deref: true, }), Fail::Immediately, ); match res { Err(err) => { assert_eq!(err.to_string(), "The reference 'refs/heads/main' should have content ref: refs/heads/main, actual content was 02a7a22d90d7c02fb494ed25551850b868e634f0"); } Ok(_) => unreachable!("must be err"), } // everything stays as is let head = store.find_loose("HEAD")?; assert!(head.log_exists(&store)); let main = store.find_loose("main").expect("referent still exists"); assert!(main.log_exists(&store)); Ok(()) } #[test] fn delete_reflog_only_of_symbolic_no_deref() -> crate::Result { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.find_loose("HEAD")?; assert!(head.log_exists(&store)); let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/main".try_into()?)), log: RefLog::Only, }, name: head.name, deref: false, }), Fail::Immediately, )? .commit(&committer())?; assert_eq!(edits.len(), 1); let head: Reference = store.find_loose("HEAD")?.into(); assert!(!head.log_exists(&store)); let main = store.find_loose("main").expect("referent still exists"); assert!(main.log_exists(&store), "log is untouched, too"); assert_eq!( main.target, head.follow(&store, None).expect("a symref")?.target, "head points to main" ); Ok(()) } #[test] fn delete_reflog_only_of_symbolic_with_deref() -> crate::Result { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.find_loose("HEAD")?; assert!(head.log_exists(&store)); let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::MustExist, log: RefLog::Only, }, name: head.name, deref: true, }), Fail::Immediately, )? .commit(&committer())?; assert_eq!(edits.len(), 2); let head: Reference = store.find_loose("HEAD")?.into(); assert!(!head.log_exists(&store)); let main = store.find_loose("main").expect("referent still exists"); assert!(!main.log_exists(&store), "log is removed"); assert_eq!( main.target, head.follow(&store, None).expect("a symref")?.target, "head points to main" ); Ok(()) } #[test] /// Based on https://github.com/git/git/blob/master/refs/files-backend.c#L514:L515 fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> crate::Result { let (_keep, store) = empty_store()?; std::fs::write(store.base.join("HEAD"), &b"broken")?; assert!(store.try_find_loose("HEAD").is_err(), "the ref is truly broken"); let res = store.transaction().prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::MustExist, log: RefLog::AndReference, }, name: "HEAD".try_into()?, deref: true, }), Fail::Immediately, ); match res { Err(err) => { assert_eq!( err.to_string(), "The reference 'HEAD' for deletion did not exist or could not be parsed" ); } Ok(_) => unreachable!("expected error"), } Ok(()) } #[test] fn non_existing_can_be_deleted_with_the_may_exist_match_constraint() -> crate::Result { let (_keep, store) = empty_store()?; let previous_value = PreviousValue::ExistingMustMatch(Target::Peeled(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"))); let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Delete { expected: previous_value.clone(), log: RefLog::AndReference, }, name: "refs/heads/not-there".try_into()?, deref: true, }), Fail::Immediately, )? .commit(&committer())?; assert_eq!( edits, vec![RefEdit { change: Change::Delete { expected: previous_value, log: RefLog::AndReference, }, name: "refs/heads/not-there".try_into()?, deref: false, }] ); Ok(()) } #[test] /// Based on https://github.com/git/git/blob/master/refs/files-backend.c#L514:L515 fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> crate::Result { let (_keep, store) = empty_store()?; std::fs::write(store.base.join("HEAD"), &b"broken")?; assert!(store.try_find_loose("HEAD").is_err(), "the ref is truly broken"); let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "HEAD".try_into()?, deref: true, }), Fail::Immediately, )? .commit(&committer())?; assert!(store.try_find_loose("HEAD")?.is_none(), "the ref was deleted"); assert_eq!( edits, vec![RefEdit { change: Change::Delete { expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "HEAD".try_into()?, deref: false, }] ); Ok(()) } #[test] fn store_write_mode_has_no_effect_and_reflogs_are_always_deleted() -> crate::Result { for reflog_writemode in &[git_ref::file::WriteReflog::Normal, git_ref::file::WriteReflog::Disable] { let (_keep, mut store) = store_writable("make_repo_for_reflog.sh")?; store.write_reflog = *reflog_writemode; assert!(store.find_loose("HEAD")?.log_exists(&store)); assert!(store.packed_buffer()?.is_none(), "there is no pack"); let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::Any, log: RefLog::Only, }, name: "HEAD".try_into()?, deref: false, }), Fail::Immediately, )? .commit(&committer())?; assert_eq!(edits.len(), 1); assert!(!store.find_loose("HEAD")?.log_exists(&store), "log was deleted"); assert!(store.packed_buffer()?.is_none(), "there still is no pack"); } Ok(()) } #[test] fn packed_refs_are_consulted_when_determining_previous_value_of_ref_to_be_deleted_and_are_deleted_from_packed_ref_file( ) -> crate::Result { let (_keep, store) = store_writable("make_packed_ref_repository.sh")?; assert!( store.try_find_loose("main")?.is_none(), "no loose main available, it's packed" ); assert!( store.packed_buffer()?.expect("packed").try_find("main")?.is_some(), "packed main is available" ); let old_id = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::MustExistAndMatch(Target::Peeled(old_id)), log: RefLog::AndReference, }, name: "refs/heads/main".try_into()?, deref: false, }), git_lock::acquire::Fail::Immediately, )? .commit(&committer())?; assert_eq!(edits.len(), 1, "an edit was performed in the packed refs store"); let packed = store.packed_buffer()?.expect("packed ref present"); assert!(packed.try_find("main")?.is_none(), "no main present after deletion"); Ok(()) } #[test] fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_refs() -> crate::Result { let (_keep, store) = store_writable("make_packed_ref_repository_for_overlay.sh")?; let packed = store.packed_buffer()?.expect("packed-refs"); let branch = store.find("newer-as-loose", Some(&packed))?; let branch_id = branch.target.as_id().map(ToOwned::to_owned).expect("peeled"); assert_ne!( packed.find("newer-as-loose")?.target(), branch_id, "the packed ref is outdated" ); let edits = store .transaction() .prepare( Some(RefEdit { change: Change::Delete { expected: PreviousValue::MustExistAndMatch(Target::Peeled(branch_id)), log: RefLog::AndReference, }, name: branch.name, deref: false, }), git_lock::acquire::Fail::Immediately, )? .commit(&committer())?; assert_eq!( edits.len(), 1, "only one edit even though technically two places were changed" ); assert!( store .try_find("newer-as-loose", store.packed_buffer()?.as_ref())? .is_none(), "reference is deleted everywhere" ); Ok(()) } #[test] fn all_contained_references_deletes_the_packed_ref_file_too() -> crate::Result { for mode in ["must-exist", "may-exist"] { let (_keep, store) = store_writable("make_packed_ref_repository.sh")?; let edits = store .transaction() .prepare( store.packed_buffer()?.expect("packed-refs").iter()?.map(|r| { let r = r.expect("valid ref"); RefEdit { change: Change::Delete { expected: match mode { "must-exist" => PreviousValue::MustExistAndMatch(Target::Peeled(r.target())), "may-exist" => PreviousValue::ExistingMustMatch(Target::Peeled(r.target())), _ => unimplemented!("unknown mode: {}", mode), }, log: RefLog::AndReference, }, name: r.name.into(), deref: false, } }), git_lock::acquire::Fail::Immediately, )? .commit(&committer())?; assert!(!store.packed_refs_path().is_file(), "packed-refs was entirely removed"); let packed = store.packed_buffer()?; assert!(packed.is_none(), "it won't make up packed refs"); for edit in edits { assert!( store.try_find(edit.name.to_partial(), packed.as_ref())?.is_none(), "delete ref cannot be found" ); } } Ok(()) }