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