1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2
3use std::borrow::Borrow;
4use std::error::Error;
5use std::fmt;
6use std::ops::Deref;
7use std::path::{Path, PathBuf};
8use std::str::FromStr;
9use std::sync::OnceLock;
10
11pub const UPSTREAM_GIT_COMPAT_VERSION: &str = "2.55.0";
12
13static ORIGINAL_CWD: OnceLock<Option<PathBuf>> = OnceLock::new();
14
15pub fn set_original_cwd(path: Option<PathBuf>) {
16 let _ = ORIGINAL_CWD.set(path);
17}
18
19pub fn original_cwd() -> Option<PathBuf> {
20 ORIGINAL_CWD.get()?.clone()
21}
22
23#[derive(Debug, Default, Clone, PartialEq, Eq)]
24pub enum DateMode {
25 #[default]
26 Default,
27 Local,
28 Raw,
29 RawLocal,
30 Unix,
31 Short,
32 ShortLocal,
33 Iso,
34 IsoLocal,
35 IsoStrict,
36 IsoStrictLocal,
37 Rfc2822,
38 Rfc2822Local,
39 Relative,
40 Human,
41 HumanLocal,
42 Strftime {
43 template: String,
44 local: bool,
45 },
46}
47
48impl DateMode {
49 pub fn parse(value: &str) -> Option<Self> {
50 if let Some(template) = value.strip_prefix("format:") {
51 return Some(Self::Strftime {
52 template: template.to_string(),
53 local: false,
54 });
55 }
56 if let Some(template) = value.strip_prefix("format-local:") {
57 return Some(Self::Strftime {
58 template: template.to_string(),
59 local: true,
60 });
61 }
62 if value == "tformat:" || value.starts_with("tformat:") {
63 return Some(Self::Strftime {
64 template: value["tformat:".len()..].to_string(),
65 local: false,
66 });
67 }
68 if value == "auto:" || value.starts_with("auto:") {
69 return Some(Self::Default);
70 }
71 Some(match value {
72 "default" => Self::Default,
73 "default-local" | "local" => Self::Local,
74 "raw" => Self::Raw,
75 "raw-local" => Self::RawLocal,
76 "unix" => Self::Unix,
77 "short" => Self::Short,
78 "short-local" => Self::ShortLocal,
79 "iso" | "iso8601" => Self::Iso,
80 "iso-local" | "iso8601-local" => Self::IsoLocal,
81 "iso-strict" | "iso8601-strict" => Self::IsoStrict,
82 "iso-strict-local" | "iso8601-strict-local" => Self::IsoStrictLocal,
83 "rfc" | "rfc2822" => Self::Rfc2822,
84 "rfc-local" | "rfc2822-local" => Self::Rfc2822Local,
85 "relative" | "relative-local" => Self::Relative,
86 "human" => Self::Human,
87 "human-local" => Self::HumanLocal,
88 _ => return None,
89 })
90 }
91
92 pub fn parse_atom_modifier(modifier: Option<&str>) -> Option<Self> {
93 modifier.map_or(Some(Self::Default), Self::parse)
94 }
95
96 pub fn render(&self, timestamp: i64, timezone: &str) -> Option<String> {
97 let tz = if self.is_local() { "+0000" } else { timezone };
98 let parts = DateParts::from_timestamp(timestamp, tz)?;
99 Some(match self {
100 Self::Default | Self::Local => {
101 let base = format!(
102 "{} {} {} {:02}:{:02}:{:02} {}",
103 parts.weekday,
104 MONTHS_ABBR[(parts.month - 1) as usize],
105 parts.day,
106 parts.hour,
107 parts.minute,
108 parts.second,
109 parts.year,
110 );
111 if self.is_local() {
112 base
113 } else {
114 format!("{base} {}", parts.timezone)
115 }
116 }
117 Self::Raw | Self::RawLocal => format!("{} {}", parts.timestamp, parts.timezone),
118 Self::Unix => parts.timestamp.to_string(),
119 Self::Short | Self::ShortLocal => {
120 format!("{:04}-{:02}-{:02}", parts.year, parts.month, parts.day)
121 }
122 Self::Iso | Self::IsoLocal => format!(
123 "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {}",
124 parts.year,
125 parts.month,
126 parts.day,
127 parts.hour,
128 parts.minute,
129 parts.second,
130 parts.timezone,
131 ),
132 Self::IsoStrict | Self::IsoStrictLocal => format!(
133 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}",
134 parts.year,
135 parts.month,
136 parts.day,
137 parts.hour,
138 parts.minute,
139 parts.second,
140 strict_timezone(parts.timezone),
141 ),
142 Self::Rfc2822 | Self::Rfc2822Local => format!(
143 "{}, {} {} {:04} {:02}:{:02}:{:02} {}",
144 parts.weekday,
145 parts.day,
146 MONTHS_ABBR[(parts.month - 1) as usize],
147 parts.year,
148 parts.hour,
149 parts.minute,
150 parts.second,
151 parts.timezone,
152 ),
153 Self::Relative => relative_date(parts.timestamp),
154 Self::Human | Self::HumanLocal => format!(
155 "{} {} {} {:02}:{:02}:{:02} {} {}",
156 parts.weekday,
157 MONTHS_ABBR[(parts.month - 1) as usize],
158 parts.day,
159 parts.hour,
160 parts.minute,
161 parts.second,
162 parts.year,
163 parts.timezone,
164 ),
165 Self::Strftime { template, .. } => strftime(template, &parts),
166 })
167 }
168
169 pub fn is_local(&self) -> bool {
170 matches!(
171 self,
172 Self::Local
173 | Self::RawLocal
174 | Self::ShortLocal
175 | Self::IsoLocal
176 | Self::IsoStrictLocal
177 | Self::Rfc2822Local
178 | Self::HumanLocal
179 | Self::Strftime { local: true, .. }
180 )
181 }
182}
183
184const MONTHS_ABBR: [&str; 12] = [
185 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
186];
187
188const MONTHS_FULL: [&str; 12] = [
189 "January",
190 "February",
191 "March",
192 "April",
193 "May",
194 "June",
195 "July",
196 "August",
197 "September",
198 "October",
199 "November",
200 "December",
201];
202
203const WEEKDAYS_FULL: [&str; 7] = [
204 "Sunday",
205 "Monday",
206 "Tuesday",
207 "Wednesday",
208 "Thursday",
209 "Friday",
210 "Saturday",
211];
212
213struct DateParts<'a> {
214 timestamp: i64,
215 timezone: &'a str,
216 weekday: &'static str,
217 year: i64,
218 month: u32,
219 day: u32,
220 hour: i64,
221 minute: i64,
222 second: i64,
223}
224
225impl<'a> DateParts<'a> {
226 fn from_timestamp(timestamp: i64, timezone: &'a str) -> Option<Self> {
227 const WEEKDAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
228 let offset_seconds = timezone_offset_seconds(timezone)?;
229 let local = timestamp + offset_seconds;
230 let days = local.div_euclid(86_400);
231 let seconds = local.rem_euclid(86_400);
232 let (year, month, day) = civil_from_days(days);
233 Some(Self {
234 timestamp,
235 timezone,
236 weekday: WEEKDAYS[(days + 4).rem_euclid(7) as usize],
237 year,
238 month,
239 day,
240 hour: seconds / 3_600,
241 minute: (seconds % 3_600) / 60,
242 second: seconds % 60,
243 })
244 }
245}
246
247fn timezone_offset_seconds(timezone: &str) -> Option<i64> {
248 if timezone.len() != 5 {
249 return None;
250 }
251 let sign = match timezone.as_bytes()[0] {
252 b'+' => 1,
253 b'-' => -1,
254 _ => return None,
255 };
256 let hours = timezone[1..3].parse::<i64>().ok()?;
257 let minutes = timezone[3..5].parse::<i64>().ok()?;
258 Some(sign * (hours * 3_600 + minutes * 60))
259}
260
261fn strict_timezone(timezone: &str) -> String {
262 let digits = timezone.strip_prefix(['+', '-']).unwrap_or(timezone);
263 if digits == "0000" {
264 "Z".to_string()
265 } else if timezone.len() == 5 {
266 format!("{}{}:{}", &timezone[..1], &timezone[1..3], &timezone[3..5])
267 } else {
268 timezone.to_string()
269 }
270}
271
272fn strftime(template: &str, parts: &DateParts<'_>) -> String {
273 let weekday_index = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
274 .iter()
275 .position(|day| *day == parts.weekday)
276 .unwrap_or(0);
277 let mut out = String::with_capacity(template.len());
278 let mut chars = template.chars().peekable();
279 while let Some(ch) = chars.next() {
280 if ch != '%' {
281 out.push(ch);
282 continue;
283 }
284 match chars.next() {
285 Some('Y') => out.push_str(&format!("{:04}", parts.year)),
286 Some('y') => out.push_str(&format!("{:02}", parts.year.rem_euclid(100))),
287 Some('m') => out.push_str(&format!("{:02}", parts.month)),
288 Some('d') => out.push_str(&format!("{:02}", parts.day)),
289 Some('e') => out.push_str(&format!("{:2}", parts.day)),
290 Some('H') => out.push_str(&format!("{:02}", parts.hour)),
291 Some('M') => out.push_str(&format!("{:02}", parts.minute)),
292 Some('S') => out.push_str(&format!("{:02}", parts.second)),
293 Some('b') | Some('h') => out.push_str(MONTHS_ABBR[(parts.month - 1) as usize]),
294 Some('B') => out.push_str(MONTHS_FULL[(parts.month - 1) as usize]),
295 Some('a') => out.push_str(parts.weekday),
296 Some('A') => out.push_str(WEEKDAYS_FULL[weekday_index]),
297 Some('%') => out.push('%'),
298 Some('n') => out.push('\n'),
299 Some('t') => out.push('\t'),
300 Some(other) => {
301 out.push('%');
302 out.push(other);
303 }
304 None => out.push('%'),
305 }
306 }
307 out
308}
309
310fn relative_date(timestamp: i64) -> String {
311 let now = std::time::SystemTime::now()
312 .duration_since(std::time::UNIX_EPOCH)
313 .map(|duration| duration.as_secs() as i64)
314 .unwrap_or(timestamp);
315 if timestamp > now {
316 return "in the future".to_string();
317 }
318 let diff = (now - timestamp) as u64;
319 if diff < 90 {
320 return format!("{diff} seconds ago");
321 }
322 let minutes = (diff + 30) / 60;
323 if minutes < 90 {
324 return format!("{minutes} minutes ago");
325 }
326 let hours = (diff + 1800) / 3600;
327 if hours < 36 {
328 return format!("{hours} hours ago");
329 }
330 let days = (diff + 43200) / 86400;
331 if days < 14 {
332 return format!("{days} days ago");
333 }
334 if days < 70 {
335 return format!("{} weeks ago", (days + 3) / 7);
336 }
337 if days < 365 {
338 return format!("{} months ago", (days + 15) / 30);
339 }
340 let years_scaled = (days * 10 + 183) / 365;
341 if days < 365 * 2 {
342 let months = ((days - 365) + 15) / 30;
343 if months > 0 {
344 return format!("1 year, {months} months ago");
345 }
346 return "1 year ago".to_string();
347 }
348 if years_scaled.is_multiple_of(10) {
349 format!("{} years ago", years_scaled / 10)
350 } else {
351 format!("{}.{} years ago", years_scaled / 10, years_scaled % 10)
352 }
353}
354
355fn civil_from_days(days: i64) -> (i64, u32, u32) {
356 let days = days + 719_468;
357 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
358 let day_of_era = days - era * 146_097;
359 let year_of_era =
360 (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
361 let year = year_of_era + era * 400;
362 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
363 let month_prime = (5 * day_of_year + 2) / 153;
364 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
365 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
366 let year = year + i64::from(month <= 2);
367 (year, month as u32, day as u32)
368}
369
370fn is_scheme_char(ch: char) -> bool {
371 ch.is_ascii_alphanumeric() || matches!(ch, '+' | '-' | '.')
372}
373
374pub fn redact_url_for_display(url: &str) -> String {
380 let mut out = String::with_capacity(url.len());
381 let mut rest = url;
382 while let Some(scheme_end) = rest.find("://") {
383 let scheme_start = rest[..scheme_end]
384 .char_indices()
385 .rev()
386 .find_map(|(idx, ch)| (!is_scheme_char(ch)).then_some(idx + ch.len_utf8()))
387 .unwrap_or(0);
388 out.push_str(&rest[..scheme_start]);
389
390 let authority_start = scheme_end + 3;
391 let authority_end = rest[authority_start..]
392 .find(|ch: char| ['/', '?', '#', ' ', '\t', '\r', '\n'].contains(&ch))
393 .map(|idx| authority_start + idx)
394 .unwrap_or(rest.len());
395 let authority = &rest[authority_start..authority_end];
396 if let Some(at) = authority.rfind('@') {
397 out.push_str(&rest[scheme_start..authority_start]);
398 out.push_str("<redacted>@");
399 out.push_str(&authority[at + 1..]);
400 } else {
401 out.push_str(&rest[scheme_start..authority_end]);
402 }
403 rest = &rest[authority_end..];
404 }
405 out.push_str(rest);
406 out
407}
408
409pub mod trace2 {
418 use std::fmt::Display;
419 use std::fmt::Write as _;
420 use std::io::Write;
421 use std::path::PathBuf;
422
423 fn escape_json(raw: &str) -> String {
424 let mut out = String::with_capacity(raw.len());
425 for ch in raw.chars() {
426 match ch {
427 '"' => out.push_str("\\\""),
428 '\\' => out.push_str("\\\\"),
429 '\n' => out.push_str("\\n"),
430 '\t' => out.push_str("\\t"),
431 ch if (ch as u32) < 0x20 => {
432 let _ = write!(out, "\\u{:04x}", ch as u32);
433 }
434 ch => out.push(ch),
435 }
436 }
437 out
438 }
439
440 fn trace_target(var: &str) -> Option<String> {
441 let target = std::env::var_os(var)?.to_string_lossy().into_owned();
442 target.starts_with('/').then_some(target)
445 }
446
447 fn append_to_target(var: &str, line: &str) {
448 let Some(target) = trace_target(var) else {
449 return;
450 };
451 if let Ok(mut file) = std::fs::OpenOptions::new()
452 .create(true)
453 .append(true)
454 .open(target)
455 {
456 let _ = file.write_all(line.as_bytes());
457 let _ = file.write_all(b"\n");
458 }
459 }
460
461 fn redact_enabled() -> bool {
462 std::env::var("GIT_TRACE2_REDACT").map_or(true, |value| value != "0")
463 }
464
465 fn maybe_redact(raw: &str) -> String {
466 if redact_enabled() {
467 super::redact_url_for_display(raw)
468 } else {
469 raw.to_string()
470 }
471 }
472
473 fn quote_arg(arg: &str) -> String {
474 if !arg.is_empty()
475 && !arg
476 .chars()
477 .any(|ch| ch.is_whitespace() || matches!(ch, '\'' | '"' | '\\'))
478 {
479 return arg.to_string();
480 }
481 let mut out = String::with_capacity(arg.len() + 2);
482 out.push('\'');
483 for ch in arg.chars() {
484 if ch == '\'' {
485 out.push_str("'\\''");
486 } else {
487 out.push(ch);
488 }
489 }
490 out.push('\'');
491 out
492 }
493
494 fn argv0() -> String {
495 let Some(arg0) = std::env::args_os().next() else {
496 return "sley".to_string();
497 };
498 let path = PathBuf::from(arg0);
499 path.file_name()
500 .map(|name| name.to_string_lossy().into_owned())
501 .filter(|name| !name.is_empty())
502 .unwrap_or_else(|| "sley".to_string())
503 }
504
505 fn render_argv(args: &[String]) -> String {
506 let mut rendered = Vec::with_capacity(args.len() + 1);
507 rendered.push(quote_arg(&argv0()));
508 rendered.extend(args.iter().map(|arg| quote_arg(arg)));
509 rendered.join(" ")
510 }
511
512 pub fn depth() -> usize {
513 std::env::var("SLEY_TRACE2_DEPTH")
514 .ok()
515 .and_then(|value| value.parse().ok())
516 .unwrap_or(0)
517 }
518
519 fn perf_line(depth: usize, event: &str, rest: &str) {
520 append_to_target(
521 "GIT_TRACE2_PERF",
522 &format!("d{depth} | main | {event} | | | | | {rest}"),
523 );
524 }
525
526 pub fn touch() {
531 for var in ["GIT_TRACE2", "GIT_TRACE2_EVENT", "GIT_TRACE2_PERF"] {
532 let Some(target) = trace_target(var) else {
533 continue;
534 };
535 let _ = std::fs::OpenOptions::new()
536 .create(true)
537 .append(true)
538 .open(target);
539 }
540 }
541
542 pub fn start(args: &[String]) {
546 let argv = maybe_redact(&render_argv(args));
547 append_to_target("GIT_TRACE2", &format!("start {argv}"));
548 perf_line(depth(), "start", &argv);
549 }
550
551 pub fn cmd_ancestry_at_depth(depth: usize, ancestry: &[String]) {
552 if ancestry.is_empty() {
553 return;
554 }
555 append_to_target(
556 "GIT_TRACE2",
557 &format!("cmd_ancestry {}", ancestry.join(" <- ")),
558 );
559 perf_line(
560 depth,
561 "cmd_ancestry",
562 &format!("ancestry:[{}]", ancestry.join(" ")),
563 );
564 let event_ancestry = ancestry
565 .iter()
566 .map(|name| format!("\"{}\"", escape_json(name)))
567 .collect::<Vec<_>>()
568 .join(",");
569 append_to_target(
570 "GIT_TRACE2_EVENT",
571 &format!(
572 "{{\"event\":\"cmd_ancestry\",\"sid\":\"sley\",\"thread\":\"main\",\"ancestry\":[{event_ancestry}]}}"
573 ),
574 );
575 }
576
577 pub fn cmd_name(name: &str, hierarchy: Option<&str>) {
578 let rest = match hierarchy {
579 Some(hierarchy) => format!("{name} ({hierarchy})"),
580 None => name.to_string(),
581 };
582 perf_line(depth(), "cmd_name", &rest);
583 }
584
585 pub fn cmd_name_at_depth(depth: usize, name: &str, hierarchy: Option<&str>) {
586 let rest = match hierarchy {
587 Some(hierarchy) => format!("{name} ({hierarchy})"),
588 None => name.to_string(),
589 };
590 perf_line(depth, "cmd_name", &rest);
591 }
592
593 pub fn child_start(class: &str, argv: &[String]) {
594 let redacted: Vec<String> = argv.iter().map(|arg| maybe_redact(arg)).collect();
595 let joined = redacted.join(" ");
596 perf_line(
597 depth(),
598 "child_start",
599 &format!("child_id:0 class:{class} argv:[{joined}]"),
600 );
601 if let Some(target) = trace_target("GIT_TRACE2_EVENT") {
602 let json_argv = redacted
603 .iter()
604 .map(|arg| format!("\"{}\"", escape_json(arg)))
605 .collect::<Vec<_>>()
606 .join(",");
607 let line = format!(
608 "{{\"event\":\"child_start\",\"sid\":\"sley\",\"thread\":\"main\",\"child_id\":0,\"child_class\":\"{}\",\"use_shell\":false,\"argv\":[{json_argv}]}}\n",
609 escape_json(class),
610 );
611 if let Ok(mut file) = std::fs::OpenOptions::new()
612 .create(true)
613 .append(true)
614 .open(&target)
615 {
616 let _ = file.write_all(line.as_bytes());
617 }
618 }
619 }
620
621 pub fn alias(name: &str, argv: &[String]) {
622 let argv = argv
623 .iter()
624 .map(|arg| maybe_redact(arg))
625 .collect::<Vec<_>>()
626 .join(" ");
627 perf_line(depth(), "alias", &format!("alias:{name} argv:[{argv}]"));
628 }
629
630 pub fn def_param(key: &str, value: impl Display) {
632 def_param_at_depth(depth(), key, value);
633 }
634
635 pub fn def_param_at_depth(depth: usize, key: &str, value: impl Display) {
636 let value = value.to_string();
637 let normal = maybe_redact(&format!("{key}={value}"));
638 append_to_target("GIT_TRACE2", &format!("def_param {normal}"));
639 let perf = maybe_redact(&format!("{key}:{value}"));
640 perf_line(depth, "def_param", &perf);
641 }
642
643 pub fn data(category: &str, key: &str, value: impl Display) {
647 let Some(target) = trace_target("GIT_TRACE2_EVENT") else {
648 return;
649 };
650 let line = format!(
651 "{{\"event\":\"data\",\"sid\":\"sley\",\"thread\":\"main\",\"nesting\":1,\"category\":\"{}\",\"key\":\"{}\",\"value\":\"{}\"}}\n",
652 escape_json(category),
653 escape_json(key),
654 escape_json(&value.to_string()),
655 );
656 if let Ok(mut file) = std::fs::OpenOptions::new()
657 .create(true)
658 .append(true)
659 .open(&target)
660 {
661 let _ = file.write_all(line.as_bytes());
662 }
663 }
664
665 pub fn counter(category: &str, name: &str, count: impl Display) {
668 let Some(target) = trace_target("GIT_TRACE2_EVENT") else {
669 return;
670 };
671 let line = format!(
672 "{{\"event\":\"counter\",\"sid\":\"sley\",\"thread\":\"main\",\"category\":\"{}\",\"name\":\"{}\",\"count\":{}}}\n",
673 escape_json(category),
674 escape_json(name),
675 count,
676 );
677 if let Ok(mut file) = std::fs::OpenOptions::new()
678 .create(true)
679 .append(true)
680 .open(&target)
681 {
682 let _ = file.write_all(line.as_bytes());
683 }
684 }
685
686 pub fn region(category: &str, label: &str) {
690 region_event("region_enter", category, label);
691 region_event("region_leave", category, label);
692 }
693
694 fn region_event(event: &str, category: &str, label: &str) {
695 let Some(target) = trace_target("GIT_TRACE2_EVENT") else {
696 return;
697 };
698 let line = format!(
699 "{{\"event\":\"{}\",\"sid\":\"sley\",\"thread\":\"main\",\"nesting\":1,\"category\":\"{}\",\"label\":\"{}\"}}\n",
700 escape_json(event),
701 escape_json(category),
702 escape_json(label),
703 );
704 if let Ok(mut file) = std::fs::OpenOptions::new()
705 .create(true)
706 .append(true)
707 .open(&target)
708 {
709 let _ = file.write_all(line.as_bytes());
710 }
711 }
712
713 pub fn bloom_statistics(
716 filter_not_present: usize,
717 maybe: usize,
718 definitely_not: usize,
719 false_positive: usize,
720 ) {
721 let Some(target) = trace_target("GIT_TRACE2_PERF") else {
722 return;
723 };
724 let line = format!(
725 "statistics:{{\"filter_not_present\":{filter_not_present},\"maybe\":{maybe},\"definitely_not\":{definitely_not},\"false_positive\":{false_positive}}}\n"
726 );
727 if let Ok(mut file) = std::fs::OpenOptions::new()
728 .create(true)
729 .append(true)
730 .open(&target)
731 {
732 let _ = file.write_all(line.as_bytes());
733 }
734 }
735
736 pub fn perf_read_directory_data(key: &str, value: impl Display) {
739 let Some(target) = trace_target("GIT_TRACE2_PERF") else {
740 return;
741 };
742 let line = format!(
743 "19:00:00.000000 file.c:1 | d0 | main | data | r1 | ? | ? | read_directory | ....{key}:{value}\n"
744 );
745 if let Ok(mut file) = std::fs::OpenOptions::new()
746 .create(true)
747 .append(true)
748 .open(&target)
749 {
750 let _ = file.write_all(line.as_bytes());
751 }
752 }
753
754 pub fn perf_setup_data(key: &str, value: impl Display) {
759 let Some(target) = trace_target("GIT_TRACE2_PERF") else {
760 return;
761 };
762 let line = format!(
763 "19:00:00.000000 setup.c:1 | d0 | main | data | r0 | ? | ? | setup | ....{key}:{value}\n"
764 );
765 if let Ok(mut file) = std::fs::OpenOptions::new()
766 .create(true)
767 .append(true)
768 .open(&target)
769 {
770 let _ = file.write_all(line.as_bytes());
771 }
772 }
773}
774
775#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
776pub enum ObjectFormat {
777 Sha1,
778 Sha256,
779}
780
781impl ObjectFormat {
782 pub const fn raw_len(self) -> usize {
783 match self {
784 Self::Sha1 => 20,
785 Self::Sha256 => 32,
786 }
787 }
788
789 pub const fn hex_len(self) -> usize {
790 self.raw_len() * 2
791 }
792
793 pub const fn name(self) -> &'static str {
794 match self {
795 Self::Sha1 => "sha1",
796 Self::Sha256 => "sha256",
797 }
798 }
799}
800
801impl FromStr for ObjectFormat {
802 type Err = GitError;
803
804 fn from_str(value: &str) -> Result<Self> {
805 match value {
806 "sha1" => Ok(Self::Sha1),
807 "sha256" => Ok(Self::Sha256),
808 other => Err(GitError::Unsupported(format!("object format {other}"))),
809 }
810 }
811}
812
813#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
814pub struct ObjectId {
815 format: ObjectFormat,
816 bytes: [u8; 32],
817}
818
819impl ObjectId {
820 pub fn from_raw(format: ObjectFormat, raw: &[u8]) -> Result<Self> {
821 if raw.len() != format.raw_len() {
822 return Err(GitError::InvalidObjectId(format!(
823 "expected {} bytes for {}, got {}",
824 format.raw_len(),
825 format.name(),
826 raw.len()
827 )));
828 }
829 let mut bytes = [0; 32];
830 bytes[..raw.len()].copy_from_slice(raw);
831 Ok(Self { format, bytes })
832 }
833
834 pub fn from_hex(format: ObjectFormat, hex: &str) -> Result<Self> {
835 if hex.len() != format.hex_len() {
836 return Err(GitError::InvalidObjectId(format!(
837 "expected {} hex digits for {}, got {}",
838 format.hex_len(),
839 format.name(),
840 hex.len()
841 )));
842 }
843 let mut raw = [0; 32];
844 for (i, pair) in hex.as_bytes().chunks_exact(2).enumerate() {
845 raw[i] = (hex_nibble(pair[0])? << 4) | hex_nibble(pair[1])?;
846 }
847 Ok(Self { format, bytes: raw })
848 }
849
850 pub const fn format(&self) -> ObjectFormat {
851 self.format
852 }
853
854 pub fn as_bytes(&self) -> &[u8] {
855 &self.bytes[..self.format.raw_len()]
856 }
857
858 pub fn to_hex(&self) -> String {
859 let mut out = String::with_capacity(self.format.hex_len());
860 let _ = self.write_hex(&mut out);
861 out
862 }
863
864 pub fn write_hex(&self, out: &mut impl fmt::Write) -> fmt::Result {
865 write_hex_bytes(self.as_bytes(), out)
866 }
867
868 pub fn hex_prefix_matches(&self, prefix: &[u8]) -> bool {
869 if prefix.len() > self.format.hex_len() {
870 return false;
871 }
872
873 prefix.iter().enumerate().all(|(index, expected)| {
874 let Some(expected) = hex_nibble_value(*expected) else {
875 return false;
876 };
877 let byte = self.as_bytes()[index / 2];
878 let actual = if index % 2 == 0 {
879 byte >> 4
880 } else {
881 byte & 0x0f
882 };
883 actual == expected
884 })
885 }
886
887 pub const fn abbrev_hex_len(&self, width: usize) -> usize {
888 let hex_len = self.format.hex_len();
889 if width < hex_len { width } else { hex_len }
890 }
891
892 pub fn null(format: ObjectFormat) -> Self {
894 Self {
895 format,
896 bytes: [0; 32],
897 }
898 }
899
900 pub fn is_null(&self) -> bool {
902 self.as_bytes().iter().all(|byte| *byte == 0)
903 }
904
905 pub fn empty_tree(format: ObjectFormat) -> Self {
907 Self::digest_object(format, "tree", b"")
908 }
909
910 pub fn empty_blob(format: ObjectFormat) -> Self {
912 Self::digest_object(format, "blob", b"")
913 }
914
915 fn digest_object(format: ObjectFormat, object_type: &str, body: &[u8]) -> Self {
919 let mut framed = Vec::with_capacity(object_type.len() + body.len() + 32);
920 framed.extend_from_slice(object_type.as_bytes());
921 framed.push(b' ');
922 framed.extend_from_slice(body.len().to_string().as_bytes());
923 framed.push(0);
924 framed.extend_from_slice(body);
925 let mut bytes = [0u8; 32];
926 match format {
927 ObjectFormat::Sha1 => bytes[..20].copy_from_slice(&sha1(&framed)),
928 ObjectFormat::Sha256 => bytes[..32].copy_from_slice(&sha256(&framed)),
929 }
930 Self { format, bytes }
931 }
932}
933
934impl fmt::Debug for ObjectId {
935 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
936 f.debug_tuple("ObjectId").field(&self.to_hex()).finish()
937 }
938}
939
940impl fmt::Display for ObjectId {
941 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
942 self.write_hex(f)
943 }
944}
945
946impl FromStr for ObjectId {
947 type Err = GitError;
948
949 fn from_str(text: &str) -> Result<Self> {
952 let format = match text.len() {
953 40 => ObjectFormat::Sha1,
954 64 => ObjectFormat::Sha256,
955 other => {
956 return Err(GitError::InvalidObjectId(format!(
957 "expected 40 or 64 hex digits, got {other}"
958 )));
959 }
960 };
961 Self::from_hex(format, text)
962 }
963}
964
965#[derive(Debug, Clone, PartialEq, Eq)]
966pub struct ByteString(Vec<u8>);
967
968impl ByteString {
969 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
970 Self(bytes.into())
971 }
972
973 pub fn as_bytes(&self) -> &[u8] {
974 &self.0
975 }
976}
977
978impl From<&str> for ByteString {
979 fn from(value: &str) -> Self {
980 Self(value.as_bytes().to_vec())
981 }
982}
983
984#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
986pub struct FullName(String);
987
988impl FullName {
989 pub fn new(name: impl AsRef<str>) -> Result<Self> {
992 let name = name.as_ref();
993 validate_full_name(name)?;
994 Ok(Self(name.to_string()))
995 }
996
997 pub fn as_str(&self) -> &str {
998 &self.0
999 }
1000}
1001
1002impl fmt::Debug for FullName {
1003 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1004 f.debug_tuple("FullName").field(&self.0).finish()
1005 }
1006}
1007
1008impl fmt::Display for FullName {
1009 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1010 f.write_str(&self.0)
1011 }
1012}
1013
1014impl From<FullName> for String {
1015 fn from(value: FullName) -> Self {
1016 value.0
1017 }
1018}
1019
1020impl Borrow<str> for FullName {
1021 fn borrow(&self) -> &str {
1022 &self.0
1023 }
1024}
1025
1026impl AsRef<str> for FullName {
1027 fn as_ref(&self) -> &str {
1028 &self.0
1029 }
1030}
1031
1032impl TryFrom<&str> for FullName {
1033 type Error = GitError;
1034
1035 fn try_from(value: &str) -> Result<Self> {
1036 Self::new(value)
1037 }
1038}
1039
1040impl TryFrom<String> for FullName {
1041 type Error = GitError;
1042
1043 fn try_from(value: String) -> Result<Self> {
1044 validate_full_name(&value)?;
1045 Ok(Self(value))
1046 }
1047}
1048
1049impl PartialEq<&str> for FullName {
1050 fn eq(&self, other: &&str) -> bool {
1051 self.0 == *other
1052 }
1053}
1054
1055impl PartialEq<FullName> for &str {
1056 fn eq(&self, other: &FullName) -> bool {
1057 *self == other.0
1058 }
1059}
1060
1061fn validate_full_name(name: &str) -> Result<()> {
1062 if name.is_empty() {
1063 return Err(GitError::InvalidFormat("ref name must not be empty".into()));
1064 }
1065 if name.chars().next().is_some_and(|ch| ch.is_whitespace())
1066 || name.chars().last().is_some_and(|ch| ch.is_whitespace())
1067 {
1068 return Err(GitError::InvalidFormat(
1069 "ref name must not have leading or trailing whitespace".into(),
1070 ));
1071 }
1072 if name.contains("//") {
1073 return Err(GitError::InvalidFormat(
1074 "ref name must not contain consecutive slashes".into(),
1075 ));
1076 }
1077 if name.bytes().any(|byte| byte.is_ascii_control()) {
1078 return Err(GitError::InvalidFormat(
1079 "ref name must not contain control characters".into(),
1080 ));
1081 }
1082 Ok(())
1083}
1084
1085#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
1087pub struct BString(Vec<u8>);
1088
1089impl BString {
1090 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
1091 Self(bytes.into())
1092 }
1093 pub fn from_bytes(bytes: &[u8]) -> Self {
1094 Self(bytes.to_vec())
1095 }
1096 pub fn as_bytes(&self) -> &[u8] {
1097 &self.0
1098 }
1099 pub fn len(&self) -> usize {
1100 self.0.len()
1101 }
1102 pub fn is_empty(&self) -> bool {
1103 self.0.is_empty()
1104 }
1105 pub fn into_bytes(self) -> Vec<u8> {
1106 self.0
1107 }
1108}
1109
1110impl From<&str> for BString {
1111 fn from(v: &str) -> Self {
1112 Self::from_bytes(v.as_bytes())
1113 }
1114}
1115impl From<&[u8]> for BString {
1116 fn from(v: &[u8]) -> Self {
1117 Self::from_bytes(v)
1118 }
1119}
1120impl<const N: usize> From<&[u8; N]> for BString {
1121 fn from(v: &[u8; N]) -> Self {
1122 Self::from_bytes(v.as_slice())
1123 }
1124}
1125impl From<Vec<u8>> for BString {
1126 fn from(v: Vec<u8>) -> Self {
1127 Self(v)
1128 }
1129}
1130impl PartialEq<&[u8]> for BString {
1131 fn eq(&self, o: &&[u8]) -> bool {
1132 self.0.as_slice() == *o
1133 }
1134}
1135impl<const N: usize> PartialEq<&[u8; N]> for BString {
1136 fn eq(&self, o: &&[u8; N]) -> bool {
1137 self.as_bytes() == o.as_slice()
1138 }
1139}
1140impl PartialEq<BString> for &[u8] {
1141 fn eq(&self, o: &BString) -> bool {
1142 *self == o.as_bytes()
1143 }
1144}
1145impl<const N: usize> PartialEq<BString> for &[u8; N] {
1146 fn eq(&self, o: &BString) -> bool {
1147 self.as_slice() == o.as_bytes()
1148 }
1149}
1150impl PartialEq<Vec<u8>> for BString {
1151 fn eq(&self, o: &Vec<u8>) -> bool {
1152 self.0 == *o
1153 }
1154}
1155impl PartialEq<BString> for Vec<u8> {
1156 fn eq(&self, o: &BString) -> bool {
1157 *self == o.0
1158 }
1159}
1160
1161impl fmt::Display for BString {
1162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1163 write!(f, "{}", String::from_utf8_lossy(&self.0))
1164 }
1165}
1166
1167impl Borrow<[u8]> for BString {
1168 fn borrow(&self) -> &[u8] {
1169 self.as_bytes()
1170 }
1171}
1172
1173impl Deref for BString {
1174 type Target = [u8];
1175
1176 fn deref(&self) -> &[u8] {
1177 self.as_bytes()
1178 }
1179}
1180
1181impl AsRef<[u8]> for BString {
1182 fn as_ref(&self) -> &[u8] {
1183 self.as_bytes()
1184 }
1185}
1186
1187#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1188pub struct RepoPath(PathBuf);
1189
1190impl RepoPath {
1191 pub fn new(path: impl Into<PathBuf>) -> Result<Self> {
1192 let path = path.into();
1193 if path.is_absolute() {
1194 return Err(GitError::InvalidPath(
1195 "repository paths must be relative".into(),
1196 ));
1197 }
1198 if path.components().any(|component| {
1199 matches!(
1200 component,
1201 std::path::Component::ParentDir | std::path::Component::Prefix(_)
1202 )
1203 }) {
1204 return Err(GitError::InvalidPath(
1205 "repository paths must not escape".into(),
1206 ));
1207 }
1208 Ok(Self(path))
1209 }
1210
1211 pub fn as_path(&self) -> &Path {
1212 &self.0
1213 }
1214}
1215
1216#[derive(Debug, Clone, PartialEq, Eq)]
1231pub struct Signature {
1232 pub name: ByteString,
1235 pub email: ByteString,
1238 pub time: GitTime,
1240 pub raw: Vec<u8>,
1244}
1245
1246impl Signature {
1247 pub fn from_ident_line(line: &[u8]) -> Option<Self> {
1261 let mail_end = line.iter().rposition(|byte| *byte == b'>')?;
1265 let mail_begin = line[..mail_end].iter().rposition(|byte| *byte == b'<')? + 1;
1266 let email = &line[mail_begin..mail_end];
1267
1268 let mut name_end = mail_begin.saturating_sub(1);
1271 if name_end > 0 && line[name_end - 1] == b' ' {
1272 name_end -= 1;
1273 }
1274 let name = &line[..name_end];
1275
1276 let rest = line.get(mail_end + 1..)?;
1279 let rest = rest.strip_prefix(b" ")?;
1280 let time = GitTime::from_time_fields(rest)?;
1281
1282 Some(Self {
1283 name: ByteString::new(name.to_vec()),
1284 email: ByteString::new(email.to_vec()),
1285 time,
1286 raw: line.to_vec(),
1287 })
1288 }
1289
1290 pub fn to_ident_bytes(&self) -> Vec<u8> {
1297 self.raw.clone()
1298 }
1299
1300 pub fn to_canonical_ident_bytes(&self) -> Vec<u8> {
1309 let mut out = Vec::with_capacity(self.raw.len());
1310 out.extend_from_slice(self.name.as_bytes());
1311 out.extend_from_slice(b" <");
1312 out.extend_from_slice(self.email.as_bytes());
1313 out.extend_from_slice(b"> ");
1314 out.extend_from_slice(self.time.to_ident_suffix().as_bytes());
1315 out
1316 }
1317}
1318
1319pub struct IdentFields<'a> {
1328 pub name: &'a [u8],
1330 pub email: &'a [u8],
1332 pub date: Option<&'a [u8]>,
1335 pub tz: Option<&'a [u8]>,
1337}
1338
1339fn ident_isspace(byte: u8) -> bool {
1344 matches!(byte, b' ' | b'\t' | b'\n' | b'\r')
1345}
1346
1347pub fn split_ident_line(line: &[u8]) -> Option<IdentFields<'_>> {
1352 let len = line.len();
1353 let lt = line.iter().position(|&byte| byte == b'<')?;
1355 let mail_begin = lt + 1;
1356
1357 let mut name_end = mail_begin - 1;
1360 if mail_begin >= 2 {
1361 let mut i = mail_begin - 2;
1362 loop {
1363 if !ident_isspace(line[i]) {
1364 name_end = i + 1;
1365 break;
1366 }
1367 if i == 0 {
1368 break;
1369 }
1370 i -= 1;
1371 }
1372 }
1373 let name = &line[..name_end];
1374
1375 let gt = line[mail_begin..].iter().position(|&byte| byte == b'>')? + mail_begin;
1377 let email = &line[mail_begin..gt];
1378
1379 let person_only = IdentFields {
1380 name,
1381 email,
1382 date: None,
1383 tz: None,
1384 };
1385
1386 let mut cp = len - 1;
1389 while line[cp] != b'>' {
1390 if cp == 0 {
1391 return Some(person_only);
1392 }
1393 cp -= 1;
1394 }
1395 let mut i = cp + 1;
1396 while i < len && ident_isspace(line[i]) {
1397 i += 1;
1398 }
1399 let date_begin = i;
1400 while i < len && line[i].is_ascii_digit() {
1401 i += 1;
1402 }
1403 if i == date_begin {
1404 return Some(person_only);
1405 }
1406 let date = &line[date_begin..i];
1407
1408 while i < len && ident_isspace(line[i]) {
1409 i += 1;
1410 }
1411 if i >= len || (line[i] != b'+' && line[i] != b'-') {
1412 return Some(person_only);
1413 }
1414 let tz_begin = i;
1415 i += 1;
1416 let tz_digits = i;
1417 while i < len && line[i].is_ascii_digit() {
1418 i += 1;
1419 }
1420 if i == tz_digits {
1421 return Some(person_only);
1422 }
1423 Some(IdentFields {
1424 name,
1425 email,
1426 date: Some(date),
1427 tz: Some(&line[tz_begin..i]),
1428 })
1429}
1430
1431fn ident_date_overflows(seconds: u64) -> bool {
1434 seconds >= i64::MAX as u64
1435}
1436
1437pub fn ident_render_date(date: &[u8], tz: &[u8], mode: &DateMode) -> String {
1444 let parsed = std::str::from_utf8(date)
1445 .ok()
1446 .and_then(|text| text.parse::<u64>().ok());
1447 let (seconds, tz_text) = match parsed {
1448 Some(value) if !ident_date_overflows(value) => {
1449 (value as i64, std::str::from_utf8(tz).unwrap_or("+0000"))
1450 }
1451 _ => (0, "+0000"),
1454 };
1455 mode.render(seconds, tz_text).unwrap_or_default()
1456}
1457
1458impl fmt::Display for Signature {
1459 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1463 write!(f, "{}", String::from_utf8_lossy(&self.raw))
1464 }
1465}
1466
1467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1480pub struct GitTime {
1481 pub seconds: i64,
1483 pub timezone_offset_minutes: i16,
1487 pub negative_utc: bool,
1491}
1492
1493impl GitTime {
1494 pub const fn new(seconds: i64, timezone_offset_minutes: i16) -> Self {
1498 Self {
1499 seconds,
1500 timezone_offset_minutes,
1501 negative_utc: false,
1502 }
1503 }
1504
1505 pub const fn with_negative_utc(seconds: i64) -> Self {
1508 Self {
1509 seconds,
1510 timezone_offset_minutes: 0,
1511 negative_utc: true,
1512 }
1513 }
1514
1515 fn from_time_fields(bytes: &[u8]) -> Option<Self> {
1519 let text = std::str::from_utf8(bytes).ok()?;
1520 let (seconds_text, tz_text) = text.split_once(' ')?;
1521 let seconds = seconds_text.parse::<i64>().ok()?;
1522 let (timezone_offset_minutes, negative_utc) = parse_timezone_token(tz_text)?;
1523 Some(Self {
1524 seconds,
1525 timezone_offset_minutes,
1526 negative_utc,
1527 })
1528 }
1529
1530 fn to_ident_suffix(self) -> String {
1533 format!("{} {}", self.seconds, self.offset_token())
1534 }
1535
1536 pub fn offset_token(self) -> String {
1540 let sign = if self.negative_utc || self.timezone_offset_minutes < 0 {
1541 '-'
1542 } else {
1543 '+'
1544 };
1545 let magnitude = self.timezone_offset_minutes.unsigned_abs();
1546 format!("{sign}{:02}{:02}", magnitude / 60, magnitude % 60)
1547 }
1548}
1549
1550fn parse_timezone_token(token: &str) -> Option<(i16, bool)> {
1556 let bytes = token.as_bytes();
1557 if bytes.len() != 5 {
1558 return None;
1559 }
1560 let negative = match bytes[0] {
1561 b'+' => false,
1562 b'-' => true,
1563 _ => return None,
1564 };
1565 if !bytes[1..].iter().all(u8::is_ascii_digit) {
1566 return None;
1567 }
1568 let hours = i16::from(bytes[1] - b'0') * 10 + i16::from(bytes[2] - b'0');
1569 let minutes = i16::from(bytes[3] - b'0') * 10 + i16::from(bytes[4] - b'0');
1570 let total = hours * 60 + minutes;
1571 let negative_utc = negative && total == 0;
1572 let signed = if negative { -total } else { total };
1573 Some((signed, negative_utc))
1574}
1575
1576#[derive(Debug, Clone, PartialEq, Eq)]
1577pub struct Capability {
1578 pub name: String,
1579 pub value: Option<String>,
1580}
1581
1582#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1583pub enum MissingObjectKind {
1584 Object,
1585 Blob,
1586 Tree,
1587 Commit,
1588 Tag,
1589}
1590
1591impl MissingObjectKind {
1592 pub const fn as_str(self) -> &'static str {
1593 match self {
1594 Self::Object => "object",
1595 Self::Blob => "blob",
1596 Self::Tree => "tree",
1597 Self::Commit => "commit",
1598 Self::Tag => "tag",
1599 }
1600 }
1601}
1602
1603impl fmt::Display for MissingObjectKind {
1604 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1605 f.write_str(self.as_str())
1606 }
1607}
1608
1609#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1610pub enum MissingObjectContext {
1611 Read,
1612 Traversal,
1613 PackInstall,
1614 RevisionWalk,
1615 WorktreeMaterialize,
1616 RemoteBoundary,
1617}
1618
1619impl MissingObjectContext {
1620 pub const fn as_str(self) -> &'static str {
1621 match self {
1622 Self::Read => "read",
1623 Self::Traversal => "traversal",
1624 Self::PackInstall => "pack-install",
1625 Self::RevisionWalk => "revision-walk",
1626 Self::WorktreeMaterialize => "worktree-materialize",
1627 Self::RemoteBoundary => "remote-boundary",
1628 }
1629 }
1630}
1631
1632impl fmt::Display for MissingObjectContext {
1633 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1634 f.write_str(self.as_str())
1635 }
1636}
1637
1638#[derive(Debug, Clone, PartialEq, Eq)]
1639pub enum NotFoundKind {
1640 Message(String),
1641 Remote {
1642 name: String,
1643 },
1644 Object {
1645 oid: ObjectId,
1646 kind: MissingObjectKind,
1647 context: Option<MissingObjectContext>,
1648 },
1649 Reference {
1650 name: String,
1651 },
1652 Repository {
1653 path: String,
1654 },
1655}
1656
1657impl fmt::Display for NotFoundKind {
1658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1659 match self {
1660 Self::Message(msg) => write!(f, "{msg}"),
1661 Self::Remote { name } => write!(f, "remote {name}"),
1662 Self::Object {
1663 oid,
1664 kind: MissingObjectKind::Object,
1665 ..
1666 } => write!(f, "object {oid}"),
1667 Self::Object { oid, kind, .. } => write!(f, "{kind} object {oid}"),
1668 Self::Reference { name } => write!(f, "{name}"),
1669 Self::Repository { path } => write!(f, "{path}"),
1670 }
1671 }
1672}
1673
1674impl NotFoundKind {
1675 pub fn object_id(&self) -> Option<ObjectId> {
1676 match self {
1677 Self::Object { oid, .. } => Some(*oid),
1678 _ => None,
1679 }
1680 }
1681
1682 pub fn missing_object_kind(&self) -> Option<MissingObjectKind> {
1683 match self {
1684 Self::Object { kind, .. } => Some(*kind),
1685 _ => None,
1686 }
1687 }
1688
1689 pub fn missing_object_context(&self) -> Option<MissingObjectContext> {
1690 match self {
1691 Self::Object { context, .. } => *context,
1692 _ => None,
1693 }
1694 }
1695}
1696
1697#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1699pub enum CliExit {
1700 Ok,
1702 UserError,
1704 Usage,
1706 Custom(i32),
1708}
1709
1710impl CliExit {
1711 pub const fn code(self) -> i32 {
1712 match self {
1713 Self::Ok => 0,
1714 Self::UserError => 128,
1715 Self::Usage => 129,
1716 Self::Custom(code) => code,
1717 }
1718 }
1719}
1720
1721#[derive(Debug, Clone, PartialEq, Eq)]
1722pub enum GitError {
1723 Io(String),
1724 InvalidObjectId(String),
1725 InvalidObject(String),
1726 InvalidFormat(String),
1727 InvalidPath(String),
1728 Unsupported(String),
1729 NotFound(NotFoundKind),
1730 Transaction(String),
1731 Command(String),
1732 Cli(CliExit, String),
1734 Exit(i32),
1736}
1737
1738pub type Result<T> = std::result::Result<T, GitError>;
1739
1740impl fmt::Display for GitError {
1741 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1742 match self {
1743 Self::Io(msg) => write!(f, "io error: {msg}"),
1744 Self::InvalidObjectId(msg) => write!(f, "invalid object id: {msg}"),
1745 Self::InvalidObject(msg) => write!(f, "invalid object: {msg}"),
1746 Self::InvalidFormat(msg) => write!(f, "invalid format: {msg}"),
1747 Self::InvalidPath(msg) => write!(f, "invalid path: {msg}"),
1748 Self::Unsupported(msg) => write!(f, "unsupported: {msg}"),
1749 Self::NotFound(kind) => write!(f, "not found: {kind}"),
1750 Self::Transaction(msg) => write!(f, "transaction failed: {msg}"),
1751 Self::Command(msg) => write!(f, "command failed: {msg}"),
1752 Self::Cli(_, msg) => f.write_str(msg),
1753 Self::Exit(code) => write!(f, "exit {code}"),
1754 }
1755 }
1756}
1757
1758impl Error for GitError {}
1759
1760impl GitError {
1761 pub fn usage(msg: impl Into<String>) -> Self {
1762 Self::Cli(CliExit::Usage, msg.into())
1763 }
1764
1765 pub fn user_error(msg: impl Into<String>) -> Self {
1766 Self::Cli(CliExit::UserError, msg.into())
1767 }
1768
1769 pub fn cli_exit(kind: CliExit, msg: impl Into<String>) -> Self {
1770 Self::Cli(kind, msg.into())
1771 }
1772
1773 pub fn cli_exit_code(&self) -> i32 {
1774 cli_exit_code(self)
1775 }
1776
1777 pub fn not_found(msg: impl Into<String>) -> Self {
1778 Self::NotFound(NotFoundKind::Message(msg.into()))
1779 }
1780
1781 pub fn remote_not_found(name: impl Into<String>) -> Self {
1782 Self::NotFound(NotFoundKind::Remote { name: name.into() })
1783 }
1784
1785 pub fn object_not_found(oid: ObjectId) -> Self {
1786 Self::object_kind_not_found(oid, MissingObjectKind::Object)
1787 }
1788
1789 pub fn object_kind_not_found(oid: ObjectId, kind: MissingObjectKind) -> Self {
1790 Self::NotFound(NotFoundKind::Object {
1791 oid,
1792 kind,
1793 context: None,
1794 })
1795 }
1796
1797 pub fn object_not_found_in(oid: ObjectId, context: MissingObjectContext) -> Self {
1798 Self::object_kind_not_found_in(oid, MissingObjectKind::Object, context)
1799 }
1800
1801 pub fn object_kind_not_found_in(
1802 oid: ObjectId,
1803 kind: MissingObjectKind,
1804 context: MissingObjectContext,
1805 ) -> Self {
1806 Self::NotFound(NotFoundKind::Object {
1807 oid,
1808 kind,
1809 context: Some(context),
1810 })
1811 }
1812
1813 pub fn reference_not_found(name: impl Into<String>) -> Self {
1814 Self::NotFound(NotFoundKind::Reference { name: name.into() })
1815 }
1816
1817 pub fn repository_not_found(path: impl Into<String>) -> Self {
1818 Self::NotFound(NotFoundKind::Repository { path: path.into() })
1819 }
1820
1821 pub fn not_found_kind(&self) -> Option<&NotFoundKind> {
1822 match self {
1823 Self::NotFound(kind) => Some(kind),
1824 _ => None,
1825 }
1826 }
1827}
1828
1829impl From<std::io::Error> for GitError {
1830 fn from(value: std::io::Error) -> Self {
1831 Self::Io(value.to_string())
1832 }
1833}
1834
1835pub fn cli_exit_code(err: &GitError) -> i32 {
1837 match err {
1838 GitError::Exit(code) => *code,
1839 GitError::Cli(kind, _) => kind.code(),
1840 GitError::Command(_) => 1,
1843 _ => 1,
1844 }
1845}
1846
1847pub fn object_id_for_bytes(
1848 format: ObjectFormat,
1849 object_type: &str,
1850 body: &[u8],
1851) -> Result<ObjectId> {
1852 match format {
1853 ObjectFormat::Sha1 => ObjectId::from_raw(format, &sha1_object_digest(object_type, body)),
1857 ObjectFormat::Sha256 => {
1858 let mut framed = Vec::with_capacity(object_type.len() + body.len() + 32);
1859 framed.extend_from_slice(object_type.as_bytes());
1860 framed.push(b' ');
1861 framed.extend_from_slice(body.len().to_string().as_bytes());
1862 framed.push(0);
1863 framed.extend_from_slice(body);
1864 ObjectId::from_raw(format, &sha256(&framed))
1865 }
1866 }
1867}
1868
1869pub fn digest_bytes(format: ObjectFormat, bytes: &[u8]) -> Result<ObjectId> {
1870 match format {
1871 ObjectFormat::Sha1 => ObjectId::from_raw(format, &sha1(bytes)),
1872 ObjectFormat::Sha256 => ObjectId::from_raw(format, &sha256(bytes)),
1873 }
1874}
1875
1876pub struct StreamingDigest {
1877 format: ObjectFormat,
1878 inner: StreamingDigestInner,
1879}
1880
1881enum StreamingDigestInner {
1882 #[cfg(not(feature = "fast-sha1"))]
1883 Sha1(Sha1Hasher),
1884 #[cfg(feature = "fast-sha1")]
1885 Sha1(sha1::Sha1),
1886 Sha256(Sha256Hasher),
1887}
1888
1889impl StreamingDigest {
1890 pub fn new(format: ObjectFormat) -> Self {
1891 let inner = match format {
1892 #[cfg(not(feature = "fast-sha1"))]
1893 ObjectFormat::Sha1 => StreamingDigestInner::Sha1(Sha1Hasher::new()),
1894 #[cfg(feature = "fast-sha1")]
1895 ObjectFormat::Sha1 => {
1896 use sha1::Digest;
1897 StreamingDigestInner::Sha1(sha1::Sha1::new())
1898 }
1899 ObjectFormat::Sha256 => StreamingDigestInner::Sha256(Sha256Hasher::new()),
1900 };
1901 Self { format, inner }
1902 }
1903
1904 pub fn update(&mut self, data: &[u8]) {
1905 match &mut self.inner {
1906 #[cfg(not(feature = "fast-sha1"))]
1907 StreamingDigestInner::Sha1(hasher) => hasher.update(data),
1908 #[cfg(feature = "fast-sha1")]
1909 StreamingDigestInner::Sha1(hasher) => {
1910 use sha1::Digest;
1911 hasher.update(data);
1912 }
1913 StreamingDigestInner::Sha256(hasher) => hasher.update(data),
1914 }
1915 }
1916
1917 pub fn finalize(self) -> Result<ObjectId> {
1918 match self.inner {
1919 #[cfg(not(feature = "fast-sha1"))]
1920 StreamingDigestInner::Sha1(hasher) => {
1921 ObjectId::from_raw(self.format, &hasher.finalize())
1922 }
1923 #[cfg(feature = "fast-sha1")]
1924 StreamingDigestInner::Sha1(hasher) => {
1925 use sha1::Digest;
1926 let bytes: [u8; 20] = hasher.finalize().into();
1927 ObjectId::from_raw(self.format, &bytes)
1928 }
1929 StreamingDigestInner::Sha256(hasher) => {
1930 ObjectId::from_raw(self.format, &hasher.finalize())
1931 }
1932 }
1933 }
1934}
1935
1936pub fn to_hex(bytes: &[u8]) -> String {
1937 let mut out = String::with_capacity(bytes.len() * 2);
1938 let _ = write_hex_bytes(bytes, &mut out);
1939 out
1940}
1941
1942fn write_hex_bytes(bytes: &[u8], out: &mut impl fmt::Write) -> fmt::Result {
1943 const HEX: &[u8; 16] = b"0123456789abcdef";
1944 for byte in bytes {
1945 out.write_char(HEX[(byte >> 4) as usize] as char)?;
1946 out.write_char(HEX[(byte & 0x0f) as usize] as char)?;
1947 }
1948 Ok(())
1949}
1950
1951fn hex_nibble_value(byte: u8) -> Option<u8> {
1952 match byte {
1953 b'0'..=b'9' => Some(byte - b'0'),
1954 b'a'..=b'f' => Some(byte - b'a' + 10),
1955 b'A'..=b'F' => Some(byte - b'A' + 10),
1956 _ => None,
1957 }
1958}
1959
1960fn hex_nibble(byte: u8) -> Result<u8> {
1961 hex_nibble_value(byte)
1962 .ok_or_else(|| GitError::InvalidObjectId(format!("non-hex byte {:?}", byte as char)))
1963}
1964
1965#[cfg(not(feature = "fast-sha1"))]
1977fn sha1(input: &[u8]) -> [u8; 20] {
1978 let mut hasher = Sha1Hasher::new();
1979 hasher.update(input);
1980 hasher.finalize()
1981}
1982
1983#[cfg(feature = "fast-sha1")]
1985fn sha1(input: &[u8]) -> [u8; 20] {
1986 use sha1::{Digest, Sha1};
1987 let mut hasher = Sha1::new();
1988 hasher.update(input);
1989 hasher.finalize().into()
1990}
1991
1992#[cfg(not(feature = "fast-sha1"))]
1995fn sha1_object_digest(object_type: &str, body: &[u8]) -> [u8; 20] {
1996 let mut hasher = Sha1Hasher::new();
1997 hasher.update(object_type.as_bytes());
1998 hasher.update(b" ");
1999 hasher.update(body.len().to_string().as_bytes());
2000 hasher.update(&[0u8]);
2001 hasher.update(body);
2002 hasher.finalize()
2003}
2004
2005#[cfg(feature = "fast-sha1")]
2006fn sha1_object_digest(object_type: &str, body: &[u8]) -> [u8; 20] {
2007 use sha1::{Digest, Sha1};
2008 let mut hasher = Sha1::new();
2009 hasher.update(object_type.as_bytes());
2010 hasher.update(b" ");
2011 hasher.update(body.len().to_string().as_bytes());
2012 hasher.update([0u8]);
2013 hasher.update(body);
2014 hasher.finalize().into()
2015}
2016
2017#[cfg(not(feature = "fast-sha1"))]
2021struct Sha1Hasher {
2022 state: [u32; 5],
2023 block: [u8; 64],
2024 block_len: usize,
2025 total_len: u64,
2026}
2027
2028#[cfg(not(feature = "fast-sha1"))]
2029impl Sha1Hasher {
2030 fn new() -> Self {
2031 Self {
2032 state: [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0],
2033 block: [0u8; 64],
2034 block_len: 0,
2035 total_len: 0,
2036 }
2037 }
2038
2039 fn update(&mut self, mut data: &[u8]) {
2040 self.total_len = self.total_len.wrapping_add(data.len() as u64);
2041 if self.block_len > 0 {
2042 let take = (64 - self.block_len).min(data.len());
2043 self.block[self.block_len..self.block_len + take].copy_from_slice(&data[..take]);
2044 self.block_len += take;
2045 data = &data[take..];
2046 if self.block_len == 64 {
2047 let block = self.block;
2048 sha1_compress(&mut self.state, &block);
2049 self.block_len = 0;
2050 }
2051 }
2052 while data.len() >= 64 {
2053 sha1_compress(&mut self.state, &data[..64]);
2054 data = &data[64..];
2055 }
2056 if !data.is_empty() {
2057 self.block[..data.len()].copy_from_slice(data);
2058 self.block_len = data.len();
2059 }
2060 }
2061
2062 fn finalize(mut self) -> [u8; 20] {
2063 let bit_len = self.total_len.wrapping_mul(8);
2064 let mut tail = [0u8; 128];
2067 tail[..self.block_len].copy_from_slice(&self.block[..self.block_len]);
2068 tail[self.block_len] = 0x80;
2069 let total = if self.block_len < 56 { 64 } else { 128 };
2070 tail[total - 8..total].copy_from_slice(&bit_len.to_be_bytes());
2071 sha1_compress(&mut self.state, &tail[..64]);
2072 if total == 128 {
2073 sha1_compress(&mut self.state, &tail[64..128]);
2074 }
2075 let mut out = [0u8; 20];
2076 out[0..4].copy_from_slice(&self.state[0].to_be_bytes());
2077 out[4..8].copy_from_slice(&self.state[1].to_be_bytes());
2078 out[8..12].copy_from_slice(&self.state[2].to_be_bytes());
2079 out[12..16].copy_from_slice(&self.state[3].to_be_bytes());
2080 out[16..20].copy_from_slice(&self.state[4].to_be_bytes());
2081 out
2082 }
2083}
2084
2085#[cfg(not(feature = "fast-sha1"))]
2087fn sha1_compress(state: &mut [u32; 5], block: &[u8]) {
2088 let mut w = [0u32; 80];
2089 for (i, word) in w.iter_mut().take(16).enumerate() {
2090 let offset = i * 4;
2091 *word = u32::from_be_bytes([
2092 block[offset],
2093 block[offset + 1],
2094 block[offset + 2],
2095 block[offset + 3],
2096 ]);
2097 }
2098 for i in 16..80 {
2099 w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
2100 }
2101
2102 let mut a = state[0];
2103 let mut b = state[1];
2104 let mut c = state[2];
2105 let mut d = state[3];
2106 let mut e = state[4];
2107
2108 for (i, word) in w.iter().enumerate() {
2109 let (f, k) = match i {
2110 0..=19 => ((b & c) | ((!b) & d), 0x5a827999u32),
2111 20..=39 => (b ^ c ^ d, 0x6ed9eba1),
2112 40..=59 => ((b & c) | (b & d) | (c & d), 0x8f1bbcdc),
2113 _ => (b ^ c ^ d, 0xca62c1d6),
2114 };
2115 let temp = a
2116 .rotate_left(5)
2117 .wrapping_add(f)
2118 .wrapping_add(e)
2119 .wrapping_add(k)
2120 .wrapping_add(*word);
2121 e = d;
2122 d = c;
2123 c = b.rotate_left(30);
2124 b = a;
2125 a = temp;
2126 }
2127
2128 state[0] = state[0].wrapping_add(a);
2129 state[1] = state[1].wrapping_add(b);
2130 state[2] = state[2].wrapping_add(c);
2131 state[3] = state[3].wrapping_add(d);
2132 state[4] = state[4].wrapping_add(e);
2133}
2134
2135fn sha256(input: &[u8]) -> [u8; 32] {
2136 let mut hasher = Sha256Hasher::new();
2137 hasher.update(input);
2138 hasher.finalize()
2139}
2140
2141struct Sha256Hasher {
2142 state: [u32; 8],
2143 block: [u8; 64],
2144 block_len: usize,
2145 total_len: u64,
2146}
2147
2148impl Sha256Hasher {
2149 const K: [u32; 64] = [
2150 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
2151 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
2152 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
2153 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
2154 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
2155 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
2156 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
2157 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
2158 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
2159 0xc67178f2,
2160 ];
2161
2162 fn new() -> Self {
2163 Self {
2164 state: [
2165 0x6a09e667u32,
2166 0xbb67ae85,
2167 0x3c6ef372,
2168 0xa54ff53a,
2169 0x510e527f,
2170 0x9b05688c,
2171 0x1f83d9ab,
2172 0x5be0cd19,
2173 ],
2174 block: [0u8; 64],
2175 block_len: 0,
2176 total_len: 0,
2177 }
2178 }
2179
2180 fn update(&mut self, mut data: &[u8]) {
2181 self.total_len = self.total_len.wrapping_add(data.len() as u64);
2182 if self.block_len > 0 {
2183 let take = (64 - self.block_len).min(data.len());
2184 self.block[self.block_len..self.block_len + take].copy_from_slice(&data[..take]);
2185 self.block_len += take;
2186 data = &data[take..];
2187 if self.block_len == 64 {
2188 let block = self.block;
2189 self.compress(&block);
2190 self.block_len = 0;
2191 }
2192 }
2193 while data.len() >= 64 {
2194 self.compress(&data[..64]);
2195 data = &data[64..];
2196 }
2197 if !data.is_empty() {
2198 self.block[..data.len()].copy_from_slice(data);
2199 self.block_len = data.len();
2200 }
2201 }
2202
2203 fn finalize(mut self) -> [u8; 32] {
2204 let bit_len = self.total_len.wrapping_mul(8);
2205 let mut tail = [0u8; 128];
2206 tail[..self.block_len].copy_from_slice(&self.block[..self.block_len]);
2207 tail[self.block_len] = 0x80;
2208 let total = if self.block_len < 56 { 64 } else { 128 };
2209 tail[total - 8..total].copy_from_slice(&bit_len.to_be_bytes());
2210 self.compress(&tail[..64]);
2211 if total == 128 {
2212 self.compress(&tail[64..128]);
2213 }
2214
2215 let mut out = [0; 32];
2216 for (idx, word) in self.state.iter().enumerate() {
2217 out[idx * 4..idx * 4 + 4].copy_from_slice(&word.to_be_bytes());
2218 }
2219 out
2220 }
2221
2222 fn compress(&mut self, chunk: &[u8]) {
2223 let mut w = [0u32; 64];
2224 for (i, word) in w.iter_mut().take(16).enumerate() {
2225 let offset = i * 4;
2226 *word = u32::from_be_bytes([
2227 chunk[offset],
2228 chunk[offset + 1],
2229 chunk[offset + 2],
2230 chunk[offset + 3],
2231 ]);
2232 }
2233 for i in 16..64 {
2234 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
2235 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
2236 w[i] = w[i - 16]
2237 .wrapping_add(s0)
2238 .wrapping_add(w[i - 7])
2239 .wrapping_add(s1);
2240 }
2241
2242 let mut a = self.state[0];
2243 let mut b = self.state[1];
2244 let mut c = self.state[2];
2245 let mut d = self.state[3];
2246 let mut e = self.state[4];
2247 let mut f = self.state[5];
2248 let mut g = self.state[6];
2249 let mut hh = self.state[7];
2250
2251 for (&word, &constant) in w.iter().zip(Self::K.iter()) {
2252 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
2253 let ch = (e & f) ^ ((!e) & g);
2254 let temp1 = hh
2255 .wrapping_add(s1)
2256 .wrapping_add(ch)
2257 .wrapping_add(constant)
2258 .wrapping_add(word);
2259 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
2260 let maj = (a & b) ^ (a & c) ^ (b & c);
2261 let temp2 = s0.wrapping_add(maj);
2262
2263 hh = g;
2264 g = f;
2265 f = e;
2266 e = d.wrapping_add(temp1);
2267 d = c;
2268 c = b;
2269 b = a;
2270 a = temp1.wrapping_add(temp2);
2271 }
2272
2273 self.state[0] = self.state[0].wrapping_add(a);
2274 self.state[1] = self.state[1].wrapping_add(b);
2275 self.state[2] = self.state[2].wrapping_add(c);
2276 self.state[3] = self.state[3].wrapping_add(d);
2277 self.state[4] = self.state[4].wrapping_add(e);
2278 self.state[5] = self.state[5].wrapping_add(f);
2279 self.state[6] = self.state[6].wrapping_add(g);
2280 self.state[7] = self.state[7].wrapping_add(hh);
2281 }
2282}
2283
2284#[cfg(test)]
2285mod tests {
2286 use super::*;
2287
2288 #[test]
2289 fn sha1_blob_matches_git_known_value() {
2290 let oid = object_id_for_bytes(ObjectFormat::Sha1, "blob", b"hello\n")
2291 .expect("known blob should hash as sha1");
2292 assert_eq!(oid.to_hex(), "ce013625030ba8dba906f756967f9e9ca394464a");
2293 }
2294
2295 #[test]
2296 fn sha256_blob_matches_git_known_value() {
2297 let oid = object_id_for_bytes(ObjectFormat::Sha256, "blob", b"hello\n")
2298 .expect("known blob should hash as sha256");
2299 assert_eq!(
2300 oid.to_hex(),
2301 "2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4"
2302 );
2303 }
2304
2305 #[test]
2306 fn object_id_round_trips_hex() {
2307 let oid = ObjectId::from_hex(
2308 ObjectFormat::Sha1,
2309 "ce013625030ba8dba906f756967f9e9ca394464a",
2310 )
2311 .expect("valid sha1 hex");
2312 assert_eq!(oid.to_hex(), "ce013625030ba8dba906f756967f9e9ca394464a");
2313 }
2314
2315 #[test]
2316 fn object_id_writes_hex_without_allocating_in_the_writer() {
2317 let oid = ObjectId::from_hex(
2318 ObjectFormat::Sha1,
2319 "CE013625030BA8DBA906F756967F9E9CA394464A",
2320 )
2321 .expect("valid uppercase sha1 hex");
2322
2323 let mut out = String::new();
2324 oid.write_hex(&mut out)
2325 .expect("writing object id hex to a String should not fail");
2326
2327 assert_eq!(out, "ce013625030ba8dba906f756967f9e9ca394464a");
2328 assert_eq!(oid.to_hex(), out);
2329 assert_eq!(format!("{oid}"), out);
2330 }
2331
2332 #[test]
2333 fn object_id_matches_hex_prefixes_by_nibble() {
2334 let oid = ObjectId::from_hex(
2335 ObjectFormat::Sha1,
2336 "ce013625030ba8dba906f756967f9e9ca394464a",
2337 )
2338 .expect("valid sha1 hex");
2339
2340 assert!(oid.hex_prefix_matches(b""));
2341 assert!(oid.hex_prefix_matches(b"c"));
2342 assert!(oid.hex_prefix_matches(b"ce013"));
2343 assert!(oid.hex_prefix_matches(b"CE013625"));
2344 assert!(oid.hex_prefix_matches(b"ce013625030ba8dba906f756967f9e9ca394464a"));
2345
2346 assert!(!oid.hex_prefix_matches(b"d"));
2347 assert!(!oid.hex_prefix_matches(b"ce014"));
2348 assert!(!oid.hex_prefix_matches(b"ce01x"));
2349
2350 let mut too_long = oid.to_hex();
2351 too_long.push('0');
2352 assert!(!oid.hex_prefix_matches(too_long.as_bytes()));
2353 }
2354
2355 #[test]
2356 fn object_id_abbrev_hex_len_clamps_to_format_width() {
2357 let sha1 = ObjectId::null(ObjectFormat::Sha1);
2358 let sha256 = ObjectId::null(ObjectFormat::Sha256);
2359
2360 assert_eq!(sha1.abbrev_hex_len(0), 0);
2361 assert_eq!(sha1.abbrev_hex_len(12), 12);
2362 assert_eq!(sha1.abbrev_hex_len(80), ObjectFormat::Sha1.hex_len());
2363 assert_eq!(sha256.abbrev_hex_len(80), ObjectFormat::Sha256.hex_len());
2364 }
2365
2366 #[test]
2367 fn signature_parses_a_normal_ident_and_round_trips() {
2368 let line = b"A U Thor <author@example.com> 1700000000 +0000";
2369 let sig = Signature::from_ident_line(line).expect("well-formed ident parses");
2370 assert_eq!(sig.name.as_bytes(), b"A U Thor");
2371 assert_eq!(sig.email.as_bytes(), b"author@example.com");
2372 assert_eq!(sig.time.seconds, 1_700_000_000);
2373 assert_eq!(sig.time.timezone_offset_minutes, 0);
2374 assert!(!sig.time.negative_utc);
2375 assert_eq!(sig.to_ident_bytes(), line);
2377 assert_eq!(sig.to_canonical_ident_bytes(), line);
2378 }
2379
2380 #[test]
2381 fn signature_parses_positive_half_hour_offset() {
2382 let line = b"Half Hour <hh@example.com> 1500000000 +0530";
2383 let sig = Signature::from_ident_line(line).expect("offset ident parses");
2384 assert_eq!(sig.time.timezone_offset_minutes, 330);
2385 assert!(!sig.time.negative_utc);
2386 assert_eq!(sig.time.offset_token(), "+0530");
2387 assert_eq!(sig.to_ident_bytes(), line);
2388 assert_eq!(sig.to_canonical_ident_bytes(), line);
2389 }
2390
2391 #[test]
2392 fn signature_parses_negative_offset() {
2393 let line = b"Western <w@example.com> 1500000000 -0500";
2394 let sig = Signature::from_ident_line(line).expect("negative offset parses");
2395 assert_eq!(sig.time.timezone_offset_minutes, -300);
2396 assert!(!sig.time.negative_utc);
2397 assert_eq!(sig.time.offset_token(), "-0500");
2398 assert_eq!(sig.to_ident_bytes(), line);
2399 }
2400
2401 #[test]
2402 fn signature_preserves_negative_zero_timezone_distinct_from_positive_zero() {
2403 let negative = b"Unknown Zone <uz@example.com> 1500000000 -0000";
2404 let positive = b"Known Zone <kz@example.com> 1500000000 +0000";
2405
2406 let neg = Signature::from_ident_line(negative).expect("-0000 parses");
2407 let pos = Signature::from_ident_line(positive).expect("+0000 parses");
2408
2409 assert_eq!(neg.time.timezone_offset_minutes, 0);
2411 assert_eq!(pos.time.timezone_offset_minutes, 0);
2412 assert!(neg.time.negative_utc);
2414 assert!(!pos.time.negative_utc);
2415 assert_ne!(neg.time, pos.time);
2416
2417 assert_eq!(neg.time.offset_token(), "-0000");
2419 assert_eq!(pos.time.offset_token(), "+0000");
2420 assert_eq!(neg.to_ident_bytes(), negative);
2421 assert_eq!(pos.to_ident_bytes(), positive);
2422 assert_eq!(neg.to_canonical_ident_bytes(), negative);
2423 assert_eq!(pos.to_canonical_ident_bytes(), positive);
2424 assert_ne!(neg.to_ident_bytes(), pos.to_ident_bytes());
2425 }
2426
2427 #[test]
2428 fn signature_handles_empty_name_and_email() {
2429 let line = b" <> 0 +0000";
2432 let sig = Signature::from_ident_line(line).expect("empty name/email parses");
2433 assert_eq!(sig.name.as_bytes(), b"");
2434 assert_eq!(sig.email.as_bytes(), b"");
2435 assert_eq!(sig.time.seconds, 0);
2436 assert_eq!(sig.to_ident_bytes(), line);
2437 }
2438
2439 #[test]
2440 fn signature_keeps_angle_brackets_inside_the_name() {
2441 let line = b"Weird <Name> <weird@example.com> 1 +0000";
2445 let sig = Signature::from_ident_line(line).expect("bracketed name parses");
2446 assert_eq!(sig.name.as_bytes(), b"Weird <Name>");
2447 assert_eq!(sig.email.as_bytes(), b"weird@example.com");
2448 assert_eq!(sig.to_ident_bytes(), line);
2449 }
2450
2451 #[test]
2452 fn signature_round_trips_non_canonical_whitespace_via_raw() {
2453 let line = b"Spaced <spaced@example.com> 5 +0000";
2457 let sig = Signature::from_ident_line(line).expect("non-canonical ident parses");
2458 assert_eq!(sig.name.as_bytes(), b"Spaced ");
2460 assert_eq!(sig.to_ident_bytes(), line);
2461 }
2462
2463 #[test]
2464 fn signature_rejects_malformed_idents() {
2465 assert!(Signature::from_ident_line(b"No Email Here 0 +0000").is_none());
2467 assert!(Signature::from_ident_line(b"A U Thor <a@example.com>").is_none());
2469 assert!(Signature::from_ident_line(b"A U Thor <a@example.com> later +0000").is_none());
2471 assert!(Signature::from_ident_line(b"A U Thor <a@example.com> 0 +00").is_none());
2473 assert!(Signature::from_ident_line(b"A U Thor <a@example.com> 0 0000").is_none());
2475 }
2476
2477 #[test]
2478 fn git_time_constructors_set_the_sentinel() {
2479 assert!(!GitTime::new(0, 0).negative_utc);
2480 assert_eq!(GitTime::new(0, 330).offset_token(), "+0530");
2481 let unknown = GitTime::with_negative_utc(42);
2482 assert!(unknown.negative_utc);
2483 assert_eq!(unknown.seconds, 42);
2484 assert_eq!(unknown.offset_token(), "-0000");
2485 }
2486
2487 #[test]
2488 fn full_name_accepts_valid_ref_names() {
2489 let name = FullName::new("refs/heads/main").expect("valid ref name");
2490 assert_eq!(name.as_str(), "refs/heads/main");
2491 assert_eq!(name, "refs/heads/main");
2492 assert_eq!(format!("{name}"), "refs/heads/main");
2493 assert_eq!(String::from(name.clone()), "refs/heads/main");
2494 let borrowed: &str = name.borrow();
2495 assert_eq!(borrowed, "refs/heads/main");
2496 }
2497
2498 #[test]
2499 fn full_name_rejects_invalid_ref_names() {
2500 assert!(FullName::new("").is_err());
2501 assert!(FullName::new(" refs/heads/main").is_err());
2502 assert!(FullName::new("refs/heads/main ").is_err());
2503 assert!(FullName::new("refs//heads/main").is_err());
2504 assert!(FullName::new("refs/heads/\nmain").is_err());
2505 }
2506
2507 #[test]
2508 fn cli_exit_codes_match_git_taxonomy() {
2509 assert_eq!(CliExit::Ok.code(), 0);
2510 assert_eq!(CliExit::UserError.code(), 128);
2511 assert_eq!(CliExit::Usage.code(), 129);
2512 assert_eq!(CliExit::Custom(1).code(), 1);
2513 assert_eq!(CliExit::Custom(5).code(), 5);
2514 }
2515
2516 #[test]
2517 fn git_error_cli_exit_code_mapping() {
2518 assert_eq!(GitError::Exit(129).cli_exit_code(), 129);
2519 assert_eq!(GitError::Exit(128).cli_exit_code(), 128);
2520 assert_eq!(GitError::usage("unknown option").cli_exit_code(), 129);
2521 assert_eq!(
2522 GitError::user_error("not a git repository").cli_exit_code(),
2523 128
2524 );
2525 assert_eq!(
2526 GitError::cli_exit(CliExit::Custom(2), "diff found changes").cli_exit_code(),
2527 2
2528 );
2529 assert_eq!(GitError::Command("bad value".into()).cli_exit_code(), 1);
2530 assert_eq!(GitError::not_found("missing ref").cli_exit_code(), 1);
2531 }
2532
2533 #[test]
2534 fn git_error_cli_displays_message_only() {
2535 let err = GitError::usage("unknown option `--foo'");
2536 assert_eq!(err.to_string(), "unknown option `--foo'");
2537 }
2538
2539 #[test]
2540 fn bstring_round_trips_bytes_and_displays_lossily() {
2541 let path = BString::from_bytes(b"src/\xFF.txt");
2542 assert_eq!(path.as_bytes(), b"src/\xFF.txt");
2543 let borrowed: &[u8] = path.borrow();
2544 assert_eq!(borrowed, b"src/\xFF.txt".as_slice());
2545 assert_eq!(format!("{path}"), "src/\u{FFFD}.txt");
2546 assert_eq!(path, b"src/\xFF.txt");
2547 assert_eq!(path.clone().into_bytes(), b"src/\xFF.txt".to_vec());
2548 }
2549
2550 #[test]
2551 fn split_ident_line_parses_well_formed_ident() {
2552 let f = split_ident_line(b"A U Thor <author@example.com> 1112911993 -0700")
2553 .expect("well formed ident should parse");
2554 assert_eq!(f.name, b"A U Thor");
2555 assert_eq!(f.email, b"author@example.com");
2556 assert_eq!(f.date, Some(&b"1112911993"[..]));
2557 assert_eq!(f.tz, Some(&b"-0700"[..]));
2558 }
2559
2560 #[test]
2561 fn split_ident_line_recovers_broken_email() {
2562 let f = split_ident_line(b"A U Thor <author@example.com>-<> 1112911993 -0700")
2565 .expect("broken-email ident should parse");
2566 assert_eq!(f.name, b"A U Thor");
2567 assert_eq!(f.email, b"author@example.com");
2568 assert_eq!(f.date, Some(&b"1112911993"[..]));
2569 assert_eq!(f.tz, Some(&b"-0700"[..]));
2570 }
2571
2572 #[test]
2573 fn split_ident_line_non_numeric_date_is_person_only() {
2574 let f = split_ident_line(b"A U Thor <author@example.com> totally_bogus -0700")
2575 .expect("ident without numeric date should still parse person");
2576 assert_eq!(f.email, b"author@example.com");
2577 assert_eq!(f.date, None);
2578 assert_eq!(f.tz, None);
2579 }
2580
2581 #[test]
2582 fn split_ident_line_whitespace_date_is_person_only() {
2583 let f = split_ident_line(b"A U Thor <author@example.com> ")
2585 .expect("ident with trailing whitespace should parse person");
2586 assert_eq!(f.date, None);
2587 let f = split_ident_line(b"A U Thor <author@example.com> \x0b")
2590 .expect("ident with non-git-whitespace suffix should parse person");
2591 assert_eq!(f.date, None);
2592 }
2593
2594 #[test]
2595 fn split_ident_line_requires_angle_brackets() {
2596 assert!(split_ident_line(b"no brackets here 123 +0000").is_none());
2597 }
2598
2599 #[test]
2600 fn ident_render_date_overflow_is_epoch_sentinel() {
2601 assert_eq!(
2604 ident_render_date(b"18446744073709551617", b"-0700", &DateMode::Default),
2605 "Thu Jan 1 00:00:00 1970 +0000"
2606 );
2607 assert_eq!(
2608 ident_render_date(b"18446744073709551614", b"-0700", &DateMode::Default),
2609 "Thu Jan 1 00:00:00 1970 +0000"
2610 );
2611 }
2612
2613 #[test]
2614 fn ident_render_date_valid_value_uses_original_timezone() {
2615 assert_eq!(
2616 ident_render_date(b"0", b"+0000", &DateMode::Default),
2617 "Thu Jan 1 00:00:00 1970 +0000"
2618 );
2619 }
2620
2621 #[test]
2622 fn redact_url_for_display_strips_https_userinfo() {
2623 assert_eq!(
2624 redact_url_for_display("https://user:pass@host/repo.git"),
2625 "https://<redacted>@host/repo.git"
2626 );
2627 }
2628
2629 #[test]
2630 fn redact_url_for_display_leaves_urls_without_userinfo_unchanged() {
2631 assert_eq!(
2632 redact_url_for_display("https://host/repo.git"),
2633 "https://host/repo.git"
2634 );
2635 assert_eq!(redact_url_for_display("origin"), "origin");
2636 }
2637}