1use super::context::{ForEachRefFormatContext, ForEachRefSignatureVerification};
5use super::{
6 ForEachRefAtom, ForEachRefAtomIdentityPart, ForEachRefAtomIdentityRole, ForEachRefEmailMode,
7 ForEachRefNameFormat, ForEachRefNameSource, ForEachRefStripDirection,
8 for_each_ref_abbrev_oid, for_each_ref_identity_date, for_each_ref_identity_email,
9 parse_for_each_ref_abbrev_width, parse_for_each_ref_hex_color,
10 write_for_each_ref_identity, write_for_each_ref_identity_date_mode,
11 write_for_each_ref_identity_date_raw, write_for_each_ref_identity_email_mode,
12 write_for_each_ref_identity_name,
13};
14use sley_core::{DateMode, GitError, Result};
15use std::io::Write;
16
17pub fn write_for_each_ref_signature(
18 stdout: &mut impl Write,
19 verification: &dyn ForEachRefSignatureVerification,
20 option: &str,
21) -> Result<()> {
22 match option.strip_prefix(':').unwrap_or("") {
23 "" => stdout.write_all(verification.bare_output())?,
25 "grade" => stdout.write_all(&[verification.grade_byte()])?,
28 "key" => stdout.write_all(verification.key().as_bytes())?,
29 "signer" => stdout.write_all(verification.signer().as_bytes())?,
30 "fingerprint" => stdout.write_all(verification.fingerprint().as_bytes())?,
31 "primarykeyfingerprint" => {
32 stdout.write_all(verification.primary_fingerprint().as_bytes())?
33 }
34 "trustlevel" => stdout.write_all(verification.trust().as_bytes())?,
35 _ => {}
36 }
37 Ok(())
38}
39
40pub fn for_each_ref_typed_refname<'a>(
41 context: &'a ForEachRefFormatContext<'_>,
42 source: ForEachRefNameSource,
43) -> &'a str {
44 match source {
45 ForEachRefNameSource::Ref => context.refname,
46 ForEachRefNameSource::Upstream => context
47 .upstream
48 .as_ref()
49 .map(|upstream| upstream.refname.as_str())
50 .unwrap_or(""),
51 ForEachRefNameSource::Push => context
52 .push
53 .as_ref()
54 .and_then(|push| push.refname.as_deref())
55 .unwrap_or(""),
56 }
57}
58
59pub fn for_each_ref_typed_identity<'a>(
60 context: &'a ForEachRefFormatContext<'_>,
61 peeled: bool,
62 role: ForEachRefAtomIdentityRole,
63) -> Option<&'a [u8]> {
64 if peeled {
65 let peeled = context.peeled_object.as_ref();
66 return match role {
67 ForEachRefAtomIdentityRole::Author => {
68 peeled.and_then(|peeled| peeled.author.as_deref())
69 }
70 ForEachRefAtomIdentityRole::Committer => {
71 peeled.and_then(|peeled| peeled.committer.as_deref())
72 }
73 ForEachRefAtomIdentityRole::Tagger => None,
74 ForEachRefAtomIdentityRole::Creator => {
75 peeled.and_then(|peeled| peeled.creator.as_deref())
76 }
77 };
78 }
79
80 let contents = context.contents.as_ref();
81 match role {
82 ForEachRefAtomIdentityRole::Author => {
83 contents.and_then(|contents| contents.author.as_deref())
84 }
85 ForEachRefAtomIdentityRole::Committer => {
86 contents.and_then(|contents| contents.committer.as_deref())
87 }
88 ForEachRefAtomIdentityRole::Tagger => {
89 contents.and_then(|contents| contents.tagger.as_deref())
90 }
91 ForEachRefAtomIdentityRole::Creator => {
92 contents.and_then(|contents| contents.creator.as_deref())
93 }
94 }
95}
96
97pub fn write_for_each_ref_typed_atom(
98 stdout: &mut impl Write,
99 atom: &ForEachRefAtom,
100 context: &ForEachRefFormatContext<'_>,
101) -> Result<()> {
102 match atom {
103 ForEachRefAtom::Raw(_) => unreachable!("raw atoms are handled by the compatibility path"),
104 ForEachRefAtom::Color(value) => {
105 let color = for_each_ref_color_escape(value)?;
106 if context.color {
107 stdout.write_all(color.as_bytes())?;
108 }
109 }
110 ForEachRefAtom::RefName { source, format } => {
111 let refname = for_each_ref_typed_refname(context, *source);
112 match format {
113 ForEachRefNameFormat::Full => stdout.write_all(refname.as_bytes())?,
114 ForEachRefNameFormat::Short => {
115 stdout.write_all(context.shorten_ref(refname).as_bytes())?
116 }
117 ForEachRefNameFormat::Strip(strip) => {
118 let refname = match strip.direction {
119 ForEachRefStripDirection::Left => {
120 super::for_each_ref_lstrip_name(refname, strip.count)
121 }
122 ForEachRefStripDirection::Right => {
123 super::for_each_ref_rstrip_name(refname, strip.count)
124 }
125 };
126 stdout.write_all(refname.as_bytes())?;
127 }
128 }
129 }
130 ForEachRefAtom::ObjectName { peeled, abbrev } => {
131 let oid = if *peeled {
132 context.peeled_object.as_ref().map(|peeled| &peeled.oid)
133 } else {
134 Some(context.oid)
135 };
136 if let Some(oid) = oid {
137 match abbrev {
138 None => write!(stdout, "{oid}")?,
139 Some(0) => stdout.write_all(
140 for_each_ref_abbrev_oid(
141 oid,
142 context.objectname_abbrev,
143 context.objectname_candidates,
144 )
145 .as_bytes(),
146 )?,
147 Some(width) => stdout.write_all(
148 for_each_ref_abbrev_oid(oid, Some(*width), context.objectname_candidates)
149 .as_bytes(),
150 )?,
151 }
152 }
153 }
154 ForEachRefAtom::Identity { peeled, role, part } => {
155 let identity = for_each_ref_typed_identity(context, *peeled, *role);
156 match part {
157 ForEachRefAtomIdentityPart::Full => write_for_each_ref_identity(stdout, identity)?,
158 ForEachRefAtomIdentityPart::Name => {
159 write_for_each_ref_identity_name(stdout, identity)?
160 }
161 ForEachRefAtomIdentityPart::Email(mode) => {
162 write_for_each_ref_identity_email_mode(stdout, identity, *mode)?
163 }
164 ForEachRefAtomIdentityPart::Date(mode) => {
165 write_for_each_ref_identity_date_mode(stdout, identity, mode)?
166 }
167 ForEachRefAtomIdentityPart::DateRaw => {
168 write_for_each_ref_identity_date_raw(stdout, identity)?
169 }
170 }
171 }
172 ForEachRefAtom::ContentsLines { peeled, count } => {
173 let message = if *peeled {
174 context
175 .peeled_object
176 .as_ref()
177 .and_then(|peeled| peeled.message.as_deref())
178 } else {
179 context
180 .contents
181 .as_ref()
182 .map(|contents| contents.message.as_ref())
183 };
184 if let Some(message) = message {
185 super::write_for_each_ref_contents_lines(stdout, message, *count)?;
186 }
187 }
188 }
189 Ok(())
190}
191
192#[derive(Clone, Copy, Default)]
195pub struct ForEachRefEmailOptions {
196 trim: bool,
197 localpart: bool,
198 mailmap: bool,
199}
200
201impl ForEachRefEmailOptions {
202 pub fn mode(&self) -> ForEachRefEmailMode {
203 if self.localpart {
204 ForEachRefEmailMode::LocalPart
205 } else if self.trim {
206 ForEachRefEmailMode::Trim
207 } else {
208 ForEachRefEmailMode::Bracketed
209 }
210 }
211
212 pub fn wants_mailmap(&self) -> bool {
213 self.mailmap
214 }
215}
216
217pub fn setup_for_each_ref_email_options(
223 arg: &str,
224) -> std::result::Result<ForEachRefEmailOptions, String> {
225 let mut options = ForEachRefEmailOptions::default();
226 let mut rest = arg;
227 loop {
228 let matched = if let Some(tail) = rest.strip_prefix("trim") {
232 options.trim = true;
233 Some(tail)
234 } else if let Some(tail) = rest.strip_prefix("localpart") {
235 options.localpart = true;
236 Some(tail)
237 } else if let Some(tail) = rest.strip_prefix("mailmap") {
238 options.mailmap = true;
239 Some(tail)
240 } else {
241 None
242 };
243 let Some(tail) = matched else {
244 return Err(rest.to_string());
246 };
247 rest = tail;
248 let bad_arg = rest;
249 if rest.is_empty() {
250 break;
251 }
252 if let Some(tail) = rest.strip_prefix(',') {
253 rest = tail;
254 } else {
255 return Err(bad_arg.to_string());
256 }
257 }
258 Ok(options)
259}
260
261pub fn for_each_ref_try_email_atom(
266 stdout: &mut impl Write,
267 placeholder: &str,
268 context: &ForEachRefFormatContext<'_>,
269) -> Option<Result<()>> {
270 let (atom, arg) = match placeholder.split_once(':') {
271 Some((atom, arg)) => (atom, Some(arg)),
272 None => (placeholder, None),
273 };
274 let (peeled, role) = match atom {
275 "authoremail" => (false, ForEachRefAtomIdentityRole::Author),
276 "committeremail" => (false, ForEachRefAtomIdentityRole::Committer),
277 "taggeremail" => (false, ForEachRefAtomIdentityRole::Tagger),
278 "*authoremail" => (true, ForEachRefAtomIdentityRole::Author),
279 "*committeremail" => (true, ForEachRefAtomIdentityRole::Committer),
280 "*taggeremail" => (true, ForEachRefAtomIdentityRole::Tagger),
281 _ => return None,
282 };
283 let options = match arg {
284 Some(arg) => match setup_for_each_ref_email_options(arg) {
285 Ok(options) => options,
286 Err(bad_arg) => {
287 let name = atom.strip_prefix('*').unwrap_or(atom);
288 eprintln!("fatal: unrecognized %({name}) argument: {bad_arg}");
289 return Some(Err(GitError::Exit(128)));
290 }
291 },
292 None => ForEachRefEmailOptions::default(),
293 };
294 Some(for_each_ref_write_email(
295 stdout, context, peeled, role, options,
296 ))
297}
298
299pub fn for_each_ref_write_email(
300 stdout: &mut impl Write,
301 context: &ForEachRefFormatContext<'_>,
302 peeled: bool,
303 role: ForEachRefAtomIdentityRole,
304 options: ForEachRefEmailOptions,
305) -> Result<()> {
306 let Some(identity) = for_each_ref_typed_identity(context, peeled, role) else {
307 return Ok(());
308 };
309 let mode = options.mode();
310 if options.wants_mailmap() {
311 let (_, email) = context.mailmap.rewrite_identity(identity);
312 let mut synthetic = Vec::with_capacity(email.len() + 2);
315 synthetic.push(b'<');
316 synthetic.extend_from_slice(&email);
317 synthetic.push(b'>');
318 if let Some(value) = for_each_ref_identity_email(&synthetic, mode) {
319 stdout.write_all(value)?;
320 }
321 } else if let Some(value) = for_each_ref_identity_email(identity, mode) {
322 stdout.write_all(value)?;
323 }
324 Ok(())
325}
326
327pub fn for_each_ref_message<'a>(
330 context: &'a ForEachRefFormatContext<'_>,
331 peeled: bool,
332) -> Option<&'a [u8]> {
333 if peeled {
334 context
335 .peeled_object
336 .as_ref()
337 .and_then(|peeled| peeled.message.as_deref())
338 } else {
339 context.contents.as_ref().map(|contents| &*contents.message)
340 }
341}
342
343pub fn for_each_ref_try_date_atom(
347 stdout: &mut impl Write,
348 placeholder: &str,
349 context: &ForEachRefFormatContext<'_>,
350) -> Option<Result<()>> {
351 let (atom, arg) = match placeholder.split_once(':') {
352 Some((atom, arg)) => (atom, Some(arg)),
353 None => (placeholder, None),
354 };
355 let (peeled, role) = match atom {
356 "authordate" => (false, ForEachRefAtomIdentityRole::Author),
357 "committerdate" => (false, ForEachRefAtomIdentityRole::Committer),
358 "taggerdate" => (false, ForEachRefAtomIdentityRole::Tagger),
359 "creatordate" => (false, ForEachRefAtomIdentityRole::Creator),
360 "*authordate" => (true, ForEachRefAtomIdentityRole::Author),
361 "*committerdate" => (true, ForEachRefAtomIdentityRole::Committer),
362 "*taggerdate" => (true, ForEachRefAtomIdentityRole::Tagger),
363 "*creatordate" => (true, ForEachRefAtomIdentityRole::Creator),
364 _ => return None,
365 };
366 let Some(mode) = DateMode::parse_atom_modifier(arg) else {
367 let name = atom.strip_prefix('*').unwrap_or(atom);
368 eprintln!(
369 "fatal: unrecognized %({name}) argument: {}",
370 arg.unwrap_or("")
371 );
372 return Some(Err(GitError::Exit(128)));
373 };
374 Some((|| -> Result<()> {
375 if let Some(identity) = for_each_ref_typed_identity(context, peeled, role)
376 && let Some(value) = for_each_ref_identity_date(identity, &mode)
377 {
378 stdout.write_all(value.as_bytes())?;
379 }
380 Ok(())
381 })())
382}
383
384pub fn for_each_ref_oid_atom_arg<'a>(placeholder: &'a str, atom: &str) -> Option<&'a str> {
387 let rest = placeholder.strip_prefix(atom)?;
388 rest.strip_prefix(':')
389}
390
391pub fn for_each_ref_oid_atom_width(
396 arg: &str,
397 atom: &str,
398 default_abbrev: Option<usize>,
399) -> Result<Option<usize>> {
400 if arg == "short" {
401 Ok(default_abbrev)
402 } else if let Some(value) = arg.strip_prefix("short=") {
403 Ok(Some(parse_for_each_ref_abbrev_width(value).map_err(
404 |_| {
405 eprintln!("fatal: positive value expected '{value}' in %({atom})");
406 GitError::Exit(128)
407 },
408 )?))
409 } else {
410 eprintln!("fatal: unrecognized %({atom}) argument: {arg}");
411 Err(GitError::Exit(128))
412 }
413}
414
415pub fn for_each_ref_try_name_atom(
419 stdout: &mut impl Write,
420 placeholder: &str,
421 context: &ForEachRefFormatContext<'_>,
422) -> Option<Result<()>> {
423 let (atom, arg) = match placeholder.split_once(':') {
424 Some((atom, arg)) => (atom, Some(arg)),
425 None => (placeholder, None),
426 };
427 let (peeled, role) = match atom {
428 "authorname" => (false, ForEachRefAtomIdentityRole::Author),
429 "committername" => (false, ForEachRefAtomIdentityRole::Committer),
430 "taggername" => (false, ForEachRefAtomIdentityRole::Tagger),
431 "*authorname" => (true, ForEachRefAtomIdentityRole::Author),
432 "*committername" => (true, ForEachRefAtomIdentityRole::Committer),
433 "*taggername" => (true, ForEachRefAtomIdentityRole::Tagger),
434 _ => return None,
435 };
436 let mailmap = match arg {
437 None => false,
438 Some("mailmap") => true,
439 Some(bad_arg) => {
440 let name = atom.strip_prefix('*').unwrap_or(atom);
441 eprintln!("fatal: unrecognized %({name}) argument: {bad_arg}");
442 return Some(Err(GitError::Exit(128)));
443 }
444 };
445 Some((|| -> Result<()> {
446 let Some(identity) = for_each_ref_typed_identity(context, peeled, role) else {
447 return Ok(());
448 };
449 if mailmap {
450 let (name, _) = context.mailmap.rewrite_identity(identity);
451 stdout.write_all(&name)?;
452 } else {
453 write_for_each_ref_identity_name(stdout, Some(identity))?;
454 }
455 Ok(())
456 })())
457}
458
459pub fn for_each_ref_color_escape(value: &str) -> Result<String> {
460 let tokens = value.split_whitespace().collect::<Vec<_>>();
461 if tokens.is_empty() {
462 return Err(GitError::Command("empty for-each-ref color".into()));
463 }
464 if tokens.len() == 1
465 && let Some((red, green, blue)) = parse_for_each_ref_hex_color(tokens[0])
466 {
467 return Ok(format!("\x1b[38;2;{red};{green};{blue}m"));
468 }
469 let mut attributes = Vec::new();
470 let mut foreground = None;
471 let mut background = None;
472 for token in tokens.iter().copied() {
473 match token {
474 "reset" => return Ok("\x1b[m".to_string()),
475 "normal" if tokens.len() == 1 || (foreground.is_some() && background.is_none()) => {}
476 "bold" => attributes.push("1".to_string()),
477 "dim" => attributes.push("2".to_string()),
478 "italic" => attributes.push("3".to_string()),
479 "ul" => attributes.push("4".to_string()),
480 "blink" => attributes.push("5".to_string()),
481 "reverse" => attributes.push("7".to_string()),
482 "strike" => attributes.push("9".to_string()),
483 "nobold" | "nodim" => attributes.push("22".to_string()),
484 "noitalic" => attributes.push("23".to_string()),
485 "noul" => attributes.push("24".to_string()),
486 "noblink" => attributes.push("25".to_string()),
487 "noreverse" => attributes.push("27".to_string()),
488 "nostrike" => attributes.push("29".to_string()),
489 "black" => for_each_ref_push_color_code(value, &mut foreground, &mut background, 30)?,
490 "red" => for_each_ref_push_color_code(value, &mut foreground, &mut background, 31)?,
491 "green" => for_each_ref_push_color_code(value, &mut foreground, &mut background, 32)?,
492 "yellow" => for_each_ref_push_color_code(value, &mut foreground, &mut background, 33)?,
493 "blue" => for_each_ref_push_color_code(value, &mut foreground, &mut background, 34)?,
494 "magenta" => {
495 for_each_ref_push_color_code(value, &mut foreground, &mut background, 35)?
496 }
497 "cyan" => for_each_ref_push_color_code(value, &mut foreground, &mut background, 36)?,
498 "white" => for_each_ref_push_color_code(value, &mut foreground, &mut background, 37)?,
499 "brightblack" => {
500 for_each_ref_push_color_code(value, &mut foreground, &mut background, 90)?
501 }
502 "brightred" => {
503 for_each_ref_push_color_code(value, &mut foreground, &mut background, 91)?
504 }
505 "brightgreen" => {
506 for_each_ref_push_color_code(value, &mut foreground, &mut background, 92)?
507 }
508 "brightyellow" => {
509 for_each_ref_push_color_code(value, &mut foreground, &mut background, 93)?
510 }
511 "brightblue" => {
512 for_each_ref_push_color_code(value, &mut foreground, &mut background, 94)?
513 }
514 "brightmagenta" => {
515 for_each_ref_push_color_code(value, &mut foreground, &mut background, 95)?
516 }
517 "brightcyan" => {
518 for_each_ref_push_color_code(value, &mut foreground, &mut background, 96)?
519 }
520 "brightwhite" => {
521 for_each_ref_push_color_code(value, &mut foreground, &mut background, 97)?
522 }
523 _ => {
524 return Err(GitError::Command(format!(
525 "unsupported for-each-ref color {value}"
526 )));
527 }
528 }
529 }
530 let mut codes = attributes;
531 if let Some(foreground) = foreground {
532 codes.push(foreground.to_string());
533 }
534 if let Some(background) = background {
535 codes.push(background.to_string());
536 }
537 if codes.is_empty() {
538 return Ok(String::new());
539 }
540 Ok(format!("\x1b[{}m", codes.join(";")))
541}
542
543pub fn for_each_ref_push_color_code(
544 value: &str,
545 foreground: &mut Option<u16>,
546 background: &mut Option<u16>,
547 code: u16,
548) -> Result<()> {
549 if foreground.is_none() {
550 *foreground = Some(code);
551 } else if background.is_none() {
552 *background = Some(code + 10);
553 } else {
554 return Err(GitError::Command(format!(
555 "unsupported for-each-ref color {value}"
556 )));
557 }
558 Ok(())
559}