Unified · Split
| @@ -0,0 +1,208 @@ | ||
| 1 | +use crate::file::transaction::prepare_and_commit::{committer, create_at, delete_at, empty_store}; | |
| 2 | +use git_lock::acquire::Fail; | |
| 3 | +use git_ref::file::transaction::PackedRefs; | |
| 4 | +use git_ref::transaction::{Change, LogChange, PreviousValue, RefEdit}; | |
| 5 | +use git_ref::Target; | |
| 6 | +use std::convert::TryInto; | |
| 7 | + | |
| 8 | +fn case_sensitive(tmp_dir: &std::path::Path) -> bool { | |
| 9 | + std::fs::write(tmp_dir.join("config"), "").expect("can create file once"); | |
| 10 | + !git_worktree::fs::Capabilities::probe(tmp_dir).ignore_case | |
| 11 | +} | |
| 12 | + | |
| 13 | +#[test] | |
| 14 | +fn conflicting_creation_without_packed_refs() -> crate::Result { | |
| 15 | + let (dir, store) = empty_store()?; | |
| 16 | + let res = store.transaction().prepare( | |
| 17 | + [create_at("refs/a"), create_at("refs/A")], | |
| 18 | + Fail::Immediately, | |
| 19 | + Fail::Immediately, | |
| 20 | + ); | |
| 21 | + | |
| 22 | + let case_sensitive = case_sensitive(dir.path()); | |
| 23 | + match res { | |
| 24 | + Ok(_) if case_sensitive => {} | |
| 25 | + Ok(_) if !case_sensitive => panic!("should fail as 'a' and 'A' clash"), | |
| 26 | + Err(err) if case_sensitive => panic!( | |
| 27 | + "should work as case sensitivity allows 'a' and 'A' to coexist: {:?}", | |
| 28 | + err | |
| 29 | + ), | |
| 30 | + Err(err) if !case_sensitive => { | |
| 31 | + assert_eq!(err.to_string(), "A lock could not be obtained for reference \"refs/A\"") | |
| 32 | + } | |
| 33 | + _ => unreachable!("actually everything is covered"), | |
| 34 | + } | |
| 35 | + Ok(()) | |
| 36 | +} | |
| 37 | + | |
| 38 | +#[test] | |
| 39 | +fn non_conflicting_creation_without_packed_refs_work() -> crate::Result { | |
| 40 | + let (_dir, store) = empty_store()?; | |
| 41 | + let ongoing = store | |
| 42 | + .transaction() | |
| 43 | + .prepare([create_at("refs/new")], Fail::Immediately, Fail::Immediately) | |
| 44 | + .unwrap(); | |
| 45 | + | |
| 46 | + let t2 = store.transaction().prepare( | |
| 47 | + [create_at("refs/non-conflicting")], | |
| 48 | + Fail::Immediately, | |
| 49 | + Fail::Immediately, | |
| 50 | + )?; | |
| 51 | + | |
| 52 | + t2.commit(committer().to_ref())?; | |
| 53 | + ongoing.commit(committer().to_ref())?; | |
| 54 | + | |
| 55 | + Ok(()) | |
| 56 | +} | |
| 57 | + | |
| 58 | +#[test] | |
| 59 | +fn packed_refs_lock_is_mandatory_for_multiple_ongoing_transactions_even_if_one_does_not_need_it() -> crate::Result { | |
| 60 | + let (_dir, store) = empty_store()?; | |
| 61 | + let ref_name = "refs/a"; | |
| 62 | + let _t1 = store | |
| 63 | + .transaction() | |
| 64 | + .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference( | |
| 65 | + Box::new(|_, _| Ok(Some(git_object::Kind::Commit))), | |
| 66 | + )) | |
| 67 | + .prepare([create_at(ref_name)], Fail::Immediately, Fail::Immediately)?; | |
| 68 | + | |
| 69 | + let t2res = store | |
| 70 | + .transaction() | |
| 71 | + .prepare([delete_at(ref_name)], Fail::Immediately, Fail::Immediately); | |
| 72 | + assert_eq!(&t2res.unwrap_err().to_string()[..51], "The lock for the packed-ref file could not be obtai", "if packed-refs are about to be created, other transactions always acquire a packed-refs lock as to not miss anything"); | |
| 73 | + Ok(()) | |
| 74 | +} | |
| 75 | + | |
| 76 | +#[test] | |
| 77 | +fn conflicting_creation_into_packed_refs() -> crate::Result { | |
| 78 | + let (_dir, store) = empty_store()?; | |
| 79 | + store | |
| 80 | + .transaction() | |
| 81 | + .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference( | |
| 82 | + Box::new(|_, _| Ok(Some(git_object::Kind::Commit))), | |
| 83 | + )) | |
| 84 | + .prepare( | |
| 85 | + [create_at("refs/a"), create_at("refs/A")], | |
| 86 | + Fail::Immediately, | |
| 87 | + Fail::Immediately, | |
| 88 | + )? | |
| 89 | + .commit(committer().to_ref())?; | |
| 90 | + | |
| 91 | + assert_eq!( | |
| 92 | + store.cached_packed_buffer()?.expect("created").iter()?.count(), | |
| 93 | + 2, | |
| 94 | + "packed-refs can store everything in case-insensitive manner" | |
| 95 | + ); | |
| 96 | + assert_eq!( | |
| 97 | + store.loose_iter()?.count(), | |
| 98 | + 0, | |
| 99 | + "refs/ directory isn't present as there is no loose ref - it removed every up to the base dir" | |
| 100 | + ); | |
| 101 | + | |
| 102 | + // The following works because locks aren't actually obtained if there would be no change. | |
| 103 | + store | |
| 104 | + .transaction() | |
| 105 | + .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference( | |
| 106 | + Box::new(|_, _| Ok(Some(git_object::Kind::Commit))), | |
| 107 | + )) | |
| 108 | + .prepare( | |
| 109 | + [ | |
| 110 | + RefEdit { | |
| 111 | + change: Change::Update { | |
| 112 | + log: LogChange::default(), | |
| 113 | + expected: PreviousValue::Any, | |
| 114 | + new: Target::Peeled(git_hash::Kind::Sha1.null()), | |
| 115 | + }, | |
| 116 | + name: "refs/a".try_into().expect("valid"), | |
| 117 | + deref: false, | |
| 118 | + }, | |
| 119 | + RefEdit { | |
| 120 | + change: Change::Update { | |
| 121 | + log: LogChange::default(), | |
| 122 | + expected: PreviousValue::MustExistAndMatch(Target::Peeled(git_hash::Kind::Sha1.null())), | |
| 123 | + new: Target::Peeled(git_hash::Kind::Sha1.null()), | |
| 124 | + }, | |
| 125 | + name: "refs/A".try_into().expect("valid"), | |
| 126 | + deref: false, | |
| 127 | + }, | |
| 128 | + ], | |
| 129 | + Fail::Immediately, | |
| 130 | + Fail::Immediately, | |
| 131 | + )? | |
| 132 | + .commit(committer().to_ref())?; | |
| 133 | + assert_eq!(store.iter()?.all()?.count(), 2); | |
| 134 | + | |
| 135 | + { | |
| 136 | + let _ongoing = store | |
| 137 | + .transaction() | |
| 138 | + .prepare([create_at("refs/new")], Fail::Immediately, Fail::Immediately)?; | |
| 139 | + | |
| 140 | + let t2res = store.transaction().prepare( | |
| 141 | + [create_at("refs/non-conflicting")], | |
| 142 | + Fail::Immediately, | |
| 143 | + Fail::Immediately, | |
| 144 | + ); | |
| 145 | + | |
| 146 | + assert_eq!( | |
| 147 | + &t2res.unwrap_err().to_string()[..40], | |
| 148 | + "The lock for the packed-ref file could n", | |
| 149 | + "packed-refs files will always be locked if they are present as we have to look up their content" | |
| 150 | + ); | |
| 151 | + } | |
| 152 | + | |
| 153 | + { | |
| 154 | + let _ongoing = store | |
| 155 | + .transaction() | |
| 156 | + .prepare([delete_at("refs/a")], Fail::Immediately, Fail::Immediately)?; | |
| 157 | + | |
| 158 | + let t2res = store | |
| 159 | + .transaction() | |
| 160 | + .prepare([delete_at("refs/A")], Fail::Immediately, Fail::Immediately); | |
| 161 | + | |
| 162 | + assert_eq!( | |
| 163 | + &t2res.unwrap_err().to_string()[..40], | |
| 164 | + "The lock for the packed-ref file could n", | |
| 165 | + "once again, packed-refs save the day" | |
| 166 | + ); | |
| 167 | + } | |
| 168 | + | |
| 169 | + // Create a loose ref at a path | |
| 170 | + assert_eq!(store.loose_iter()?.count(), 0); | |
| 171 | + store | |
| 172 | + .transaction() | |
| 173 | + .prepare( | |
| 174 | + [RefEdit { | |
| 175 | + change: Change::Update { | |
| 176 | + log: LogChange::default(), | |
| 177 | + expected: PreviousValue::Any, | |
| 178 | + new: Target::Symbolic("refs/heads/does-not-matter".try_into().expect("valid")), | |
| 179 | + }, | |
| 180 | + name: "refs/a".try_into().expect("valid"), | |
| 181 | + deref: false, | |
| 182 | + }], | |
| 183 | + Fail::Immediately, | |
| 184 | + Fail::Immediately, | |
| 185 | + )? | |
| 186 | + .commit(committer().to_ref())?; | |
| 187 | + assert_eq!( | |
| 188 | + store.loose_iter()?.count(), | |
| 189 | + 1, | |
| 190 | + "we created a loose ref, overlaying the packed one" | |
| 191 | + ); | |
| 192 | + | |
| 193 | + store | |
| 194 | + .transaction() | |
| 195 | + .prepare( | |
| 196 | + [delete_at("refs/a"), delete_at("refs/A")], | |
| 197 | + Fail::Immediately, | |
| 198 | + Fail::Immediately, | |
| 199 | + )? | |
| 200 | + .commit(committer().to_ref())?; | |
| 201 | + | |
| 202 | + assert_eq!( | |
| 203 | + store.iter()?.all()?.count(), | |
| 204 | + 0, | |
| 205 | + "we deleted our only two packed refs and one loose ref with the same name" | |
| 206 | + ); | |
| 207 | + Ok(()) | |
| 208 | +} | |
| @@ -21,217 +21,7 @@ use crate::file::{ · 20 unmodified lines | ||
| 21 | 21 | transaction::prepare_and_commit::{committer, empty_store, log_line, reflog_lines}, |
| 22 | 22 | }; |
| 23 | 23 | |
| 24 | −mod collisions { | |
| 25 | − use crate::file::transaction::prepare_and_commit::{committer, create_at, delete_at, empty_store}; | |
| 26 | − use git_lock::acquire::Fail; | |
| 27 | − use git_ref::file::transaction::PackedRefs; | |
| 28 | − use git_ref::transaction::{Change, LogChange, PreviousValue, RefEdit}; | |
| 29 | − use git_ref::Target; | |
| 30 | − use std::convert::TryInto; | |
| 31 | − | |
| 32 | − fn case_sensitive(tmp_dir: &std::path::Path) -> bool { | |
| 33 | − std::fs::write(tmp_dir.join("config"), "").expect("can create file once"); | |
| 34 | − !git_worktree::fs::Capabilities::probe(tmp_dir).ignore_case | |
| 35 | − } | |
| 36 | − | |
| 37 | − #[test] | |
| 38 | − fn conflicting_creation_without_packed_refs() -> crate::Result { | |
| 39 | − let (dir, store) = empty_store()?; | |
| 40 | − let res = store.transaction().prepare( | |
| 41 | − [create_at("refs/a"), create_at("refs/A")], | |
| 42 | − Fail::Immediately, | |
| 43 | − Fail::Immediately, | |
| 44 | − ); | |
| 45 | − | |
| 46 | − let case_sensitive = case_sensitive(dir.path()); | |
| 47 | − match res { | |
| 48 | − Ok(_) if case_sensitive => {} | |
| 49 | − Ok(_) if !case_sensitive => panic!("should fail as 'a' and 'A' clash"), | |
| 50 | − Err(err) if case_sensitive => panic!( | |
| 51 | − "should work as case sensitivity allows 'a' and 'A' to coexist: {:?}", | |
| 52 | − err | |
| 53 | − ), | |
| 54 | − Err(err) if !case_sensitive => { | |
| 55 | − assert_eq!(err.to_string(), "A lock could not be obtained for reference \"refs/A\"") | |
| 56 | − } | |
| 57 | − _ => unreachable!("actually everything is covered"), | |
| 58 | − } | |
| 59 | − Ok(()) | |
| 60 | − } | |
| 61 | − | |
| 62 | − #[test] | |
| 63 | − fn non_conflicting_creation_without_packed_refs_work() -> crate::Result { | |
| 64 | − let (_dir, store) = empty_store()?; | |
| 65 | − let ongoing = store | |
| 66 | − .transaction() | |
| 67 | − .prepare([create_at("refs/new")], Fail::Immediately, Fail::Immediately) | |
| 68 | − .unwrap(); | |
| 69 | − | |
| 70 | − let t2 = store.transaction().prepare( | |
| 71 | − [create_at("refs/non-conflicting")], | |
| 72 | − Fail::Immediately, | |
| 73 | − Fail::Immediately, | |
| 74 | − )?; | |
| 75 | − | |
| 76 | − t2.commit(committer().to_ref())?; | |
| 77 | − ongoing.commit(committer().to_ref())?; | |
| 78 | − | |
| 79 | − Ok(()) | |
| 80 | − } | |
| 81 | − | |
| 82 | − #[test] | |
| 83 | − fn packed_refs_lock_is_mandatory_for_multiple_ongoing_transactions_even_if_one_does_not_need_it() -> crate::Result { | |
| 84 | − let (_dir, store) = empty_store()?; | |
| 85 | − let ref_name = "refs/a"; | |
| 86 | − let _t1 = store | |
| 87 | − .transaction() | |
| 88 | − .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference( | |
| 89 | − Box::new(|_, _| Ok(Some(git_object::Kind::Commit))), | |
| 90 | − )) | |
| 91 | − .prepare([create_at(ref_name)], Fail::Immediately, Fail::Immediately)?; | |
| 92 | − | |
| 93 | − let t2res = store | |
| 94 | − .transaction() | |
| 95 | − .prepare([delete_at(ref_name)], Fail::Immediately, Fail::Immediately); | |
| 96 | − assert_eq!(&t2res.unwrap_err().to_string()[..51], "The lock for the packed-ref file could not be obtai", "if packed-refs are about to be created, other transactions always acquire a packed-refs lock as to not miss anything"); | |
| 97 | − Ok(()) | |
| 98 | − } | |
| 99 | − | |
| 100 | − #[test] | |
| 101 | − fn conflicting_creation_into_packed_refs() -> crate::Result { | |
| 102 | − let (_dir, store) = empty_store()?; | |
| 103 | − store | |
| 104 | − .transaction() | |
| 105 | − .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference( | |
| 106 | − Box::new(|_, _| Ok(Some(git_object::Kind::Commit))), | |
| 107 | − )) | |
| 108 | − .prepare( | |
| 109 | − [create_at("refs/a"), create_at("refs/A")], | |
| 110 | − Fail::Immediately, | |
| 111 | − Fail::Immediately, | |
| 112 | − )? | |
| 113 | − .commit(committer().to_ref())?; | |
| 114 | − | |
| 115 | − assert_eq!( | |
| 116 | − store.cached_packed_buffer()?.expect("created").iter()?.count(), | |
| 117 | − 2, | |
| 118 | − "packed-refs can store everything in case-insensitive manner" | |
| 119 | − ); | |
| 120 | − assert_eq!( | |
| 121 | − store.loose_iter()?.count(), | |
| 122 | − 0, | |
| 123 | − "refs/ directory isn't present as there is no loose ref - it removed every up to the base dir" | |
| 124 | − ); | |
| 125 | − | |
| 126 | − // The following works because locks aren't actually obtained if there would be no change. | |
| 127 | − store | |
| 128 | − .transaction() | |
| 129 | − .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference( | |
| 130 | − Box::new(|_, _| Ok(Some(git_object::Kind::Commit))), | |
| 131 | − )) | |
| 132 | − .prepare( | |
| 133 | − [ | |
| 134 | − RefEdit { | |
| 135 | − change: Change::Update { | |
| 136 | − log: LogChange::default(), | |
| 137 | − expected: PreviousValue::Any, | |
| 138 | − new: Target::Peeled(git_hash::Kind::Sha1.null()), | |
| 139 | − }, | |
| 140 | − name: "refs/a".try_into().expect("valid"), | |
| 141 | − deref: false, | |
| 142 | − }, | |
| 143 | − RefEdit { | |
| 144 | − change: Change::Update { | |
| 145 | − log: LogChange::default(), | |
| 146 | − expected: PreviousValue::MustExistAndMatch(Target::Peeled(git_hash::Kind::Sha1.null())), | |
| 147 | − new: Target::Peeled(git_hash::Kind::Sha1.null()), | |
| 148 | − }, | |
| 149 | − name: "refs/A".try_into().expect("valid"), | |
| 150 | − deref: false, | |
| 151 | − }, | |
| 152 | − ], | |
| 153 | − Fail::Immediately, | |
| 154 | − Fail::Immediately, | |
| 155 | − )? | |
| 156 | − .commit(committer().to_ref())?; | |
| 157 | − assert_eq!(store.iter()?.all()?.count(), 2); | |
| 158 | − | |
| 159 | − { | |
| 160 | − let _ongoing = | |
| 161 | − store | |
| 162 | − .transaction() | |
| 163 | − .prepare([create_at("refs/new")], Fail::Immediately, Fail::Immediately)?; | |
| 164 | − | |
| 165 | − let t2res = store.transaction().prepare( | |
| 166 | − [create_at("refs/non-conflicting")], | |
| 167 | − Fail::Immediately, | |
| 168 | − Fail::Immediately, | |
| 169 | − ); | |
| 170 | − | |
| 171 | − assert_eq!( | |
| 172 | − &t2res.unwrap_err().to_string()[..40], | |
| 173 | − "The lock for the packed-ref file could n", | |
| 174 | − "packed-refs files will always be locked if they are present as we have to look up their content" | |
| 175 | − ); | |
| 176 | − } | |
| 177 | − | |
| 178 | − { | |
| 179 | − let _ongoing = store | |
| 180 | − .transaction() | |
| 181 | − .prepare([delete_at("refs/a")], Fail::Immediately, Fail::Immediately)?; | |
| 182 | − | |
| 183 | − let t2res = store | |
| 184 | − .transaction() | |
| 185 | − .prepare([delete_at("refs/A")], Fail::Immediately, Fail::Immediately); | |
| 186 | − | |
| 187 | − assert_eq!( | |
| 188 | − &t2res.unwrap_err().to_string()[..40], | |
| 189 | − "The lock for the packed-ref file could n", | |
| 190 | − "once again, packed-refs save the day" | |
| 191 | − ); | |
| 192 | − } | |
| 193 | − | |
| 194 | − // Create a loose ref at a path | |
| 195 | − assert_eq!(store.loose_iter()?.count(), 0); | |
| 196 | − store | |
| 197 | − .transaction() | |
| 198 | − .prepare( | |
| 199 | − [RefEdit { | |
| 200 | − change: Change::Update { | |
| 201 | − log: LogChange::default(), | |
| 202 | − expected: PreviousValue::Any, | |
| 203 | − new: Target::Symbolic("refs/heads/does-not-matter".try_into().expect("valid")), | |
| 204 | − }, | |
| 205 | − name: "refs/a".try_into().expect("valid"), | |
| 206 | − deref: false, | |
| 207 | − }], | |
| 208 | − Fail::Immediately, | |
| 209 | − Fail::Immediately, | |
| 210 | − )? | |
| 211 | − .commit(committer().to_ref())?; | |
| 212 | − assert_eq!( | |
| 213 | − store.loose_iter()?.count(), | |
| 214 | − 1, | |
| 215 | − "we created a loose ref, overlaying the packed one" | |
| 216 | − ); | |
| 217 | − | |
| 218 | − store | |
| 219 | − .transaction() | |
| 220 | − .prepare( | |
| 221 | − [delete_at("refs/a"), delete_at("refs/A")], | |
| 222 | − Fail::Immediately, | |
| 223 | − Fail::Immediately, | |
| 224 | − )? | |
| 225 | − .commit(committer().to_ref())?; | |
| 226 | − | |
| 227 | − assert_eq!( | |
| 228 | − store.iter()?.all()?.count(), | |
| 229 | − 0, | |
| 230 | − "we deleted our only two packed refs and one loose ref with the same name" | |
| 231 | − ); | |
| 232 | − Ok(()) | |
| 233 | − } | |
| 234 | −} | |
| 24 | +mod collisions; | |
| 235 | 25 | |
| 236 | 26 | #[test] |
| 237 | 27 | fn intermediate_directories_are_removed_on_rollback() -> crate::Result { |