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