1use 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#[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 #[must_use]
20 pub fn new() -> Self {
21 Self::default()
22 }
23
24 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 pub fn update(mut self, update: RefUpdate) -> Result<Self, RefStoreError> {
43 self.push_update(update)?;
44 Ok(self)
45 }
46
47 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 #[must_use]
68 pub fn updates(&self) -> &[RefUpdate] {
69 &self.updates
70 }
71
72 #[must_use]
74 pub fn into_updates(self) -> Vec<RefUpdate> {
75 self.updates
76 }
77
78 #[must_use]
80 pub fn reflog_renames(&self) -> &[RefReflogRename] {
81 &self.reflog_renames
82 }
83
84 #[must_use]
86 pub fn case_only_renames(&self) -> &[RefCaseOnlyRename] {
87 &self.case_only_renames
88 }
89
90 #[must_use]
92 pub fn with_reflog_rename(mut self, rename: RefReflogRename) -> Self {
93 self.reflog_renames.push(rename);
94 self
95 }
96
97 #[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 #[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 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#[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}