]>
Commit | Line | Data |
---|---|---|
0a29b90c FG |
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 committing 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<'_> { | |
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 | ||
26 | unsafe fn from_raw(ptr: *mut raw::git_transaction) -> Transaction<'repo> { | |
27 | Transaction { | |
28 | raw: ptr, | |
29 | _marker: marker::PhantomData, | |
30 | } | |
31 | } | |
32 | ||
33 | 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. | |
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. | |
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. | |
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 atomicity is **not* guaranteed: if the transaction fails to | |
110 | /// modify `refname`, the reflog may still have been committed to disk. | |
111 | /// | |
112 | /// If this is combined with setting the target, that update won't be | |
113 | /// written to the log (i.e. the `reflog_signature` and `reflog_message` | |
114 | /// parameters will be ignored). | |
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`. | |
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. | |
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] | |
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] | |
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] | |
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] | |
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] | |
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] | |
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] | |
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] | |
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 | } |