Skip to main content

grit_lib/refs/store/
transaction.rs

1//! [`RefTransaction`] builder and validation.
2
3use std::collections::HashSet;
4
5use super::error::RefStoreError;
6use super::types::{Expected, RawRef, RefCaseOnlyRename, RefReflogRename, RefUpdate};
7use super::validation::{validate_reflog_update, validate_storable_refname};
8
9/// Batch of ref updates prepared for [`super::RefStore::prepare`].
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
11pub struct RefTransaction {
12    updates: Vec<RefUpdate>,
13    reflog_renames: Vec<RefReflogRename>,
14    case_only_renames: Vec<RefCaseOnlyRename>,
15}
16
17impl RefTransaction {
18    /// Start an empty transaction.
19    #[must_use]
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// Start a transaction holding `updates`, checking for duplicate names in one pass.
25    ///
26    /// # Errors
27    ///
28    /// Returns [`RefStoreError::DuplicateUpdate`] for the first name queued twice.
29    pub(crate) fn from_updates(updates: Vec<RefUpdate>) -> Result<Self, RefStoreError> {
30        Self::validate_no_duplicates(&updates)?;
31        Ok(Self {
32            updates,
33            ..Self::default()
34        })
35    }
36
37    /// Append an update, rejecting duplicate ref names in the same batch.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`RefStoreError::DuplicateUpdate`] when `update.name` was already queued.
42    pub fn update(mut self, update: RefUpdate) -> Result<Self, RefStoreError> {
43        self.push_update(update)?;
44        Ok(self)
45    }
46
47    /// Append an update in place.
48    ///
49    /// # Errors
50    ///
51    /// Returns [`RefStoreError::DuplicateUpdate`] when `update.name` was already queued.
52    pub fn push_update(&mut self, update: RefUpdate) -> Result<(), RefStoreError> {
53        if self
54            .updates
55            .iter()
56            .any(|existing| existing.name == update.name)
57        {
58            return Err(RefStoreError::DuplicateUpdate {
59                name: update.name.clone(),
60            });
61        }
62        self.updates.push(update);
63        Ok(())
64    }
65
66    /// Borrow the queued updates.
67    #[must_use]
68    pub fn updates(&self) -> &[RefUpdate] {
69        &self.updates
70    }
71
72    /// Consume the transaction and return the update list.
73    #[must_use]
74    pub fn into_updates(self) -> Vec<RefUpdate> {
75        self.updates
76    }
77
78    /// Reflog rename operations bundled with this transaction.
79    #[must_use]
80    pub fn reflog_renames(&self) -> &[RefReflogRename] {
81        &self.reflog_renames
82    }
83
84    /// Case-only ref renames bundled with this transaction.
85    #[must_use]
86    pub fn case_only_renames(&self) -> &[RefCaseOnlyRename] {
87        &self.case_only_renames
88    }
89
90    /// Append a branch reflog rename to this transaction.
91    #[must_use]
92    pub fn with_reflog_rename(mut self, rename: RefReflogRename) -> Self {
93        self.reflog_renames.push(rename);
94        self
95    }
96
97    /// Append a case-only branch ref rename (files backend).
98    #[must_use]
99    pub fn with_case_only_rename(mut self, rename: RefCaseOnlyRename) -> Self {
100        self.case_only_renames.push(rename);
101        self
102    }
103
104    /// Split into ref updates, reflog renames, and case-only ref renames.
105    #[must_use]
106    pub fn into_parts(self) -> (Vec<RefUpdate>, Vec<RefReflogRename>, Vec<RefCaseOnlyRename>) {
107        (self.updates, self.reflog_renames, self.case_only_renames)
108    }
109
110    /// Backend-independent checks every [`super::RefStore::prepare`] runs before taking locks.
111    ///
112    /// Duplicate names cannot reach here because [`Self::push_update`] rejects them.
113    pub(crate) fn validate(&self) -> Result<(), RefStoreError> {
114        for update in &self.updates {
115            validate_storable_refname(&update.name)?;
116            if !update.flags.log_only
117                && matches!(&update.new_value, Some(RawRef::Direct(oid)) if oid.is_zero())
118            {
119                return Err(RefStoreError::NullOid {
120                    name: update.name.clone(),
121                });
122            }
123            if let Some(ref log) = update.reflog {
124                validate_reflog_update(log)?;
125            }
126        }
127        for rename in &self.reflog_renames {
128            validate_reflog_update(&rename.append)?;
129        }
130        let renames = self
131            .reflog_renames
132            .iter()
133            .map(|r| (&r.from, &r.to))
134            .chain(self.case_only_renames.iter().map(|r| (&r.from, &r.to)));
135        for (from, to) in renames {
136            validate_storable_refname(from)?;
137            validate_storable_refname(to)?;
138        }
139        Ok(())
140    }
141
142    fn validate_no_duplicates(updates: &[RefUpdate]) -> Result<(), RefStoreError> {
143        let mut seen = HashSet::with_capacity(updates.len());
144        for update in updates {
145            if !seen.insert(&update.name) {
146                return Err(RefStoreError::DuplicateUpdate {
147                    name: update.name.clone(),
148                });
149            }
150        }
151        Ok(())
152    }
153}
154
155/// Whether `actual` satisfies `expected`.
156#[must_use]
157pub fn expected_matches(actual: Option<&RawRef>, expected: &Expected) -> bool {
158    match expected {
159        Expected::Any => true,
160        Expected::Missing => actual.is_none(),
161        Expected::Exists => actual.is_some(),
162        Expected::Oid(want) => matches!(actual, Some(RawRef::Direct(got)) if got == want),
163        Expected::Symref(want) => matches!(actual, Some(RawRef::Symbolic(got)) if got == want),
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use crate::objects::ObjectId;
170
171    use super::*;
172
173    fn oid(byte: u8) -> ObjectId {
174        let mut bytes = [0u8; 20];
175        bytes[19] = byte;
176        ObjectId::from_bytes(&bytes).expect("valid oid")
177    }
178
179    #[test]
180    fn duplicate_names_rejected() {
181        let txn = RefTransaction::new().update(RefUpdate {
182            name: "refs/heads/a".to_owned(),
183            new_value: Some(RawRef::Direct(oid(1))),
184            expected: Expected::Any,
185            reflog: None,
186            flags: Default::default(),
187        });
188        assert!(txn.is_ok());
189        let err = txn
190            .unwrap()
191            .update(RefUpdate {
192                name: "refs/heads/a".to_owned(),
193                new_value: Some(RawRef::Direct(oid(2))),
194                expected: Expected::Any,
195                reflog: None,
196                flags: Default::default(),
197            })
198            .unwrap_err();
199        assert!(matches!(
200            err,
201            RefStoreError::DuplicateUpdate { name } if name == "refs/heads/a"
202        ));
203    }
204
205    fn direct(name: &str, value: ObjectId) -> RefUpdate {
206        RefUpdate {
207            name: name.to_owned(),
208            new_value: Some(RawRef::Direct(value)),
209            expected: Expected::Any,
210            reflog: None,
211            flags: Default::default(),
212        }
213    }
214
215    fn sample_log() -> super::super::ReflogUpdate {
216        super::super::ReflogUpdate {
217            identity: "A <a@example.com>".to_owned(),
218            message: "rename".to_owned(),
219            time: time::OffsetDateTime::UNIX_EPOCH,
220            old_oid: None,
221            new_oid: None,
222        }
223    }
224
225    #[test]
226    fn validate_rejects_unsafe_update_names() {
227        for name in [
228            "",
229            "heads/x",
230            "refs/heads/../x",
231            "refs/heads/a b",
232            "refs/heads/x/",
233        ] {
234            let err = RefTransaction::new()
235                .update(direct(name, oid(1)))
236                .unwrap()
237                .validate()
238                .unwrap_err();
239            assert_eq!(
240                err,
241                RefStoreError::InvalidRefName {
242                    name: name.to_owned()
243                }
244            );
245        }
246    }
247
248    #[test]
249    fn validate_rejects_unsafe_rename_sides() {
250        let reflog = RefTransaction::new().with_reflog_rename(RefReflogRename {
251            from: "refs/heads/ok".to_owned(),
252            to: "refs/heads/x.lock".to_owned(),
253            append: sample_log(),
254        });
255        assert_eq!(
256            reflog.validate().unwrap_err(),
257            RefStoreError::InvalidRefName {
258                name: "refs/heads/x.lock".to_owned()
259            }
260        );
261        let case_only = RefTransaction::new().with_case_only_rename(RefCaseOnlyRename {
262            from: "refs/heads//x".to_owned(),
263            to: "refs/heads/X".to_owned(),
264        });
265        assert_eq!(
266            case_only.validate().unwrap_err(),
267            RefStoreError::InvalidRefName {
268                name: "refs/heads//x".to_owned()
269            }
270        );
271    }
272
273    #[test]
274    fn validate_rejects_null_oid_unless_log_only() {
275        let err = RefTransaction::new()
276            .update(direct("refs/heads/z", ObjectId::zero()))
277            .unwrap()
278            .validate()
279            .unwrap_err();
280        assert_eq!(
281            err,
282            RefStoreError::NullOid {
283                name: "refs/heads/z".to_owned()
284            }
285        );
286
287        let mut log_only = direct("HEAD", ObjectId::zero());
288        log_only.flags.log_only = true;
289        assert!(RefTransaction::new()
290            .update(log_only)
291            .unwrap()
292            .validate()
293            .is_ok());
294    }
295
296    #[test]
297    fn validate_rejects_forged_reflog_identity_and_message() {
298        let mut update = direct("HEAD", oid(1));
299        update.flags.log_only = true;
300        update.reflog = Some(super::super::ReflogUpdate {
301            identity: "Evil <a@b>\nforged".to_owned(),
302            message: "ok".to_owned(),
303            time: time::OffsetDateTime::UNIX_EPOCH,
304            old_oid: None,
305            new_oid: None,
306        });
307        assert!(matches!(
308            RefTransaction::new()
309                .update(update)
310                .unwrap()
311                .validate()
312                .unwrap_err(),
313            RefStoreError::InvalidReflog(_)
314        ));
315
316        let mut update = direct("HEAD", oid(1));
317        update.flags.log_only = true;
318        update.reflog = Some(super::super::ReflogUpdate {
319            identity: "A <a@example.com>".to_owned(),
320            message: "bad\nline".to_owned(),
321            time: time::OffsetDateTime::UNIX_EPOCH,
322            old_oid: None,
323            new_oid: None,
324        });
325        assert!(matches!(
326            RefTransaction::new()
327                .update(update)
328                .unwrap()
329                .validate()
330                .unwrap_err(),
331            RefStoreError::InvalidReflog(_)
332        ));
333
334        let txn = RefTransaction::new().with_reflog_rename(RefReflogRename {
335            from: "refs/heads/a".to_owned(),
336            to: "refs/heads/b".to_owned(),
337            append: super::super::ReflogUpdate {
338                identity: "Evil <a@b>\nforged".to_owned(),
339                message: "rename".to_owned(),
340                time: time::OffsetDateTime::UNIX_EPOCH,
341                old_oid: None,
342                new_oid: None,
343            },
344        });
345        assert!(matches!(
346            txn.validate().unwrap_err(),
347            RefStoreError::InvalidReflog(_)
348        ));
349    }
350
351    #[test]
352    fn validate_accepts_pseudorefs_symrefs_and_deletes() {
353        let txn = RefTransaction::new()
354            .update(direct("ORIG_HEAD", oid(1)))
355            .unwrap()
356            .update(RefUpdate {
357                name: "HEAD".to_owned(),
358                new_value: Some(RawRef::Symbolic("refs/heads/main".to_owned())),
359                expected: Expected::Any,
360                reflog: None,
361                flags: Default::default(),
362            })
363            .unwrap()
364            .update(RefUpdate {
365                name: "refs/heads/gone".to_owned(),
366                new_value: None,
367                expected: Expected::Any,
368                reflog: None,
369                flags: Default::default(),
370            })
371            .unwrap();
372        assert!(txn.validate().is_ok());
373    }
374
375    #[test]
376    fn from_updates_rejects_a_name_queued_twice() {
377        let update = |name: &str| RefUpdate {
378            name: name.to_owned(),
379            new_value: None,
380            expected: Expected::Any,
381            reflog: None,
382            flags: Default::default(),
383        };
384        let txn =
385            RefTransaction::from_updates(vec![update("refs/heads/a"), update("refs/heads/b")])
386                .expect("distinct names");
387        assert_eq!(txn.updates().len(), 2);
388        let err = RefTransaction::from_updates(vec![
389            update("refs/heads/a"),
390            update("refs/heads/b"),
391            update("refs/heads/a"),
392        ])
393        .unwrap_err();
394        assert!(matches!(
395            err,
396            RefStoreError::DuplicateUpdate { name } if name == "refs/heads/a"
397        ));
398    }
399
400    #[test]
401    fn expected_oid_requires_direct_match() {
402        let direct = RawRef::Direct(oid(1));
403        assert!(expected_matches(Some(&direct), &Expected::Oid(oid(1))));
404        assert!(!expected_matches(Some(&direct), &Expected::Oid(oid(2))));
405        assert!(!expected_matches(None, &Expected::Exists));
406        assert!(expected_matches(None, &Expected::Missing));
407    }
408}