1 use std::ffi::CString; 2 use std::marker; 3 4 use crate::{raw, util::Binding, Error, Oid, Reflog, Repository, Signature}; 5 6 /// A structure representing a transactional update of a repository's references. 7 /// 8 /// Transactions work by locking loose refs for as long as the [`Transaction`] 9 /// is held, and committing all changes to disk when [`Transaction::commit`] is 10 /// called. Note that comitting is not atomic: if an operation fails, the 11 /// transaction aborts, but previous successful operations are not rolled back. 12 pub struct Transaction<'repo> { 13 raw: *mut raw::git_transaction, 14 _marker: marker::PhantomData<&'repo Repository>, 15 } 16 17 impl Drop for Transaction<'_> { drop(&mut self)18 fn drop(&mut self) { 19 unsafe { raw::git_transaction_free(self.raw) } 20 } 21 } 22 23 impl<'repo> Binding for Transaction<'repo> { 24 type Raw = *mut raw::git_transaction; 25 from_raw(ptr: *mut raw::git_transaction) -> Transaction<'repo>26 unsafe fn from_raw(ptr: *mut raw::git_transaction) -> Transaction<'repo> { 27 Transaction { 28 raw: ptr, 29 _marker: marker::PhantomData, 30 } 31 } 32 raw(&self) -> *mut raw::git_transaction33 fn raw(&self) -> *mut raw::git_transaction { 34 self.raw 35 } 36 } 37 38 impl<'repo> Transaction<'repo> { 39 /// Lock the specified reference by name. lock_ref(&mut self, refname: &str) -> Result<(), Error>40 pub fn lock_ref(&mut self, refname: &str) -> Result<(), Error> { 41 let refname = CString::new(refname).unwrap(); 42 unsafe { 43 try_call!(raw::git_transaction_lock_ref(self.raw, refname)); 44 } 45 46 Ok(()) 47 } 48 49 /// Set the target of the specified reference. 50 /// 51 /// The reference must have been locked via `lock_ref`. 52 /// 53 /// If `reflog_signature` is `None`, the [`Signature`] is read from the 54 /// repository config. set_target( &mut self, refname: &str, target: Oid, reflog_signature: Option<&Signature<'_>>, reflog_message: &str, ) -> Result<(), Error>55 pub fn set_target( 56 &mut self, 57 refname: &str, 58 target: Oid, 59 reflog_signature: Option<&Signature<'_>>, 60 reflog_message: &str, 61 ) -> Result<(), Error> { 62 let refname = CString::new(refname).unwrap(); 63 let reflog_message = CString::new(reflog_message).unwrap(); 64 unsafe { 65 try_call!(raw::git_transaction_set_target( 66 self.raw, 67 refname, 68 target.raw(), 69 reflog_signature.map(|s| s.raw()), 70 reflog_message 71 )); 72 } 73 74 Ok(()) 75 } 76 77 /// Set the target of the specified symbolic reference. 78 /// 79 /// The reference must have been locked via `lock_ref`. 80 /// 81 /// If `reflog_signature` is `None`, the [`Signature`] is read from the 82 /// repository config. set_symbolic_target( &mut self, refname: &str, target: &str, reflog_signature: Option<&Signature<'_>>, reflog_message: &str, ) -> Result<(), Error>83 pub fn set_symbolic_target( 84 &mut self, 85 refname: &str, 86 target: &str, 87 reflog_signature: Option<&Signature<'_>>, 88 reflog_message: &str, 89 ) -> Result<(), Error> { 90 let refname = CString::new(refname).unwrap(); 91 let target = CString::new(target).unwrap(); 92 let reflog_message = CString::new(reflog_message).unwrap(); 93 unsafe { 94 try_call!(raw::git_transaction_set_symbolic_target( 95 self.raw, 96 refname, 97 target, 98 reflog_signature.map(|s| s.raw()), 99 reflog_message 100 )); 101 } 102 103 Ok(()) 104 } 105 106 /// Add a [`Reflog`] to the transaction. 107 /// 108 /// This commit the in-memory [`Reflog`] to disk when the transaction commits. 109 /// Note that atomicty is **not* guaranteed: if the transaction fails to 110 /// modify `refname`, the reflog may still have been comitted to disk. 111 /// 112 /// If this is combined with setting the target, that update won't be 113 /// written to the log (ie. the `reflog_signature` and `reflog_message` 114 /// parameters will be ignored). set_reflog(&mut self, refname: &str, reflog: Reflog) -> Result<(), Error>115 pub fn set_reflog(&mut self, refname: &str, reflog: Reflog) -> Result<(), Error> { 116 let refname = CString::new(refname).unwrap(); 117 unsafe { 118 try_call!(raw::git_transaction_set_reflog( 119 self.raw, 120 refname, 121 reflog.raw() 122 )); 123 } 124 125 Ok(()) 126 } 127 128 /// Remove a reference. 129 /// 130 /// The reference must have been locked via `lock_ref`. remove(&mut self, refname: &str) -> Result<(), Error>131 pub fn remove(&mut self, refname: &str) -> Result<(), Error> { 132 let refname = CString::new(refname).unwrap(); 133 unsafe { 134 try_call!(raw::git_transaction_remove(self.raw, refname)); 135 } 136 137 Ok(()) 138 } 139 140 /// Commit the changes from the transaction. 141 /// 142 /// The updates will be made one by one, and the first failure will stop the 143 /// processing. commit(self) -> Result<(), Error>144 pub fn commit(self) -> Result<(), Error> { 145 unsafe { 146 try_call!(raw::git_transaction_commit(self.raw)); 147 } 148 Ok(()) 149 } 150 } 151 152 #[cfg(test)] 153 mod tests { 154 use crate::{Error, ErrorClass, ErrorCode, Oid, Repository}; 155 156 #[test] smoke()157 fn smoke() { 158 let (_td, repo) = crate::test::repo_init(); 159 160 let mut tx = t!(repo.transaction()); 161 162 t!(tx.lock_ref("refs/heads/main")); 163 t!(tx.lock_ref("refs/heads/next")); 164 165 t!(tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero")); 166 t!(tx.set_symbolic_target( 167 "refs/heads/next", 168 "refs/heads/main", 169 None, 170 "set next to main", 171 )); 172 173 t!(tx.commit()); 174 175 assert_eq!(repo.refname_to_id("refs/heads/main").unwrap(), Oid::zero()); 176 assert_eq!( 177 repo.find_reference("refs/heads/next") 178 .unwrap() 179 .symbolic_target() 180 .unwrap(), 181 "refs/heads/main" 182 ); 183 } 184 185 #[test] locks_same_repo_handle()186 fn locks_same_repo_handle() { 187 let (_td, repo) = crate::test::repo_init(); 188 189 let mut tx1 = t!(repo.transaction()); 190 t!(tx1.lock_ref("refs/heads/seen")); 191 192 let mut tx2 = t!(repo.transaction()); 193 assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked)) 194 } 195 196 #[test] locks_across_repo_handles()197 fn locks_across_repo_handles() { 198 let (td, repo1) = crate::test::repo_init(); 199 let repo2 = t!(Repository::open(&td)); 200 201 let mut tx1 = t!(repo1.transaction()); 202 t!(tx1.lock_ref("refs/heads/seen")); 203 204 let mut tx2 = t!(repo2.transaction()); 205 assert!(matches!(tx2.lock_ref("refs/heads/seen"), Err(e) if e.code() == ErrorCode::Locked)) 206 } 207 208 #[test] drop_unlocks()209 fn drop_unlocks() { 210 let (_td, repo) = crate::test::repo_init(); 211 212 let mut tx = t!(repo.transaction()); 213 t!(tx.lock_ref("refs/heads/seen")); 214 drop(tx); 215 216 let mut tx2 = t!(repo.transaction()); 217 t!(tx2.lock_ref("refs/heads/seen")) 218 } 219 220 #[test] commit_unlocks()221 fn commit_unlocks() { 222 let (_td, repo) = crate::test::repo_init(); 223 224 let mut tx = t!(repo.transaction()); 225 t!(tx.lock_ref("refs/heads/seen")); 226 t!(tx.commit()); 227 228 let mut tx2 = t!(repo.transaction()); 229 t!(tx2.lock_ref("refs/heads/seen")); 230 } 231 232 #[test] prevents_non_transactional_updates()233 fn prevents_non_transactional_updates() { 234 let (_td, repo) = crate::test::repo_init(); 235 let head = t!(repo.refname_to_id("HEAD")); 236 237 let mut tx = t!(repo.transaction()); 238 t!(tx.lock_ref("refs/heads/seen")); 239 240 assert!(matches!( 241 repo.reference("refs/heads/seen", head, true, "competing with lock"), 242 Err(e) if e.code() == ErrorCode::Locked 243 )); 244 } 245 246 #[test] remove()247 fn remove() { 248 let (_td, repo) = crate::test::repo_init(); 249 let head = t!(repo.refname_to_id("HEAD")); 250 let next = "refs/heads/next"; 251 252 t!(repo.reference( 253 next, 254 head, 255 true, 256 "refs/heads/next@{0}: branch: Created from HEAD" 257 )); 258 259 { 260 let mut tx = t!(repo.transaction()); 261 t!(tx.lock_ref(next)); 262 t!(tx.remove(next)); 263 t!(tx.commit()); 264 } 265 assert!(matches!(repo.refname_to_id(next), Err(e) if e.code() == ErrorCode::NotFound)) 266 } 267 268 #[test] must_lock_ref()269 fn must_lock_ref() { 270 let (_td, repo) = crate::test::repo_init(); 271 272 // 273 fn is_not_locked_err(e: &Error) -> bool { 274 e.code() == ErrorCode::NotFound 275 && e.class() == ErrorClass::Reference 276 && e.message() == "the specified reference is not locked" 277 } 278 279 let mut tx = t!(repo.transaction()); 280 assert!(matches!( 281 tx.set_target("refs/heads/main", Oid::zero(), None, "set main to zero"), 282 Err(e) if is_not_locked_err(&e) 283 )) 284 } 285 } 286