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