git2/
transaction.rs

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}