]> git.proxmox.com Git - rustc.git/blame - extra/git2/src/transaction.rs
New upstream version 1.73.0+dfsg1
[rustc.git] / extra / git2 / src / transaction.rs
CommitLineData
0a29b90c
FG
1use std::ffi::CString;
2use std::marker;
3
4use 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.
12pub struct Transaction<'repo> {
13 raw: *mut raw::git_transaction,
14 _marker: marker::PhantomData<&'repo Repository>,
15}
16
17impl Drop for Transaction<'_> {
18 fn drop(&mut self) {
19 unsafe { raw::git_transaction_free(self.raw) }
20 }
21}
22
23impl<'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
38impl<'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)]
153mod 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}