1use std::collections::HashSet;
4
5use super::error::RefStoreError;
6use super::types::{Expected, RawRef, RefCaseOnlyRename, RefReflogRename, RefUpdate};
7use super::validation::{
8 validate_reflog_update, validate_storable_refname, validate_symref_target,
9};
10
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct RefTransaction {
14 updates: Vec<RefUpdate>,
15 names: HashSet<String>,
16 reflog_renames: Vec<RefReflogRename>,
17 case_only_renames: Vec<RefCaseOnlyRename>,
18 initial: bool,
19 allow_failures: bool,
22}
23
24impl RefTransaction {
25 #[must_use]
27 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub(crate) fn from_updates(updates: Vec<RefUpdate>) -> Result<Self, RefStoreError> {
37 let mut names = HashSet::with_capacity(updates.len());
38 for update in &updates {
39 if !names.insert(update.name.clone()) {
40 return Err(RefStoreError::DuplicateUpdate {
41 name: update.name.clone(),
42 });
43 }
44 }
45 Ok(Self {
46 updates,
47 names,
48 ..Self::default()
49 })
50 }
51
52 pub fn update(mut self, update: RefUpdate) -> Result<Self, RefStoreError> {
58 self.push_update(update)?;
59 Ok(self)
60 }
61
62 pub fn push_update(&mut self, update: RefUpdate) -> Result<(), RefStoreError> {
68 if !self.names.insert(update.name.clone()) {
69 return Err(RefStoreError::DuplicateUpdate {
70 name: update.name.clone(),
71 });
72 }
73 self.updates.push(update);
74 Ok(())
75 }
76
77 #[must_use]
79 pub fn updates(&self) -> &[RefUpdate] {
80 &self.updates
81 }
82
83 #[must_use]
85 pub fn into_updates(self) -> Vec<RefUpdate> {
86 self.updates
87 }
88
89 #[must_use]
91 pub fn reflog_renames(&self) -> &[RefReflogRename] {
92 &self.reflog_renames
93 }
94
95 #[must_use]
97 pub fn case_only_renames(&self) -> &[RefCaseOnlyRename] {
98 &self.case_only_renames
99 }
100
101 #[must_use]
103 pub fn with_reflog_rename(mut self, rename: RefReflogRename) -> Self {
104 self.reflog_renames.push(rename);
105 self
106 }
107
108 #[must_use]
110 pub fn with_case_only_rename(mut self, rename: RefCaseOnlyRename) -> Self {
111 self.case_only_renames.push(rename);
112 self
113 }
114
115 #[must_use]
127 pub fn initial(mut self) -> Self {
128 self.initial = true;
129 self
130 }
131
132 #[must_use]
134 pub fn is_initial(&self) -> bool {
135 self.initial
136 }
137
138 #[must_use]
145 pub fn allowing_failures(mut self) -> Self {
146 self.allow_failures = true;
147 self
148 }
149
150 #[must_use]
152 pub fn allows_failures(&self) -> bool {
153 self.allow_failures
154 }
155
156 #[must_use]
158 pub fn into_parts(self) -> (Vec<RefUpdate>, Vec<RefReflogRename>, Vec<RefCaseOnlyRename>) {
159 (self.updates, self.reflog_renames, self.case_only_renames)
160 }
161
162 pub(crate) fn lock_name(&self) -> String {
165 self.updates
166 .first()
167 .map(|u| &u.name)
168 .or_else(|| self.reflog_renames.first().map(|r| &r.from))
169 .or_else(|| self.case_only_renames.first().map(|r| &r.from))
170 .map_or_else(|| "refs".to_owned(), Clone::clone)
171 }
172
173 pub(crate) fn validate(&self) -> Result<(), RefStoreError> {
178 for update in &self.updates {
179 validate_storable_refname(&update.name)?;
180 if !update.flags.log_only
181 && matches!(&update.new_value, Some(RawRef::Direct(oid)) if oid.is_zero())
182 {
183 return Err(RefStoreError::NullOid {
184 name: update.name.clone(),
185 });
186 }
187 if let Some(ref log) = update.reflog {
188 validate_reflog_update(log)?;
189 }
190 if let Some(RawRef::Symbolic(target)) = &update.new_value {
191 validate_symref_target(&update.name, target)?;
192 }
193 }
194 for rename in &self.reflog_renames {
195 validate_reflog_update(&rename.append)?;
196 }
197 let renames = self
198 .reflog_renames
199 .iter()
200 .map(|r| (&r.from, &r.to))
201 .chain(self.case_only_renames.iter().map(|r| (&r.from, &r.to)));
202 for (from, to) in renames {
203 validate_storable_refname(from)?;
204 validate_storable_refname(to)?;
205 }
206 Ok(())
207 }
208}
209
210#[must_use]
212pub fn expected_matches(actual: Option<&RawRef>, expected: &Expected) -> bool {
213 match expected {
214 Expected::Any => true,
215 Expected::Missing => actual.is_none(),
216 Expected::Exists => actual.is_some(),
217 Expected::Oid(want) => matches!(actual, Some(RawRef::Direct(got)) if got == want),
218 Expected::Symref(want) => matches!(actual, Some(RawRef::Symbolic(got)) if got == want),
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use crate::objects::ObjectId;
225
226 use super::*;
227
228 fn oid(byte: u8) -> ObjectId {
229 let mut bytes = [0u8; 20];
230 bytes[19] = byte;
231 ObjectId::from_bytes(&bytes).expect("valid oid")
232 }
233
234 #[test]
235 fn duplicate_names_rejected() {
236 let txn = RefTransaction::new().update(RefUpdate {
237 name: "refs/heads/a".to_owned(),
238 new_value: Some(RawRef::Direct(oid(1))),
239 expected: Expected::Any,
240 reflog: None,
241 flags: Default::default(),
242 });
243 assert!(txn.is_ok());
244 let err = txn
245 .unwrap()
246 .update(RefUpdate {
247 name: "refs/heads/a".to_owned(),
248 new_value: Some(RawRef::Direct(oid(2))),
249 expected: Expected::Any,
250 reflog: None,
251 flags: Default::default(),
252 })
253 .unwrap_err();
254 assert!(matches!(
255 err,
256 RefStoreError::DuplicateUpdate { name } if name == "refs/heads/a"
257 ));
258 }
259
260 fn direct(name: &str, value: ObjectId) -> RefUpdate {
261 RefUpdate {
262 name: name.to_owned(),
263 new_value: Some(RawRef::Direct(value)),
264 expected: Expected::Any,
265 reflog: None,
266 flags: Default::default(),
267 }
268 }
269
270 fn sample_log() -> super::super::ReflogUpdate {
271 super::super::ReflogUpdate {
272 identity: "A <a@example.com>".to_owned(),
273 message: "rename".to_owned(),
274 time: time::OffsetDateTime::UNIX_EPOCH,
275 old_oid: None,
276 new_oid: None,
277 }
278 }
279
280 #[test]
281 fn validate_rejects_unsafe_update_names() {
282 for name in [
283 "",
284 "heads/x",
285 "refs/heads/../x",
286 "refs/heads/a b",
287 "refs/heads/x/",
288 ] {
289 let err = RefTransaction::new()
290 .update(direct(name, oid(1)))
291 .unwrap()
292 .validate()
293 .unwrap_err();
294 assert_eq!(
295 err,
296 RefStoreError::InvalidRefName {
297 name: name.to_owned()
298 }
299 );
300 }
301 }
302
303 #[test]
304 fn validate_rejects_unsafe_rename_sides() {
305 let reflog = RefTransaction::new().with_reflog_rename(RefReflogRename {
306 from: "refs/heads/ok".to_owned(),
307 to: "refs/heads/x.lock".to_owned(),
308 append: sample_log(),
309 });
310 assert_eq!(
311 reflog.validate().unwrap_err(),
312 RefStoreError::InvalidRefName {
313 name: "refs/heads/x.lock".to_owned()
314 }
315 );
316 let case_only = RefTransaction::new().with_case_only_rename(RefCaseOnlyRename {
317 from: "refs/heads//x".to_owned(),
318 to: "refs/heads/X".to_owned(),
319 });
320 assert_eq!(
321 case_only.validate().unwrap_err(),
322 RefStoreError::InvalidRefName {
323 name: "refs/heads//x".to_owned()
324 }
325 );
326 }
327
328 #[test]
329 fn validate_rejects_null_oid_unless_log_only() {
330 let err = RefTransaction::new()
331 .update(direct("refs/heads/z", ObjectId::zero()))
332 .unwrap()
333 .validate()
334 .unwrap_err();
335 assert_eq!(
336 err,
337 RefStoreError::NullOid {
338 name: "refs/heads/z".to_owned()
339 }
340 );
341
342 let mut log_only = direct("HEAD", ObjectId::zero());
343 log_only.flags.log_only = true;
344 assert!(RefTransaction::new()
345 .update(log_only)
346 .unwrap()
347 .validate()
348 .is_ok());
349 }
350
351 #[test]
352 fn validate_rejects_forged_reflog_identity_and_message() {
353 let mut update = direct("HEAD", oid(1));
354 update.flags.log_only = true;
355 update.reflog = Some(super::super::ReflogUpdate {
356 identity: "Evil <a@b>\nforged".to_owned(),
357 message: "ok".to_owned(),
358 time: time::OffsetDateTime::UNIX_EPOCH,
359 old_oid: None,
360 new_oid: None,
361 });
362 assert!(matches!(
363 RefTransaction::new()
364 .update(update)
365 .unwrap()
366 .validate()
367 .unwrap_err(),
368 RefStoreError::InvalidReflog(_)
369 ));
370
371 let mut update = direct("HEAD", oid(1));
372 update.flags.log_only = true;
373 update.reflog = Some(super::super::ReflogUpdate {
374 identity: "A <a@example.com>".to_owned(),
375 message: "bad\nline".to_owned(),
376 time: time::OffsetDateTime::UNIX_EPOCH,
377 old_oid: None,
378 new_oid: None,
379 });
380 assert!(matches!(
381 RefTransaction::new()
382 .update(update)
383 .unwrap()
384 .validate()
385 .unwrap_err(),
386 RefStoreError::InvalidReflog(_)
387 ));
388
389 let txn = RefTransaction::new().with_reflog_rename(RefReflogRename {
390 from: "refs/heads/a".to_owned(),
391 to: "refs/heads/b".to_owned(),
392 append: super::super::ReflogUpdate {
393 identity: "Evil <a@b>\nforged".to_owned(),
394 message: "rename".to_owned(),
395 time: time::OffsetDateTime::UNIX_EPOCH,
396 old_oid: None,
397 new_oid: None,
398 },
399 });
400 assert!(matches!(
401 txn.validate().unwrap_err(),
402 RefStoreError::InvalidReflog(_)
403 ));
404 }
405
406 #[test]
407 fn validate_accepts_pseudorefs_symrefs_and_deletes() {
408 let txn = RefTransaction::new()
409 .update(direct("ORIG_HEAD", oid(1)))
410 .unwrap()
411 .update(RefUpdate {
412 name: "HEAD".to_owned(),
413 new_value: Some(RawRef::Symbolic("refs/heads/main".to_owned())),
414 expected: Expected::Any,
415 reflog: None,
416 flags: Default::default(),
417 })
418 .unwrap()
419 .update(RefUpdate {
420 name: "refs/heads/alias".to_owned(),
421 new_value: Some(RawRef::Symbolic("refs/heads/not-yet".to_owned())),
422 expected: Expected::Any,
423 reflog: None,
424 flags: Default::default(),
425 })
426 .unwrap()
427 .update(RefUpdate {
428 name: "FETCH_HEAD".to_owned(),
429 new_value: Some(RawRef::Symbolic("ORIG_HEAD".to_owned())),
430 expected: Expected::Any,
431 reflog: None,
432 flags: Default::default(),
433 })
434 .unwrap()
435 .update(RefUpdate {
436 name: "refs/heads/gone".to_owned(),
437 new_value: None,
438 expected: Expected::Any,
439 reflog: None,
440 flags: Default::default(),
441 })
442 .unwrap();
443 assert!(txn.validate().is_ok());
444 }
445
446 fn symref(name: &str, target: &str) -> RefUpdate {
447 RefUpdate {
448 name: name.to_owned(),
449 new_value: Some(RawRef::Symbolic(target.to_owned())),
450 expected: Expected::Any,
451 reflog: None,
452 flags: Default::default(),
453 }
454 }
455
456 fn invalid_symref(name: &str, target: &str) -> RefStoreError {
457 RefStoreError::InvalidSymrefTarget {
458 name: name.to_owned(),
459 target: target.to_owned(),
460 }
461 }
462
463 #[test]
464 fn validate_rejects_invalid_symref_targets() {
465 for (name, target) in [
466 ("HEAD", "foo"),
467 ("refs/heads/sym1", "foo"),
468 ("refs/heads/loop", "refs/heads/loop"),
469 ("HEAD", "refs/heads/a..b"),
470 ] {
471 let err = RefTransaction::new()
472 .update(symref(name, target))
473 .unwrap()
474 .validate()
475 .unwrap_err();
476 assert_eq!(err, invalid_symref(name, target), "{name} -> {target}");
477 }
478 }
479
480 #[test]
481 fn from_updates_rejects_a_name_queued_twice() {
482 let update = |name: &str| RefUpdate {
483 name: name.to_owned(),
484 new_value: None,
485 expected: Expected::Any,
486 reflog: None,
487 flags: Default::default(),
488 };
489 let txn =
490 RefTransaction::from_updates(vec![update("refs/heads/a"), update("refs/heads/b")])
491 .expect("distinct names");
492 assert_eq!(txn.updates().len(), 2);
493 let err = RefTransaction::from_updates(vec![
494 update("refs/heads/a"),
495 update("refs/heads/b"),
496 update("refs/heads/a"),
497 ])
498 .unwrap_err();
499 assert!(matches!(
500 err,
501 RefStoreError::DuplicateUpdate { name } if name == "refs/heads/a"
502 ));
503 }
504
505 #[test]
506 fn expected_oid_requires_direct_match() {
507 let direct = RawRef::Direct(oid(1));
508 assert!(expected_matches(Some(&direct), &Expected::Oid(oid(1))));
509 assert!(!expected_matches(Some(&direct), &Expected::Oid(oid(2))));
510 assert!(!expected_matches(None, &Expected::Exists));
511 assert!(expected_matches(None, &Expected::Missing));
512 }
513
514 #[test]
515 fn from_updates_then_push_update_rejects_duplicate_name() {
516 let update = |name: &str| RefUpdate {
517 name: name.to_owned(),
518 new_value: Some(RawRef::Direct(oid(1))),
519 expected: Expected::Any,
520 reflog: None,
521 flags: Default::default(),
522 };
523 let mut txn =
524 RefTransaction::from_updates(vec![update("refs/heads/a"), update("refs/heads/b")])
525 .expect("distinct names");
526 let err = txn
527 .push_update(update("refs/heads/a"))
528 .expect_err("duplicate after from_updates");
529 assert!(matches!(
530 err,
531 RefStoreError::DuplicateUpdate { name } if name == "refs/heads/a"
532 ));
533 assert_eq!(txn.updates().len(), 2);
534 }
535
536 fn push_unique_updates(n: u32) -> std::time::Duration {
537 let mut txn = RefTransaction::new();
538 let start = std::time::Instant::now();
539 for i in 0..n {
540 txn.push_update(RefUpdate {
541 name: format!("refs/heads/b{i:07}"),
542 new_value: Some(RawRef::Direct(oid((i % 256) as u8))),
543 expected: Expected::Missing,
544 reflog: None,
545 flags: Default::default(),
546 })
547 .expect("unique name");
548 }
549 start.elapsed()
550 }
551
552 #[test]
553 fn push_update_batch_build_scales_linearly() {
554 if cfg!(debug_assertions) {
555 push_unique_updates(100);
558 return;
559 }
560 let small = push_unique_updates(2_000);
561 let large = push_unique_updates(8_000);
562 let ratio = large.as_secs_f64() / small.as_secs_f64().max(1e-9);
563 assert!(
564 ratio < 12.0,
565 "expected roughly linear scaling (~4x for 4x refs), got {ratio:.1}x \
566 (small={small:?}, large={large:?})"
567 );
568 }
569
570 #[test]
571 #[ignore = "release-only sanity check for large batches (#2281); cargo test -p grit-lib --release push_update_100k -- --ignored --exact"]
572 fn push_update_100k_build_under_one_second() {
573 let elapsed = push_unique_updates(100_000);
574 assert!(
575 elapsed.as_secs() < 3,
576 "100k push_update build took {elapsed:?} (expected well under 1s in release)"
577 );
578 }
579}