1use sley_core::{DateMode, GitError, ObjectId, Result};
9use sley_strbuf_expand::{
10 AtomTable, ExpandFormat, ExpandSegment, MagicPrefix, PaddingAlign, PaddingSpec,
11};
12use std::collections::HashMap;
13use std::io::Write;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ForEachRefFormat {
17 inner: ExpandFormat<ForEachRefAtom>,
18 segments: Vec<ForEachRefFormatSegment>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum ForEachRefFormatSegment {
23 Literal(Vec<u8>),
24 Atom(ForEachRefAtom),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ForEachRefAtom {
29 Raw(String),
30 Color(String),
31 RefName {
32 source: ForEachRefNameSource,
33 format: ForEachRefNameFormat,
34 },
35 ObjectName {
36 peeled: bool,
37 abbrev: Option<usize>,
38 },
39 Identity {
40 peeled: bool,
41 role: ForEachRefAtomIdentityRole,
42 part: ForEachRefAtomIdentityPart,
43 },
44 ContentsLines {
45 peeled: bool,
46 count: usize,
47 },
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ForEachRefNameSource {
52 Ref,
53 Upstream,
54 Push,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ForEachRefNameFormat {
59 Full,
60 Short,
61 Strip(ForEachRefStrip),
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct ForEachRefStrip {
66 pub direction: ForEachRefStripDirection,
67 pub count: isize,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum ForEachRefStripDirection {
72 Left,
73 Right,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ForEachRefAtomIdentityRole {
78 Author,
79 Committer,
80 Tagger,
81 Creator,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct ForEachRefDateSort {
92 pub peeled: bool,
93 pub role: ForEachRefAtomIdentityRole,
94 pub mode: DateMode,
95 pub descending: bool,
96}
97
98pub fn parse_for_each_ref_date_sort(value: &str) -> Result<Option<ForEachRefDateSort>> {
100 let (value, descending) = value
101 .strip_prefix('-')
102 .map(|value| (value, true))
103 .unwrap_or((value, false));
104 let (value, peeled) = value
105 .strip_prefix('*')
106 .map(|value| (value, true))
107 .unwrap_or((value, false));
108 let (atom, modifier) = value
109 .split_once(':')
110 .map(|(atom, modifier)| (atom, Some(modifier)))
111 .unwrap_or((value, None));
112 let role = match atom {
113 "authordate" => ForEachRefAtomIdentityRole::Author,
114 "committerdate" => ForEachRefAtomIdentityRole::Committer,
115 "taggerdate" => ForEachRefAtomIdentityRole::Tagger,
116 "creatordate" => ForEachRefAtomIdentityRole::Creator,
117 _ => return Ok(None),
118 };
119 let mode = DateMode::parse_atom_modifier(modifier).ok_or_else(|| {
120 GitError::Command(format!(
121 "unrecognized %({atom}) argument: {}",
122 modifier.unwrap_or("")
123 ))
124 })?;
125 Ok(Some(ForEachRefDateSort {
126 peeled,
127 role,
128 mode,
129 descending,
130 }))
131}
132
133pub fn select_for_each_ref_is_base_candidate(
139 tip_first_parent_history: &[ObjectId],
140 candidate_first_parent_histories: &[Vec<ObjectId>],
141) -> Option<usize> {
142 let tip_positions = tip_first_parent_history
143 .iter()
144 .enumerate()
145 .map(|(position, oid)| (*oid, position))
146 .collect::<HashMap<_, _>>();
147
148 candidate_first_parent_histories
149 .iter()
150 .enumerate()
151 .filter_map(|(candidate, history)| {
152 history
153 .iter()
154 .filter_map(|oid| tip_positions.get(oid).copied())
155 .min()
156 .map(|tip_distance| (tip_distance, candidate))
157 })
158 .min()
159 .map(|(_, candidate)| candidate)
160}
161
162pub fn is_for_each_ref_root_ref(name: &str) -> bool {
168 let root_syntax = !name.is_empty()
169 && name
170 .bytes()
171 .all(|byte| byte.is_ascii_uppercase() || byte == b'-' || byte == b'_');
172 if !root_syntax || matches!(name, "FETCH_HEAD" | "MERGE_HEAD") {
173 return false;
174 }
175 name.ends_with("_HEAD")
176 || matches!(
177 name,
178 "HEAD"
179 | "AUTO_MERGE"
180 | "BISECT_EXPECTED_REV"
181 | "NOTES_MERGE_PARTIAL"
182 | "NOTES_MERGE_REF"
183 | "MERGE_AUTOSTASH"
184 )
185}
186
187pub fn parse_for_each_ref_hex_color(value: &str) -> Option<(u8, u8, u8)> {
189 let hex = value.strip_prefix('#')?;
190 if hex.len() != 6 {
191 return None;
192 }
193 Some((
194 u8::from_str_radix(&hex[0..2], 16).ok()?,
195 u8::from_str_radix(&hex[2..4], 16).ok()?,
196 u8::from_str_radix(&hex[4..6], 16).ok()?,
197 ))
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum ForEachRefAtomIdentityPart {
202 Full,
203 Name,
204 Email(ForEachRefEmailMode),
205 Date(DateMode),
206 DateRaw,
207}
208
209impl ForEachRefAtom {
210 fn parse(value: &str) -> Result<Self> {
211 let value = match value.split_once(':') {
216 Some((name, "")) => name,
217 _ => value,
218 };
219 if let Some(color) = value.strip_prefix("color:") {
220 return Ok(Self::Color(color.to_string()));
221 }
222 if let Some(atom) = parse_for_each_ref_refname_atom(value)? {
223 return Ok(atom);
224 }
225 if let Some(atom) = parse_for_each_ref_objectname_atom(value)? {
226 return Ok(atom);
227 }
228 if let Some(atom) = parse_for_each_ref_identity_atom(value) {
229 return Ok(atom);
230 }
231 if let Some(count) = value.strip_prefix("contents:lines=") {
232 return Ok(Self::ContentsLines {
233 peeled: false,
234 count: parse_for_each_ref_contents_lines_count(count)?,
235 });
236 }
237 if let Some(count) = value.strip_prefix("*contents:lines=") {
238 return Ok(Self::ContentsLines {
239 peeled: true,
240 count: parse_for_each_ref_contents_lines_count(count)?,
241 });
242 }
243 Ok(Self::Raw(value.to_string()))
244 }
245}
246
247struct ForEachRefAtomTable;
248
249impl AtomTable for ForEachRefAtomTable {
250 type Atom = ForEachRefAtom;
251
252 fn parse_atom(&self, value: &str) -> Result<Self::Atom> {
253 ForEachRefAtom::parse(value)
254 }
255}
256
257fn parse_for_each_ref_refname_atom(value: &str) -> Result<Option<ForEachRefAtom>> {
258 for (prefix, source) in [
259 ("refname", ForEachRefNameSource::Ref),
260 ("upstream", ForEachRefNameSource::Upstream),
261 ("push", ForEachRefNameSource::Push),
262 ] {
263 if value == prefix {
264 return Ok(Some(ForEachRefAtom::RefName {
265 source,
266 format: ForEachRefNameFormat::Full,
267 }));
268 }
269 let Some(modifier) = value
270 .strip_prefix(prefix)
271 .and_then(|value| value.strip_prefix(':'))
272 else {
273 continue;
274 };
275 let format = if modifier == "short" {
276 ForEachRefNameFormat::Short
277 } else if let Some(count) = modifier
278 .strip_prefix("lstrip=")
279 .or_else(|| modifier.strip_prefix("strip="))
280 {
281 ForEachRefNameFormat::Strip(ForEachRefStrip {
282 direction: ForEachRefStripDirection::Left,
283 count: parse_for_each_ref_strip_count(count)?,
284 })
285 } else if let Some(count) = modifier.strip_prefix("rstrip=") {
286 ForEachRefNameFormat::Strip(ForEachRefStrip {
287 direction: ForEachRefStripDirection::Right,
288 count: parse_for_each_ref_strip_count(count)?,
289 })
290 } else if prefix == "refname" {
291 eprintln!("fatal: unrecognized %({prefix}) argument: {modifier}");
295 return Err(GitError::Exit(128));
296 } else {
297 continue;
298 };
299 return Ok(Some(ForEachRefAtom::RefName { source, format }));
300 }
301 Ok(None)
302}
303
304fn parse_for_each_ref_objectname_atom(value: &str) -> Result<Option<ForEachRefAtom>> {
305 for (prefix, peeled) in [("objectname", false), ("*objectname", true)] {
306 if value == prefix {
307 return Ok(Some(ForEachRefAtom::ObjectName {
308 peeled,
309 abbrev: None,
310 }));
311 }
312 if value.strip_prefix(prefix) == Some(":short") {
313 return Ok(Some(ForEachRefAtom::ObjectName {
314 peeled,
315 abbrev: Some(0),
316 }));
317 }
318 if let Some(width) = value
319 .strip_prefix(prefix)
320 .and_then(|value| value.strip_prefix(":short="))
321 {
322 return Ok(Some(ForEachRefAtom::ObjectName {
323 peeled,
324 abbrev: Some(parse_for_each_ref_abbrev_width(width)?),
325 }));
326 }
327 }
328 Ok(None)
329}
330
331fn parse_for_each_ref_identity_atom(value: &str) -> Option<ForEachRefAtom> {
332 let (value, peeled) = value
333 .strip_prefix('*')
334 .map(|value| (value, true))
335 .unwrap_or((value, false));
336 let (atom, has_modifier) = value
337 .split_once(':')
338 .map_or((value, false), |(atom, _)| (atom, true));
339 let plain = |part: ForEachRefAtomIdentityPart| if has_modifier { None } else { Some(part) };
343 let (role, part) = match atom {
344 "author" => (
345 ForEachRefAtomIdentityRole::Author,
346 plain(ForEachRefAtomIdentityPart::Full)?,
347 ),
348 "authorname" => (
349 ForEachRefAtomIdentityRole::Author,
350 plain(ForEachRefAtomIdentityPart::Name)?,
351 ),
352 "committer" => (
353 ForEachRefAtomIdentityRole::Committer,
354 plain(ForEachRefAtomIdentityPart::Full)?,
355 ),
356 "committername" => (
357 ForEachRefAtomIdentityRole::Committer,
358 plain(ForEachRefAtomIdentityPart::Name)?,
359 ),
360 "tagger" => (
361 ForEachRefAtomIdentityRole::Tagger,
362 plain(ForEachRefAtomIdentityPart::Full)?,
363 ),
364 "taggername" => (
365 ForEachRefAtomIdentityRole::Tagger,
366 plain(ForEachRefAtomIdentityPart::Name)?,
367 ),
368 "creator" => (
369 ForEachRefAtomIdentityRole::Creator,
370 plain(ForEachRefAtomIdentityPart::Full)?,
371 ),
372 _ => return None,
373 };
374 Some(ForEachRefAtom::Identity { peeled, role, part })
375}
376
377pub fn parse_for_each_ref_contents_lines_count(value: &str) -> Result<usize> {
378 value
379 .parse::<usize>()
380 .map_err(|_| GitError::Command(format!("invalid for-each-ref contents line count {value}")))
381}
382
383impl ForEachRefFormat {
384 pub fn parse(format_spec: &str) -> Result<Self> {
385 let inner = ExpandFormat::parse(format_spec, &ForEachRefAtomTable)?;
386 let segments = inner
387 .segments()
388 .iter()
389 .filter_map(|segment| match segment {
390 ExpandSegment::Literal(literal) => {
391 Some(ForEachRefFormatSegment::Literal(literal.clone()))
392 }
393 ExpandSegment::Atom(atom) => Some(ForEachRefFormatSegment::Atom(atom.atom.clone())),
394 ExpandSegment::Padding(_) => None,
395 })
396 .collect();
397 Ok(Self { inner, segments })
398 }
399
400 pub fn segments(&self) -> &[ForEachRefFormatSegment] {
401 &self.segments
402 }
403
404 pub fn ends_with_unreset_color(&self) -> bool {
408 let mut need_reset = false;
409 for segment in &self.segments {
410 if let ForEachRefFormatSegment::Atom(ForEachRefAtom::Color(value)) = segment {
411 need_reset = value.trim() != "reset";
412 }
413 }
414 need_reset
415 }
416}
417
418pub fn write_for_each_ref_format(
419 stdout: &mut impl Write,
420 format: &ForEachRefFormat,
421 quote: ForEachRefQuoteMode,
422 reset_color_at_eol: bool,
423 mut write_atom: impl FnMut(&mut Vec<u8>, &ForEachRefAtom) -> Result<()>,
424) -> Result<()> {
425 if !format
426 .inner
427 .segments()
428 .iter()
429 .any(for_each_ref_segment_has_control)
430 {
431 format
432 .inner
433 .write_to(stdout, &mut write_atom, |stdout, value| {
434 write_for_each_ref_quoted_atom(stdout, value, quote)
435 })?;
436 if reset_color_at_eol {
437 stdout.write_all(b"\x1b[m")?;
438 }
439 return Ok(());
440 }
441
442 let mut rendered = Vec::new();
443 let (idx, stop) = write_for_each_ref_format_range(
444 &mut rendered,
445 format.inner.segments(),
446 0,
447 &[],
448 quote,
449 &mut write_atom,
450 )?;
451 if idx != format.inner.segments().len() || stop.is_some() {
452 return Err(GitError::Command(
453 "improper for-each-ref format control atom usage".into(),
454 ));
455 }
456 stdout.write_all(&rendered)?;
457 if reset_color_at_eol {
458 stdout.write_all(b"\x1b[m")?;
459 }
460 Ok(())
461}
462
463fn for_each_ref_segment_has_control(segment: &ExpandSegment<ForEachRefAtom>) -> bool {
464 match segment {
465 ExpandSegment::Atom(atom) => for_each_ref_control_atom(&atom.atom).is_some(),
466 ExpandSegment::Literal(_) | ExpandSegment::Padding(_) => false,
467 }
468}
469
470fn write_for_each_ref_format_range(
471 out: &mut Vec<u8>,
472 segments: &[ExpandSegment<ForEachRefAtom>],
473 mut idx: usize,
474 stops: &[ForEachRefControlStop],
475 quote: ForEachRefQuoteMode,
476 write_atom: &mut impl FnMut(&mut Vec<u8>, &ForEachRefAtom) -> Result<()>,
477) -> Result<(usize, Option<ForEachRefControlStop>)> {
478 let mut pending_padding = None;
479 while idx < segments.len() {
480 match &segments[idx] {
481 ExpandSegment::Literal(literal) => out.extend_from_slice(literal),
482 ExpandSegment::Padding(padding) => pending_padding = Some(*padding),
483 ExpandSegment::Atom(atom) => {
484 if let Some(control) = for_each_ref_control_atom(&atom.atom) {
485 if let Some(stop) = control.stop()
486 && stops.contains(&stop)
487 {
488 return Ok((idx, Some(stop)));
489 }
490 match control {
491 ForEachRefControlAtom::Align(options) => {
492 let (value, next) =
493 render_for_each_ref_align(segments, idx + 1, &options, write_atom)?;
494 let mut value = value;
495 apply_for_each_ref_padding(&mut value, pending_padding.take());
496 apply_for_each_ref_magic(out, atom.magic, &value);
497 write_for_each_ref_quoted_atom(out, &value, quote)?;
498 idx = next;
499 continue;
500 }
501 ForEachRefControlAtom::If(condition) => {
502 let (value, next) = render_for_each_ref_if(
503 segments,
504 idx + 1,
505 &condition,
506 quote,
507 write_atom,
508 )?;
509 let mut value = value;
510 apply_for_each_ref_padding(&mut value, pending_padding.take());
511 apply_for_each_ref_magic(out, atom.magic, &value);
512 out.extend_from_slice(&value);
513 idx = next;
514 continue;
515 }
516 ForEachRefControlAtom::Then
517 | ForEachRefControlAtom::Else
518 | ForEachRefControlAtom::End => {
519 return Err(GitError::Command(
520 "improper for-each-ref format control atom usage".into(),
521 ));
522 }
523 }
524 }
525
526 let mut value = Vec::new();
527 write_atom(&mut value, &atom.atom)?;
528 apply_for_each_ref_padding(&mut value, pending_padding.take());
529 apply_for_each_ref_magic(out, atom.magic, &value);
530 write_for_each_ref_quoted_atom(out, &value, quote)?;
531 }
532 }
533 idx += 1;
534 }
535 Ok((idx, None))
536}
537
538fn render_for_each_ref_align(
539 segments: &[ExpandSegment<ForEachRefAtom>],
540 start: usize,
541 options: &ForEachRefAlignOptions,
542 write_atom: &mut impl FnMut(&mut Vec<u8>, &ForEachRefAtom) -> Result<()>,
543) -> Result<(Vec<u8>, usize)> {
544 let mut value = Vec::new();
545 let (idx, stop) = write_for_each_ref_format_range(
546 &mut value,
547 segments,
548 start,
549 &[ForEachRefControlStop::End],
550 ForEachRefQuoteMode::None,
551 write_atom,
552 )?;
553 if stop != Some(ForEachRefControlStop::End) {
554 return Err(GitError::Command("missing %(end) atom for %(align)".into()));
555 }
556 apply_for_each_ref_align(&mut value, options);
557 Ok((value, idx + 1))
558}
559
560fn render_for_each_ref_if(
561 segments: &[ExpandSegment<ForEachRefAtom>],
562 start: usize,
563 condition: &ForEachRefIfCondition,
564 quote: ForEachRefQuoteMode,
565 write_atom: &mut impl FnMut(&mut Vec<u8>, &ForEachRefAtom) -> Result<()>,
566) -> Result<(Vec<u8>, usize)> {
567 let mut test = Vec::new();
568 let (then_idx, stop) = write_for_each_ref_format_range(
569 &mut test,
570 segments,
571 start,
572 &[ForEachRefControlStop::Then],
573 ForEachRefQuoteMode::None,
574 write_atom,
575 )?;
576 if stop != Some(ForEachRefControlStop::Then) {
577 return Err(GitError::Command("missing %(then) atom for %(if)".into()));
578 }
579
580 let mut true_value = Vec::new();
581 let (branch_idx, branch_stop) = write_for_each_ref_format_range(
582 &mut true_value,
583 segments,
584 then_idx + 1,
585 &[ForEachRefControlStop::Else, ForEachRefControlStop::End],
586 quote,
587 write_atom,
588 )?;
589
590 let mut false_value = Vec::new();
591 let end_idx = match branch_stop {
592 Some(ForEachRefControlStop::End) => branch_idx,
593 Some(ForEachRefControlStop::Else) => {
594 let (idx, stop) = write_for_each_ref_format_range(
595 &mut false_value,
596 segments,
597 branch_idx + 1,
598 &[ForEachRefControlStop::End],
599 quote,
600 write_atom,
601 )?;
602 if stop != Some(ForEachRefControlStop::End) {
603 return Err(GitError::Command("missing %(end) atom for %(if)".into()));
604 }
605 idx
606 }
607 Some(ForEachRefControlStop::Then) | None => {
608 return Err(GitError::Command("missing %(end) atom for %(if)".into()));
609 }
610 };
611
612 let test = trim_ascii(&test);
613 let matched = match condition {
614 ForEachRefIfCondition::NonEmpty => !test.is_empty(),
615 ForEachRefIfCondition::Equals(value) => test == value.as_bytes(),
616 ForEachRefIfCondition::NotEquals(value) => test != value.as_bytes(),
617 };
618 Ok((if matched { true_value } else { false_value }, end_idx + 1))
619}
620
621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
622enum ForEachRefControlStop {
623 Then,
624 Else,
625 End,
626}
627
628enum ForEachRefControlAtom {
629 Align(ForEachRefAlignOptions),
630 If(ForEachRefIfCondition),
631 Then,
632 Else,
633 End,
634}
635
636impl ForEachRefControlAtom {
637 fn stop(&self) -> Option<ForEachRefControlStop> {
638 match self {
639 Self::Then => Some(ForEachRefControlStop::Then),
640 Self::Else => Some(ForEachRefControlStop::Else),
641 Self::End => Some(ForEachRefControlStop::End),
642 Self::Align(_) | Self::If(_) => None,
643 }
644 }
645}
646
647#[derive(Clone, Copy)]
648enum ForEachRefAlignPosition {
649 Left,
650 Middle,
651 Right,
652}
653
654struct ForEachRefAlignOptions {
655 width: usize,
656 position: ForEachRefAlignPosition,
657}
658
659enum ForEachRefIfCondition {
660 NonEmpty,
661 Equals(String),
662 NotEquals(String),
663}
664
665fn for_each_ref_control_atom(atom: &ForEachRefAtom) -> Option<ForEachRefControlAtom> {
666 let ForEachRefAtom::Raw(value) = atom else {
667 return None;
668 };
669 if let Some(options) = value.strip_prefix("align:") {
670 return parse_for_each_ref_align_options(options).map(ForEachRefControlAtom::Align);
671 }
672 if value == "if" {
673 return Some(ForEachRefControlAtom::If(ForEachRefIfCondition::NonEmpty));
674 }
675 if let Some(expected) = value.strip_prefix("if:equals=") {
676 return Some(ForEachRefControlAtom::If(ForEachRefIfCondition::Equals(
677 expected.to_string(),
678 )));
679 }
680 if let Some(expected) = value.strip_prefix("if:notequals=") {
681 return Some(ForEachRefControlAtom::If(ForEachRefIfCondition::NotEquals(
682 expected.to_string(),
683 )));
684 }
685 match value.as_str() {
686 "then" => Some(ForEachRefControlAtom::Then),
687 "else" => Some(ForEachRefControlAtom::Else),
688 "end" => Some(ForEachRefControlAtom::End),
689 _ => None,
690 }
691}
692
693fn parse_for_each_ref_align_options(value: &str) -> Option<ForEachRefAlignOptions> {
694 let mut width = None;
695 let mut position = ForEachRefAlignPosition::Left;
696 for part in value.split(',') {
697 if let Some(rest) = part.strip_prefix("width=") {
698 width = rest.parse::<usize>().ok();
699 } else if let Some(rest) = part.strip_prefix("position=") {
700 position = parse_for_each_ref_align_position(rest)?;
701 } else if let Ok(parsed) = part.parse::<usize>() {
702 width = Some(parsed);
703 } else {
704 position = parse_for_each_ref_align_position(part)?;
705 }
706 }
707 Some(ForEachRefAlignOptions {
708 width: width?,
709 position,
710 })
711}
712
713fn parse_for_each_ref_align_position(value: &str) -> Option<ForEachRefAlignPosition> {
714 match value {
715 "left" => Some(ForEachRefAlignPosition::Left),
716 "middle" => Some(ForEachRefAlignPosition::Middle),
717 "right" => Some(ForEachRefAlignPosition::Right),
718 _ => None,
719 }
720}
721
722fn apply_for_each_ref_align(value: &mut Vec<u8>, options: &ForEachRefAlignOptions) {
723 let width = for_each_ref_display_width(value);
724 if width >= options.width {
725 return;
726 }
727 let extra = options.width - width;
728 let (left, right) = match options.position {
729 ForEachRefAlignPosition::Left => (0, extra),
730 ForEachRefAlignPosition::Middle => (extra / 2, extra - extra / 2),
731 ForEachRefAlignPosition::Right => (extra, 0),
732 };
733 let mut padded = Vec::with_capacity(value.len() + extra);
734 padded.extend(std::iter::repeat_n(b' ', left));
735 padded.extend_from_slice(value);
736 padded.extend(std::iter::repeat_n(b' ', right));
737 *value = padded;
738}
739
740fn apply_for_each_ref_magic(out: &mut Vec<u8>, magic: MagicPrefix, value: &[u8]) {
741 match (magic, value.is_empty()) {
742 (MagicPrefix::None, _) | (MagicPrefix::DeleteLfBeforeEmpty, _) => {}
743 (MagicPrefix::AddLfBeforeNonEmpty, false) => out.extend_from_slice(b"\n"),
744 (MagicPrefix::AddSpaceBeforeNonEmpty, false) => out.extend_from_slice(b" "),
745 (MagicPrefix::AddLfBeforeNonEmpty | MagicPrefix::AddSpaceBeforeNonEmpty, true) => {}
746 }
747 if magic == MagicPrefix::DeleteLfBeforeEmpty && value.is_empty() {
748 while out.last().copied() == Some(b'\n') {
749 out.pop();
750 }
751 }
752}
753
754fn apply_for_each_ref_padding(value: &mut Vec<u8>, padding: Option<PaddingSpec>) {
755 let Some(padding) = padding else {
756 return;
757 };
758 let width = for_each_ref_display_width(value);
759 let target = padding.width.max(0) as usize;
760 if width >= target {
761 return;
762 }
763 let extra = target - width;
764 let (left, right) = match padding.align {
765 PaddingAlign::Left => (0, extra),
766 PaddingAlign::Right | PaddingAlign::LeftAndSteal => (extra, 0),
767 PaddingAlign::Center => (extra / 2, extra - extra / 2),
768 };
769 let mut padded = Vec::with_capacity(value.len() + extra);
770 padded.extend(std::iter::repeat_n(b' ', left));
771 padded.extend_from_slice(value);
772 padded.extend(std::iter::repeat_n(b' ', right));
773 *value = padded;
774}
775
776fn for_each_ref_display_width(value: &[u8]) -> usize {
777 let mut width = 0usize;
778 let mut idx = 0usize;
779 while idx < value.len() {
780 if value[idx] == 0x1b
781 && value.get(idx + 1) == Some(&b'[')
782 && let Some(end) = value[idx + 2..]
783 .iter()
784 .position(|byte| (0x40..=0x7e).contains(byte))
785 {
786 idx += end + 3;
787 continue;
788 }
789 width += 1;
790 idx += 1;
791 }
792 width
793}
794
795fn trim_ascii(value: &[u8]) -> &[u8] {
796 let start = value
797 .iter()
798 .position(|byte| !byte.is_ascii_whitespace())
799 .unwrap_or(value.len());
800 let end = value
801 .iter()
802 .rposition(|byte| !byte.is_ascii_whitespace())
803 .map(|idx| idx + 1)
804 .unwrap_or(start);
805 &value[start..end]
806}
807
808#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
809pub enum ForEachRefQuoteMode {
810 #[default]
811 None,
812 Shell,
813 Python,
814 Perl,
815 Tcl,
816}
817
818pub fn write_for_each_ref_quoted_atom(
819 stdout: &mut impl Write,
820 value: &[u8],
821 quote: ForEachRefQuoteMode,
822) -> Result<()> {
823 match quote {
824 ForEachRefQuoteMode::None => stdout.write_all(value)?,
825 ForEachRefQuoteMode::Shell => {
826 stdout.write_all(b"'")?;
827 for byte in value {
828 if *byte == b'\'' {
829 stdout.write_all(br#"'\''"#)?;
830 } else {
831 stdout.write_all(&[*byte])?;
832 }
833 }
834 stdout.write_all(b"'")?;
835 }
836 ForEachRefQuoteMode::Python | ForEachRefQuoteMode::Perl => {
837 stdout.write_all(b"'")?;
838 for byte in value {
839 match (*byte, quote) {
840 (b'\\', _) => stdout.write_all(br#"\\"#)?,
841 (b'\'', _) => stdout.write_all(br#"\'"#)?,
842 (b'\n', ForEachRefQuoteMode::Python) => stdout.write_all(br#"\n"#)?,
843 _ => stdout.write_all(&[*byte])?,
844 }
845 }
846 stdout.write_all(b"'")?;
847 }
848 ForEachRefQuoteMode::Tcl => {
849 stdout.write_all(b"\"")?;
850 for byte in value {
851 match *byte {
852 b'\\' => stdout.write_all(br#"\\"#)?,
853 b'"' => stdout.write_all(br#"\""#)?,
854 b'\n' => stdout.write_all(br#"\n"#)?,
855 _ => stdout.write_all(&[*byte])?,
856 }
857 }
858 stdout.write_all(b"\"")?;
859 }
860 }
861 Ok(())
862}
863
864#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
865pub struct ForEachRefTrack {
866 pub ahead: usize,
867 pub behind: usize,
868 pub gone: bool,
871}
872
873#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
874pub enum ForEachRefEmailMode {
875 #[default]
876 Bracketed,
877 Trim,
878 LocalPart,
879}
880
881pub fn write_for_each_ref_track(
882 stdout: &mut impl Write,
883 track: ForEachRefTrack,
884 bracketed: bool,
885) -> Result<()> {
886 if track.gone {
887 if bracketed {
890 stdout.write_all(b"[gone]")?;
891 } else {
892 stdout.write_all(b"gone")?;
893 }
894 return Ok(());
895 }
896 if bracketed && (track.ahead > 0 || track.behind > 0) {
897 stdout.write_all(b"[")?;
898 }
899 match (track.ahead, track.behind) {
900 (0, _) => {}
901 (ahead, 0) => write!(stdout, "ahead {ahead}")?,
902 (ahead, behind) => write!(stdout, "ahead {ahead}, behind {behind}")?,
903 }
904 if track.ahead == 0 && track.behind > 0 {
905 write!(stdout, "behind {}", track.behind)?;
906 }
907 if bracketed && (track.ahead > 0 || track.behind > 0) {
908 stdout.write_all(b"]")?;
909 }
910 Ok(())
911}
912
913pub fn for_each_ref_track_short(track: ForEachRefTrack) -> &'static str {
914 if track.gone {
915 return "";
917 }
918 match (track.ahead, track.behind) {
919 (0, 0) => "=",
920 (_, 0) => ">",
921 (0, _) => "<",
922 (_, _) => "<>",
923 }
924}
925
926pub fn write_for_each_ref_identity(stdout: &mut impl Write, identity: Option<&[u8]>) -> Result<()> {
927 if let Some(identity) = identity {
928 stdout.write_all(identity)?;
929 }
930 Ok(())
931}
932
933pub fn write_for_each_ref_identity_name(
934 stdout: &mut impl Write,
935 identity: Option<&[u8]>,
936) -> Result<()> {
937 if let Some(identity) = identity
938 && let Some(name) = for_each_ref_identity_name(identity)
939 {
940 stdout.write_all(name)?;
941 }
942 Ok(())
943}
944
945pub fn write_for_each_ref_identity_email(
946 stdout: &mut impl Write,
947 identity: Option<&[u8]>,
948) -> Result<()> {
949 write_for_each_ref_identity_email_mode(stdout, identity, ForEachRefEmailMode::Bracketed)
950}
951
952pub fn write_for_each_ref_identity_email_mode(
953 stdout: &mut impl Write,
954 identity: Option<&[u8]>,
955 mode: ForEachRefEmailMode,
956) -> Result<()> {
957 if let Some(identity) = identity
958 && let Some(email) = for_each_ref_identity_email(identity, mode)
959 {
960 stdout.write_all(email)?;
961 }
962 Ok(())
963}
964
965pub fn write_for_each_ref_identity_date_raw(
966 stdout: &mut impl Write,
967 identity: Option<&[u8]>,
968) -> Result<()> {
969 if let Some(identity) = identity
970 && let Some(date) = for_each_ref_identity_date_raw(identity)
971 {
972 stdout.write_all(date)?;
973 }
974 Ok(())
975}
976
977pub fn write_for_each_ref_identity_date(
978 stdout: &mut impl Write,
979 identity: Option<&[u8]>,
980) -> Result<()> {
981 write_for_each_ref_identity_date_mode(stdout, identity, &DateMode::Default)
982}
983
984pub fn write_for_each_ref_identity_date_mode(
985 stdout: &mut impl Write,
986 identity: Option<&[u8]>,
987 mode: &DateMode,
988) -> Result<()> {
989 if let Some(identity) = identity
990 && let Some(date) = for_each_ref_identity_date(identity, mode)
991 {
992 stdout.write_all(date.as_bytes())?;
993 }
994 Ok(())
995}
996
997pub fn for_each_ref_identity_name(identity: &[u8]) -> Option<&[u8]> {
998 let marker = identity.windows(2).position(|window| window == b" <")?;
999 Some(&identity[..marker])
1000}
1001
1002pub fn for_each_ref_identity_email(identity: &[u8], mode: ForEachRefEmailMode) -> Option<&[u8]> {
1003 let start = identity.iter().position(|byte| *byte == b'<')?;
1004 let end = identity[start..].iter().position(|byte| *byte == b'>')?;
1005 let bracketed = &identity[start..=start + end];
1006 match mode {
1007 ForEachRefEmailMode::Bracketed => Some(bracketed),
1008 ForEachRefEmailMode::Trim => Some(&identity[start + 1..start + end]),
1009 ForEachRefEmailMode::LocalPart => {
1010 let trimmed = &identity[start + 1..start + end];
1011 let at = trimmed.iter().position(|byte| *byte == b'@')?;
1012 Some(&trimmed[..at])
1013 }
1014 }
1015}
1016
1017pub fn for_each_ref_identity_date_raw(identity: &[u8]) -> Option<&[u8]> {
1018 let fields = sley_core::split_ident_line(identity)?;
1021 let date = fields.date?;
1022 let tz = fields.tz?;
1023 let base = identity.as_ptr() as usize;
1024 let start = date.as_ptr() as usize - base;
1025 let end = (tz.as_ptr() as usize - base) + tz.len();
1026 Some(&identity[start..end])
1027}
1028
1029pub fn for_each_ref_identity_date(identity: &[u8], mode: &DateMode) -> Option<String> {
1030 let fields = sley_core::split_ident_line(identity)?;
1034 let date = fields.date?;
1035 let tz = fields.tz.unwrap_or(b"+0000");
1036 Some(sley_core::ident_render_date(date, tz, mode))
1037}
1038
1039pub fn for_each_ref_identity_timestamp(identity: &[u8]) -> Option<i64> {
1040 let fields = sley_core::split_ident_line(identity)?;
1041 let date = fields.date?;
1042 std::str::from_utf8(date).ok()?.parse::<i64>().ok()
1043}
1044
1045const FOR_EACH_REF_SIGNATURE_MARKERS: [&[u8]; 4] = [
1048 b"-----BEGIN PGP SIGNATURE-----",
1049 b"-----BEGIN PGP MESSAGE-----",
1050 b"-----BEGIN SIGNED MESSAGE-----",
1051 b"-----BEGIN SSH SIGNATURE-----",
1052];
1053
1054fn for_each_ref_signature_start(message: &[u8]) -> usize {
1058 let mut start = 0;
1059 let mut sig = message.len();
1060 while start < message.len() {
1061 let line = &message[start..];
1062 if FOR_EACH_REF_SIGNATURE_MARKERS
1063 .iter()
1064 .any(|marker| line.starts_with(marker))
1065 {
1066 sig = start;
1067 }
1068 match line.iter().position(|byte| *byte == b'\n') {
1069 Some(eol) => start += eol + 1,
1070 None => break,
1071 }
1072 }
1073 sig
1074}
1075
1076pub struct ForEachRefMessageParts<'a> {
1079 pub subject: &'a [u8],
1082 pub body_without_sig: &'a [u8],
1084 pub body_with_sig: &'a [u8],
1086 pub signature: &'a [u8],
1088 pub bare: &'a [u8],
1091}
1092
1093pub fn for_each_ref_message_parts(message: &[u8]) -> ForEachRefMessageParts<'_> {
1097 let mut start = 0;
1099 while message.get(start) == Some(&b'\n') {
1100 start += 1;
1101 }
1102 let buf = &message[start..];
1103 let bare = buf;
1104 let sigstart = for_each_ref_signature_start(buf);
1105 let signature = &buf[sigstart..];
1106
1107 let subject_region = &buf[..sigstart];
1110 let subject_end = for_each_ref_blank_line(subject_region).unwrap_or(sigstart);
1111 let mut sublen = subject_end;
1112 while sublen > 0 && matches!(buf[sublen - 1], b'\n' | b'\r') {
1113 sublen -= 1;
1114 }
1115 let subject = &buf[..sublen];
1116
1117 let mut body_start = subject_end;
1119 while body_start < buf.len() && matches!(buf[body_start], b'\n' | b'\r') {
1120 body_start += 1;
1121 }
1122 let body_with_sig = &buf[body_start..];
1123 let body_without_sig = &buf[body_start..sigstart.max(body_start)];
1124 ForEachRefMessageParts {
1125 subject,
1126 body_without_sig,
1127 body_with_sig,
1128 signature,
1129 bare,
1130 }
1131}
1132
1133fn for_each_ref_blank_line(buf: &[u8]) -> Option<usize> {
1136 let lf = buf.windows(2).position(|window| window == b"\n\n");
1137 let crlf = buf.windows(4).position(|window| window == b"\r\n\r\n");
1138 match (lf, crlf) {
1139 (Some(a), Some(b)) => Some(a.min(b)),
1140 (Some(a), None) => Some(a),
1141 (None, Some(b)) => Some(b),
1142 (None, None) => None,
1143 }
1144}
1145
1146pub fn for_each_ref_copy_subject(subject: &[u8]) -> String {
1149 let mut out = String::with_capacity(subject.len());
1150 let mut idx = 0;
1151 while idx < subject.len() {
1152 let byte = subject[idx];
1153 if byte == b'\r' && subject.get(idx + 1) == Some(&b'\n') {
1154 idx += 1;
1155 continue;
1156 }
1157 if byte == b'\n' {
1158 out.push(' ');
1159 } else {
1160 out.push(byte as char);
1161 }
1162 idx += 1;
1163 }
1164 out
1165}
1166
1167pub fn for_each_ref_sanitize_subject(subject: &str) -> String {
1170 let bytes = subject.as_bytes();
1171 let mut out = Vec::with_capacity(bytes.len());
1172 let mut space = 2u8; let mut idx = 0;
1174 while idx < bytes.len() {
1175 let byte = bytes[idx];
1176 if for_each_ref_istitlechar(byte) {
1177 if space == 1 {
1178 out.push(b'-');
1179 }
1180 space = 0;
1181 out.push(byte);
1182 if byte == b'.' {
1183 while bytes.get(idx + 1) == Some(&b'.') {
1184 idx += 1;
1185 }
1186 }
1187 } else {
1188 space |= 1;
1189 }
1190 idx += 1;
1191 }
1192 while matches!(out.last(), Some(b'.') | Some(b'-')) {
1193 out.pop();
1194 }
1195 String::from_utf8_lossy(&out).into_owned()
1196}
1197
1198fn for_each_ref_istitlechar(byte: u8) -> bool {
1199 byte.is_ascii_alphanumeric() || byte == b'.' || byte == b'_'
1200}
1201
1202pub fn for_each_ref_short_name(refname: &str) -> &str {
1203 if let Some(remote) = refname.strip_prefix("refs/remotes/")
1204 && let Some(remote_name) = remote.strip_suffix("/HEAD")
1205 {
1206 return remote_name;
1207 }
1208 refname
1209 .strip_prefix("refs/heads/")
1210 .or_else(|| refname.strip_prefix("refs/tags/"))
1211 .or_else(|| refname.strip_prefix("refs/remotes/"))
1212 .unwrap_or(refname)
1213}
1214
1215const REF_REV_PARSE_RULES: [&str; 6] = [
1218 "{}",
1219 "refs/{}",
1220 "refs/tags/{}",
1221 "refs/heads/{}",
1222 "refs/remotes/{}",
1223 "refs/remotes/{}/HEAD",
1224];
1225
1226fn expand_ref_rule(rule: &str, short: &str) -> String {
1227 rule.replace("{}", short)
1228}
1229
1230fn match_ref_parse_rule<'a>(refname: &'a str, rule: &str) -> Option<&'a str> {
1233 let (prefix, suffix) = rule.split_once("{}").expect("rule contains {}");
1234 refname
1235 .strip_prefix(prefix)
1236 .and_then(|rest| rest.strip_suffix(suffix))
1237}
1238
1239pub fn shorten_unambiguous_ref(
1245 refname: &str,
1246 strict: bool,
1247 ref_exists: impl Fn(&str) -> bool,
1248) -> String {
1249 for matched in (1..REF_REV_PARSE_RULES.len()).rev() {
1251 let Some(short) = match_ref_parse_rule(refname, REF_REV_PARSE_RULES[matched]) else {
1252 continue;
1253 };
1254 let rules_to_fail = if strict {
1255 REF_REV_PARSE_RULES.len()
1256 } else {
1257 matched
1258 };
1259 let ambiguous = (0..rules_to_fail).any(|rule_idx| {
1260 rule_idx != matched
1261 && ref_exists(&expand_ref_rule(REF_REV_PARSE_RULES[rule_idx], short))
1262 });
1263 if !ambiguous {
1264 return short.to_string();
1265 }
1266 }
1267 refname.to_string()
1268}
1269
1270pub fn parse_for_each_ref_strip_count(value: &str) -> Result<isize> {
1271 value
1272 .parse::<isize>()
1273 .map_err(|_| GitError::Command(format!("invalid refname strip count {value}")))
1274}
1275
1276pub fn for_each_ref_lstrip_name(refname: &str, count: isize) -> String {
1277 let components = refname.split('/').collect::<Vec<_>>();
1278 if count == 0 {
1279 return refname.to_string();
1280 }
1281 let start = if count > 0 {
1282 (count as usize).min(components.len())
1283 } else {
1284 components.len().saturating_sub(count.unsigned_abs())
1285 };
1286 components[start..].join("/")
1287}
1288
1289pub fn for_each_ref_rstrip_name(refname: &str, count: isize) -> String {
1290 let components = refname.split('/').collect::<Vec<_>>();
1291 if count == 0 {
1292 return refname.to_string();
1293 }
1294 let end = if count > 0 {
1295 components.len().saturating_sub(count as usize)
1296 } else {
1297 count.unsigned_abs().min(components.len())
1298 };
1299 components[..end].join("/")
1300}
1301
1302pub fn for_each_ref_abbrev_oid(
1303 oid: &ObjectId,
1304 width: Option<usize>,
1305 candidates: &[ObjectId],
1306) -> String {
1307 let hex = oid.to_hex();
1308 let mut width = oid.abbrev_hex_len(width.unwrap_or(hex.len()));
1309 while width < hex.len() {
1310 let prefix = &hex.as_bytes()[..width];
1311 if !candidates
1312 .iter()
1313 .any(|candidate| candidate != oid && candidate.hex_prefix_matches(prefix))
1314 {
1315 break;
1316 }
1317 width += 1;
1318 }
1319 hex[..width].to_string()
1320}
1321
1322pub fn parse_for_each_ref_abbrev_width(value: &str) -> Result<usize> {
1323 let width = value
1324 .parse::<usize>()
1325 .ok()
1326 .filter(|width| *width > 0)
1327 .ok_or_else(|| {
1328 GitError::Command(format!(
1329 "positive value expected in for-each-ref objectname:short format: {value}"
1330 ))
1331 })?;
1332 Ok(width.max(4))
1333}
1334
1335pub fn commit_identity_date(raw: &[u8], mode: &DateMode) -> String {
1336 for_each_ref_identity_date(raw, mode).unwrap_or_default()
1337}
1338
1339pub fn commit_identity_date_or_sentinel(raw: &[u8], mode: &DateMode) -> String {
1346 match sley_core::split_ident_line(raw) {
1347 Some(fields) => {
1348 let date = fields.date.unwrap_or(b"0");
1349 let tz = fields.tz.unwrap_or(b"+0000");
1350 sley_core::ident_render_date(date, tz, mode)
1351 }
1352 None => sley_core::ident_render_date(b"0", b"+0000", mode),
1356 }
1357}
1358
1359#[cfg(test)]
1360mod tests {
1361 use super::*;
1362 use sley_core::ObjectFormat;
1363
1364 #[test]
1365 fn ref_filter_root_refs_exclude_pseudorefs() {
1366 for name in ["HEAD", "ORIG_HEAD", "AUTO_MERGE", "BISECT_EXPECTED_REV"] {
1367 assert!(is_for_each_ref_root_ref(name), "{name}");
1368 }
1369 for name in ["FETCH_HEAD", "MERGE_HEAD", "DANGLING", "refs/heads/main"] {
1370 assert!(!is_for_each_ref_root_ref(name), "{name}");
1371 }
1372 }
1373
1374 #[test]
1375 fn ref_filter_hex_color_requires_six_hex_digits() {
1376 assert_eq!(
1377 parse_for_each_ref_hex_color("#aa22ac"),
1378 Some((0xaa, 0x22, 0xac))
1379 );
1380 assert_eq!(
1381 parse_for_each_ref_hex_color("#AA22AC"),
1382 Some((0xaa, 0x22, 0xac))
1383 );
1384 assert_eq!(parse_for_each_ref_hex_color("#abc"), None);
1385 assert_eq!(parse_for_each_ref_hex_color("#gg22ac"), None);
1386 }
1387
1388 #[test]
1389 fn format_parser_decodes_literals_atoms_and_percent_escapes() {
1390 let format =
1391 ForEachRefFormat::parse("refs/%%/%(refname)%09%(objectname)%q").expect("valid format");
1392 assert_eq!(
1393 format.segments(),
1394 &[
1395 ForEachRefFormatSegment::Literal(b"refs/%/".to_vec()),
1396 ForEachRefFormatSegment::Atom(ForEachRefAtom::RefName {
1397 source: ForEachRefNameSource::Ref,
1398 format: ForEachRefNameFormat::Full
1399 }),
1400 ForEachRefFormatSegment::Literal(b"\t".to_vec()),
1401 ForEachRefFormatSegment::Atom(ForEachRefAtom::ObjectName {
1402 peeled: false,
1403 abbrev: None
1404 }),
1405 ForEachRefFormatSegment::Literal(b"%q".to_vec()),
1406 ]
1407 );
1408 }
1409
1410 #[test]
1411 fn format_parser_decodes_typed_ref_filter_atoms() {
1412 let format = ForEachRefFormat::parse(
1413 "%(refname:short) %(upstream:lstrip=2) %(*objectname:short=7) %(authoremail:trim) %(authordate:iso8601-strict) %(*contents:lines=2)",
1414 )
1415 .expect("valid format");
1416 assert_eq!(
1417 format.segments(),
1418 &[
1419 ForEachRefFormatSegment::Atom(ForEachRefAtom::RefName {
1420 source: ForEachRefNameSource::Ref,
1421 format: ForEachRefNameFormat::Short,
1422 }),
1423 ForEachRefFormatSegment::Literal(b" ".to_vec()),
1424 ForEachRefFormatSegment::Atom(ForEachRefAtom::RefName {
1425 source: ForEachRefNameSource::Upstream,
1426 format: ForEachRefNameFormat::Strip(ForEachRefStrip {
1427 direction: ForEachRefStripDirection::Left,
1428 count: 2,
1429 }),
1430 }),
1431 ForEachRefFormatSegment::Literal(b" ".to_vec()),
1432 ForEachRefFormatSegment::Atom(ForEachRefAtom::ObjectName {
1433 peeled: true,
1434 abbrev: Some(7),
1435 }),
1436 ForEachRefFormatSegment::Literal(b" ".to_vec()),
1437 ForEachRefFormatSegment::Atom(ForEachRefAtom::Raw("authoremail:trim".to_string())),
1442 ForEachRefFormatSegment::Literal(b" ".to_vec()),
1443 ForEachRefFormatSegment::Atom(ForEachRefAtom::Raw(
1444 "authordate:iso8601-strict".to_string(),
1445 )),
1446 ForEachRefFormatSegment::Literal(b" ".to_vec()),
1447 ForEachRefFormatSegment::Atom(ForEachRefAtom::ContentsLines {
1448 peeled: true,
1449 count: 2,
1450 }),
1451 ]
1452 );
1453 }
1454
1455 #[test]
1456 fn format_parser_rejects_unterminated_atoms() {
1457 assert!(ForEachRefFormat::parse("%(refname").is_err());
1458 }
1459
1460 #[test]
1461 fn format_parser_rejects_invalid_typed_atom_numbers() {
1462 assert!(ForEachRefFormat::parse("%(contents:lines=nope)").is_err());
1463 assert!(ForEachRefFormat::parse("%(objectname:short=0)").is_err());
1464 assert!(ForEachRefFormat::parse("%(refname:lstrip=nope)").is_err());
1465 }
1466
1467 #[test]
1468 fn format_renderer_streams_literals_atoms_and_quotes() {
1469 let format = ForEachRefFormat::parse("branch=%(refname)").expect("valid format");
1470 let mut out = Vec::new();
1471 write_for_each_ref_format(
1472 &mut out,
1473 &format,
1474 ForEachRefQuoteMode::Shell,
1475 false,
1476 |atom, name| {
1477 assert_eq!(
1478 name,
1479 &ForEachRefAtom::RefName {
1480 source: ForEachRefNameSource::Ref,
1481 format: ForEachRefNameFormat::Full
1482 }
1483 );
1484 atom.extend_from_slice(b"main's");
1485 Ok(())
1486 },
1487 )
1488 .expect("writes to in-memory buffer");
1489 assert_eq!(out, b"branch='main'\\''s'");
1490 }
1491
1492 #[test]
1493 fn format_renderer_uses_shared_padding_and_magic() {
1494 let format =
1495 ForEachRefFormat::parse("x\n%-(*objectname)%>(6)%(refname)").expect("valid format");
1496 let mut out = Vec::new();
1497 write_for_each_ref_format(
1498 &mut out,
1499 &format,
1500 ForEachRefQuoteMode::None,
1501 false,
1502 |value, atom| {
1503 match atom {
1504 ForEachRefAtom::ObjectName { peeled: true, .. } => {}
1505 ForEachRefAtom::RefName { .. } => value.extend_from_slice(b"main"),
1506 other => panic!("unexpected atom {other:?}"),
1507 }
1508 Ok(())
1509 },
1510 )
1511 .expect("writes to in-memory buffer");
1512 assert_eq!(out, b"x main");
1513 }
1514
1515 #[test]
1516 fn identity_parts_match_git_identity_layout() {
1517 let ident = b"Ada Lovelace <ada@example.com> 1717430401 -0530";
1518 assert_eq!(
1519 for_each_ref_identity_name(ident),
1520 Some(&b"Ada Lovelace"[..])
1521 );
1522 assert_eq!(
1523 for_each_ref_identity_email(ident, ForEachRefEmailMode::Bracketed),
1524 Some(&b"<ada@example.com>"[..])
1525 );
1526 assert_eq!(
1527 for_each_ref_identity_email(ident, ForEachRefEmailMode::Trim),
1528 Some(&b"ada@example.com"[..])
1529 );
1530 assert_eq!(
1531 for_each_ref_identity_email(ident, ForEachRefEmailMode::LocalPart),
1532 Some(&b"ada"[..])
1533 );
1534 assert_eq!(for_each_ref_identity_timestamp(ident), Some(1717430401));
1535 assert_eq!(
1536 for_each_ref_identity_date(ident, &DateMode::Raw).as_deref(),
1537 Some("1717430401 -0530")
1538 );
1539 }
1540
1541 #[test]
1542 fn dates_use_identity_timezone() {
1543 let ident = b"Ada <ada@example.com> 1717430401 -0530";
1544 assert_eq!(
1545 for_each_ref_identity_date(ident, &DateMode::Short).as_deref(),
1546 Some("2024-06-03")
1547 );
1548 assert_eq!(
1549 for_each_ref_identity_date(ident, &DateMode::IsoStrict).as_deref(),
1550 Some("2024-06-03T10:30:01-05:30")
1551 );
1552 }
1553
1554 #[test]
1555 fn date_sort_parser_preserves_custom_format_semantics() {
1556 let sort = parse_for_each_ref_date_sort("-*creatordate:format:%H:%M:%S")
1557 .expect("valid date sort")
1558 .expect("recognized date atom");
1559 assert!(sort.peeled);
1560 assert!(sort.descending);
1561 assert_eq!(sort.role, ForEachRefAtomIdentityRole::Creator);
1562 assert_eq!(
1563 sort.mode,
1564 DateMode::Strftime {
1565 template: "%H:%M:%S".to_string(),
1566 local: false,
1567 }
1568 );
1569 assert!(
1570 parse_for_each_ref_date_sort("refname")
1571 .expect("non-date sort is not an error")
1572 .is_none()
1573 );
1574 }
1575
1576 #[test]
1577 fn is_base_selection_minimizes_tip_first_parent_distance_and_keeps_ref_order() {
1578 let oid =
1579 |hex: &str| ObjectId::from_hex(ObjectFormat::Sha1, hex).expect("valid test object id");
1580 let root = oid("0000000000000000000000000000000000000001");
1581 let near = oid("0000000000000000000000000000000000000002");
1582 let tip = oid("0000000000000000000000000000000000000003");
1583 let left = oid("0000000000000000000000000000000000000004");
1584 let right = oid("0000000000000000000000000000000000000005");
1585 let histories = vec![vec![left, near, root], vec![right, near, root], vec![root]];
1586 assert_eq!(
1587 select_for_each_ref_is_base_candidate(&[tip, near, root], &histories),
1588 Some(0),
1589 "nearest intersection wins and the first candidate breaks a tie"
1590 );
1591 assert_eq!(
1592 select_for_each_ref_is_base_candidate(&[tip], &histories),
1593 None
1594 );
1595 }
1596
1597 #[test]
1598 fn tracking_formats_match_ref_filter_atoms() {
1599 assert_eq!(
1600 for_each_ref_track_short(ForEachRefTrack {
1601 ahead: 0,
1602 behind: 0,
1603 gone: false,
1604 }),
1605 "="
1606 );
1607 assert_eq!(
1608 for_each_ref_track_short(ForEachRefTrack {
1609 ahead: 1,
1610 behind: 0,
1611 gone: false,
1612 }),
1613 ">"
1614 );
1615 assert_eq!(
1616 for_each_ref_track_short(ForEachRefTrack {
1617 ahead: 0,
1618 behind: 1,
1619 gone: false,
1620 }),
1621 "<"
1622 );
1623 assert_eq!(
1624 for_each_ref_track_short(ForEachRefTrack {
1625 ahead: 1,
1626 behind: 1,
1627 gone: false,
1628 }),
1629 "<>"
1630 );
1631
1632 let mut out = Vec::new();
1633 write_for_each_ref_track(
1634 &mut out,
1635 ForEachRefTrack {
1636 ahead: 2,
1637 behind: 3,
1638 gone: false,
1639 },
1640 true,
1641 )
1642 .expect("writes to in-memory buffer");
1643 assert_eq!(out, b"[ahead 2, behind 3]");
1644 }
1645
1646 #[test]
1647 fn refname_shortening_and_stripping_match_ref_filter_rules() {
1648 assert_eq!(for_each_ref_short_name("refs/heads/main"), "main");
1649 assert_eq!(for_each_ref_short_name("refs/tags/v1"), "v1");
1650 assert_eq!(
1651 for_each_ref_short_name("refs/remotes/origin/HEAD"),
1652 "origin"
1653 );
1654 assert_eq!(for_each_ref_lstrip_name("refs/heads/main", 2), "main");
1655 assert_eq!(for_each_ref_lstrip_name("refs/heads/main", -1), "main");
1656 assert_eq!(for_each_ref_rstrip_name("refs/heads/main", 1), "refs/heads");
1657 assert_eq!(
1658 for_each_ref_rstrip_name("refs/heads/main", -2),
1659 "refs/heads"
1660 );
1661 }
1662
1663 #[test]
1664 fn abbreviations_extend_to_avoid_ambiguity() {
1665 let one = ObjectId::from_hex(
1666 ObjectFormat::Sha1,
1667 "1111111111111111111111111111111111111111",
1668 )
1669 .expect("valid object id");
1670 let two = ObjectId::from_hex(
1671 ObjectFormat::Sha1,
1672 "1111122222222222222222222222222222222222",
1673 )
1674 .expect("valid object id");
1675 assert_eq!(
1676 parse_for_each_ref_abbrev_width("2").expect("valid abbrev width"),
1677 4
1678 );
1679 assert_eq!(
1680 for_each_ref_abbrev_oid(&one, Some(4), &[one.clone(), two]),
1681 "111111"
1682 );
1683 }
1684}