1use std::borrow::Borrow;
2use std::error::Error;
3use std::fmt;
4use std::ops::Deref;
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7use std::sync::Mutex;
8
9pub const UPSTREAM_GIT_COMPAT_VERSION: &str = "2.55.0";
10
11static ORIGINAL_CWD: Mutex<Option<PathBuf>> = Mutex::new(None);
12
13pub fn set_original_cwd(path: Option<PathBuf>) {
14 if let Ok(mut original) = ORIGINAL_CWD.lock() {
15 *original = path;
16 }
17}
18
19pub fn original_cwd() -> Option<PathBuf> {
20 ORIGINAL_CWD.lock().ok()?.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
370pub mod trace2 {
379 use std::fmt::Display;
380 use std::fmt::Write as _;
381 use std::io::Write;
382 use std::path::PathBuf;
383
384 fn escape_json(raw: &str) -> String {
385 let mut out = String::with_capacity(raw.len());
386 for ch in raw.chars() {
387 match ch {
388 '"' => out.push_str("\\\""),
389 '\\' => out.push_str("\\\\"),
390 '\n' => out.push_str("\\n"),
391 '\t' => out.push_str("\\t"),
392 ch if (ch as u32) < 0x20 => {
393 let _ = write!(out, "\\u{:04x}", ch as u32);
394 }
395 ch => out.push(ch),
396 }
397 }
398 out
399 }
400
401 fn trace_target(var: &str) -> Option<String> {
402 let target = std::env::var_os(var)?.to_string_lossy().into_owned();
403 target.starts_with('/').then_some(target)
406 }
407
408 fn append_to_target(var: &str, line: &str) {
409 let Some(target) = trace_target(var) else {
410 return;
411 };
412 if let Ok(mut file) = std::fs::OpenOptions::new()
413 .create(true)
414 .append(true)
415 .open(target)
416 {
417 let _ = file.write_all(line.as_bytes());
418 let _ = file.write_all(b"\n");
419 }
420 }
421
422 fn redact_enabled() -> bool {
423 std::env::var("GIT_TRACE2_REDACT").map_or(true, |value| value != "0")
424 }
425
426 fn is_scheme_char(ch: char) -> bool {
427 ch.is_ascii_alphanumeric() || matches!(ch, '+' | '-' | '.')
428 }
429
430 fn redact_unsafe_urls(raw: &str) -> String {
431 let mut out = String::with_capacity(raw.len());
432 let mut rest = raw;
433 while let Some(scheme_end) = rest.find("://") {
434 let scheme_start = rest[..scheme_end]
435 .char_indices()
436 .rev()
437 .find_map(|(idx, ch)| (!is_scheme_char(ch)).then_some(idx + ch.len_utf8()))
438 .unwrap_or(0);
439 out.push_str(&rest[..scheme_start]);
440
441 let authority_start = scheme_end + 3;
442 let authority_end = rest[authority_start..]
443 .find(|ch: char| matches!(ch, '/' | '?' | '#' | ' ' | '\t' | '\r' | '\n'))
444 .map(|idx| authority_start + idx)
445 .unwrap_or(rest.len());
446 let authority = &rest[authority_start..authority_end];
447 if let Some(at) = authority.rfind('@') {
448 out.push_str(&rest[scheme_start..authority_start]);
449 out.push_str("<redacted>@");
450 out.push_str(&authority[at + 1..]);
451 } else {
452 out.push_str(&rest[scheme_start..authority_end]);
453 }
454 rest = &rest[authority_end..];
455 }
456 out.push_str(rest);
457 out
458 }
459
460 fn maybe_redact(raw: &str) -> String {
461 if redact_enabled() {
462 redact_unsafe_urls(raw)
463 } else {
464 raw.to_string()
465 }
466 }
467
468 fn quote_arg(arg: &str) -> String {
469 if !arg.is_empty()
470 && !arg
471 .chars()
472 .any(|ch| ch.is_whitespace() || matches!(ch, '\'' | '"' | '\\'))
473 {
474 return arg.to_string();
475 }
476 let mut out = String::with_capacity(arg.len() + 2);
477 out.push('\'');
478 for ch in arg.chars() {
479 if ch == '\'' {
480 out.push_str("'\\''");
481 } else {
482 out.push(ch);
483 }
484 }
485 out.push('\'');
486 out
487 }
488
489 fn argv0() -> String {
490 let Some(arg0) = std::env::args_os().next() else {
491 return "sley".to_string();
492 };
493 let path = PathBuf::from(arg0);
494 path.file_name()
495 .map(|name| name.to_string_lossy().into_owned())
496 .filter(|name| !name.is_empty())
497 .unwrap_or_else(|| "sley".to_string())
498 }
499
500 fn render_argv(args: &[String]) -> String {
501 let mut rendered = Vec::with_capacity(args.len() + 1);
502 rendered.push(quote_arg(&argv0()));
503 rendered.extend(args.iter().map(|arg| quote_arg(arg)));
504 rendered.join(" ")
505 }
506
507 pub fn depth() -> usize {
508 std::env::var("SLEY_TRACE2_DEPTH")
509 .ok()
510 .and_then(|value| value.parse().ok())
511 .unwrap_or(0)
512 }
513
514 fn perf_line(depth: usize, event: &str, rest: &str) {
515 append_to_target(
516 "GIT_TRACE2_PERF",
517 &format!("d{depth} | main | {event} | | | | | {rest}"),
518 );
519 }
520
521 pub fn touch() {
526 for var in ["GIT_TRACE2", "GIT_TRACE2_EVENT", "GIT_TRACE2_PERF"] {
527 let Some(target) = trace_target(var) else {
528 continue;
529 };
530 let _ = std::fs::OpenOptions::new()
531 .create(true)
532 .append(true)
533 .open(target);
534 }
535 }
536
537 pub fn start(args: &[String]) {
541 let argv = maybe_redact(&render_argv(args));
542 append_to_target("GIT_TRACE2", &format!("start {argv}"));
543 perf_line(depth(), "start", &argv);
544 }
545
546 pub fn cmd_ancestry_at_depth(depth: usize, ancestry: &[String]) {
547 if ancestry.is_empty() {
548 return;
549 }
550 append_to_target(
551 "GIT_TRACE2",
552 &format!("cmd_ancestry {}", ancestry.join(" <- ")),
553 );
554 perf_line(
555 depth,
556 "cmd_ancestry",
557 &format!("ancestry:[{}]", ancestry.join(" ")),
558 );
559 let event_ancestry = ancestry
560 .iter()
561 .map(|name| format!("\"{}\"", escape_json(name)))
562 .collect::<Vec<_>>()
563 .join(",");
564 append_to_target(
565 "GIT_TRACE2_EVENT",
566 &format!(
567 "{{\"event\":\"cmd_ancestry\",\"sid\":\"sley\",\"thread\":\"main\",\"ancestry\":[{event_ancestry}]}}"
568 ),
569 );
570 }
571
572 pub fn cmd_name(name: &str, hierarchy: Option<&str>) {
573 let rest = match hierarchy {
574 Some(hierarchy) => format!("{name} ({hierarchy})"),
575 None => name.to_string(),
576 };
577 perf_line(depth(), "cmd_name", &rest);
578 }
579
580 pub fn cmd_name_at_depth(depth: usize, name: &str, hierarchy: Option<&str>) {
581 let rest = match hierarchy {
582 Some(hierarchy) => format!("{name} ({hierarchy})"),
583 None => name.to_string(),
584 };
585 perf_line(depth, "cmd_name", &rest);
586 }
587
588 pub fn child_start(class: &str, argv: &[String]) {
589 let argv = argv
590 .iter()
591 .map(|arg| maybe_redact(arg))
592 .collect::<Vec<_>>()
593 .join(" ");
594 perf_line(
595 depth(),
596 "child_start",
597 &format!("child_id:0 class:{class} argv:[{argv}]"),
598 );
599 }
600
601 pub fn alias(name: &str, argv: &[String]) {
602 let argv = argv
603 .iter()
604 .map(|arg| maybe_redact(arg))
605 .collect::<Vec<_>>()
606 .join(" ");
607 perf_line(depth(), "alias", &format!("alias:{name} argv:[{argv}]"));
608 }
609
610 pub fn def_param(key: &str, value: impl Display) {
612 def_param_at_depth(depth(), key, value);
613 }
614
615 pub fn def_param_at_depth(depth: usize, key: &str, value: impl Display) {
616 let value = value.to_string();
617 let normal = maybe_redact(&format!("{key}={value}"));
618 append_to_target("GIT_TRACE2", &format!("def_param {normal}"));
619 let perf = maybe_redact(&format!("{key}:{value}"));
620 perf_line(depth, "def_param", &perf);
621 }
622
623 pub fn data(category: &str, key: &str, value: impl Display) {
627 let Some(target) = trace_target("GIT_TRACE2_EVENT") else {
628 return;
629 };
630 let line = format!(
631 "{{\"event\":\"data\",\"sid\":\"sley\",\"thread\":\"main\",\"nesting\":1,\"category\":\"{}\",\"key\":\"{}\",\"value\":\"{}\"}}\n",
632 escape_json(category),
633 escape_json(key),
634 escape_json(&value.to_string()),
635 );
636 if let Ok(mut file) = std::fs::OpenOptions::new()
637 .create(true)
638 .append(true)
639 .open(&target)
640 {
641 let _ = file.write_all(line.as_bytes());
642 }
643 }
644
645 pub fn counter(category: &str, name: &str, count: impl Display) {
648 let Some(target) = trace_target("GIT_TRACE2_EVENT") else {
649 return;
650 };
651 let line = format!(
652 "{{\"event\":\"counter\",\"sid\":\"sley\",\"thread\":\"main\",\"category\":\"{}\",\"name\":\"{}\",\"count\":{}}}\n",
653 escape_json(category),
654 escape_json(name),
655 count,
656 );
657 if let Ok(mut file) = std::fs::OpenOptions::new()
658 .create(true)
659 .append(true)
660 .open(&target)
661 {
662 let _ = file.write_all(line.as_bytes());
663 }
664 }
665
666 pub fn region(category: &str, label: &str) {
670 region_event("region_enter", category, label);
671 region_event("region_leave", category, label);
672 }
673
674 fn region_event(event: &str, category: &str, label: &str) {
675 let Some(target) = trace_target("GIT_TRACE2_EVENT") else {
676 return;
677 };
678 let line = format!(
679 "{{\"event\":\"{}\",\"sid\":\"sley\",\"thread\":\"main\",\"nesting\":1,\"category\":\"{}\",\"label\":\"{}\"}}\n",
680 escape_json(event),
681 escape_json(category),
682 escape_json(label),
683 );
684 if let Ok(mut file) = std::fs::OpenOptions::new()
685 .create(true)
686 .append(true)
687 .open(&target)
688 {
689 let _ = file.write_all(line.as_bytes());
690 }
691 }
692
693 pub fn bloom_statistics(
696 filter_not_present: usize,
697 maybe: usize,
698 definitely_not: usize,
699 false_positive: usize,
700 ) {
701 let Some(target) = trace_target("GIT_TRACE2_PERF") else {
702 return;
703 };
704 let line = format!(
705 "statistics:{{\"filter_not_present\":{filter_not_present},\"maybe\":{maybe},\"definitely_not\":{definitely_not},\"false_positive\":{false_positive}}}\n"
706 );
707 if let Ok(mut file) = std::fs::OpenOptions::new()
708 .create(true)
709 .append(true)
710 .open(&target)
711 {
712 let _ = file.write_all(line.as_bytes());
713 }
714 }
715
716 pub fn perf_read_directory_data(key: &str, value: impl Display) {
719 let Some(target) = trace_target("GIT_TRACE2_PERF") else {
720 return;
721 };
722 let line = format!(
723 "19:00:00.000000 file.c:1 | d0 | main | data | r1 | ? | ? | read_directory | ....{key}:{value}\n"
724 );
725 if let Ok(mut file) = std::fs::OpenOptions::new()
726 .create(true)
727 .append(true)
728 .open(&target)
729 {
730 let _ = file.write_all(line.as_bytes());
731 }
732 }
733
734 pub fn perf_setup_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 setup.c:1 | d0 | main | data | r0 | ? | ? | setup | ....{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
755#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
756pub enum ObjectFormat {
757 Sha1,
758 Sha256,
759}
760
761impl ObjectFormat {
762 pub const fn raw_len(self) -> usize {
763 match self {
764 Self::Sha1 => 20,
765 Self::Sha256 => 32,
766 }
767 }
768
769 pub const fn hex_len(self) -> usize {
770 self.raw_len() * 2
771 }
772
773 pub const fn name(self) -> &'static str {
774 match self {
775 Self::Sha1 => "sha1",
776 Self::Sha256 => "sha256",
777 }
778 }
779}
780
781impl FromStr for ObjectFormat {
782 type Err = GitError;
783
784 fn from_str(value: &str) -> Result<Self> {
785 match value {
786 "sha1" => Ok(Self::Sha1),
787 "sha256" => Ok(Self::Sha256),
788 other => Err(GitError::Unsupported(format!("object format {other}"))),
789 }
790 }
791}
792
793#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
794pub struct ObjectId {
795 format: ObjectFormat,
796 bytes: [u8; 32],
797}
798
799impl ObjectId {
800 pub fn from_raw(format: ObjectFormat, raw: &[u8]) -> Result<Self> {
801 if raw.len() != format.raw_len() {
802 return Err(GitError::InvalidObjectId(format!(
803 "expected {} bytes for {}, got {}",
804 format.raw_len(),
805 format.name(),
806 raw.len()
807 )));
808 }
809 let mut bytes = [0; 32];
810 bytes[..raw.len()].copy_from_slice(raw);
811 Ok(Self { format, bytes })
812 }
813
814 pub fn from_hex(format: ObjectFormat, hex: &str) -> Result<Self> {
815 if hex.len() != format.hex_len() {
816 return Err(GitError::InvalidObjectId(format!(
817 "expected {} hex digits for {}, got {}",
818 format.hex_len(),
819 format.name(),
820 hex.len()
821 )));
822 }
823 let mut raw = [0; 32];
824 for (i, pair) in hex.as_bytes().chunks_exact(2).enumerate() {
825 raw[i] = (hex_nibble(pair[0])? << 4) | hex_nibble(pair[1])?;
826 }
827 Ok(Self { format, bytes: raw })
828 }
829
830 pub const fn format(&self) -> ObjectFormat {
831 self.format
832 }
833
834 pub fn as_bytes(&self) -> &[u8] {
835 &self.bytes[..self.format.raw_len()]
836 }
837
838 pub fn to_hex(&self) -> String {
839 let mut out = String::with_capacity(self.format.hex_len());
840 self.write_hex(&mut out)
841 .expect("writing object id hex to a String cannot fail");
842 out
843 }
844
845 pub fn write_hex(&self, out: &mut impl fmt::Write) -> fmt::Result {
846 write_hex_bytes(self.as_bytes(), out)
847 }
848
849 pub fn hex_prefix_matches(&self, prefix: &[u8]) -> bool {
850 if prefix.len() > self.format.hex_len() {
851 return false;
852 }
853
854 prefix.iter().enumerate().all(|(index, expected)| {
855 let Some(expected) = hex_nibble_value(*expected) else {
856 return false;
857 };
858 let byte = self.as_bytes()[index / 2];
859 let actual = if index % 2 == 0 {
860 byte >> 4
861 } else {
862 byte & 0x0f
863 };
864 actual == expected
865 })
866 }
867
868 pub const fn abbrev_hex_len(&self, width: usize) -> usize {
869 let hex_len = self.format.hex_len();
870 if width < hex_len { width } else { hex_len }
871 }
872
873 pub fn null(format: ObjectFormat) -> Self {
875 Self {
876 format,
877 bytes: [0; 32],
878 }
879 }
880
881 pub fn is_null(&self) -> bool {
883 self.as_bytes().iter().all(|byte| *byte == 0)
884 }
885
886 pub fn empty_tree(format: ObjectFormat) -> Self {
888 Self::digest_object(format, "tree", b"")
889 }
890
891 pub fn empty_blob(format: ObjectFormat) -> Self {
893 Self::digest_object(format, "blob", b"")
894 }
895
896 fn digest_object(format: ObjectFormat, object_type: &str, body: &[u8]) -> Self {
900 let mut framed = Vec::with_capacity(object_type.len() + body.len() + 32);
901 framed.extend_from_slice(object_type.as_bytes());
902 framed.push(b' ');
903 framed.extend_from_slice(body.len().to_string().as_bytes());
904 framed.push(0);
905 framed.extend_from_slice(body);
906 let mut bytes = [0u8; 32];
907 match format {
908 ObjectFormat::Sha1 => bytes[..20].copy_from_slice(&sha1(&framed)),
909 ObjectFormat::Sha256 => bytes[..32].copy_from_slice(&sha256(&framed)),
910 }
911 Self { format, bytes }
912 }
913}
914
915impl fmt::Debug for ObjectId {
916 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
917 f.debug_tuple("ObjectId").field(&self.to_hex()).finish()
918 }
919}
920
921impl fmt::Display for ObjectId {
922 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
923 self.write_hex(f)
924 }
925}
926
927impl FromStr for ObjectId {
928 type Err = GitError;
929
930 fn from_str(text: &str) -> Result<Self> {
933 let format = match text.len() {
934 40 => ObjectFormat::Sha1,
935 64 => ObjectFormat::Sha256,
936 other => {
937 return Err(GitError::InvalidObjectId(format!(
938 "expected 40 or 64 hex digits, got {other}"
939 )));
940 }
941 };
942 Self::from_hex(format, text)
943 }
944}
945
946#[derive(Debug, Clone, PartialEq, Eq)]
947pub struct ByteString(Vec<u8>);
948
949impl ByteString {
950 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
951 Self(bytes.into())
952 }
953
954 pub fn as_bytes(&self) -> &[u8] {
955 &self.0
956 }
957}
958
959impl From<&str> for ByteString {
960 fn from(value: &str) -> Self {
961 Self(value.as_bytes().to_vec())
962 }
963}
964
965#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
967pub struct FullName(String);
968
969impl FullName {
970 pub fn new(name: impl AsRef<str>) -> Result<Self> {
973 let name = name.as_ref();
974 validate_full_name(name)?;
975 Ok(Self(name.to_string()))
976 }
977
978 pub fn as_str(&self) -> &str {
979 &self.0
980 }
981}
982
983impl fmt::Debug for FullName {
984 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
985 f.debug_tuple("FullName").field(&self.0).finish()
986 }
987}
988
989impl fmt::Display for FullName {
990 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
991 f.write_str(&self.0)
992 }
993}
994
995impl From<FullName> for String {
996 fn from(value: FullName) -> Self {
997 value.0
998 }
999}
1000
1001impl Borrow<str> for FullName {
1002 fn borrow(&self) -> &str {
1003 &self.0
1004 }
1005}
1006
1007impl AsRef<str> for FullName {
1008 fn as_ref(&self) -> &str {
1009 &self.0
1010 }
1011}
1012
1013impl TryFrom<&str> for FullName {
1014 type Error = GitError;
1015
1016 fn try_from(value: &str) -> Result<Self> {
1017 Self::new(value)
1018 }
1019}
1020
1021impl TryFrom<String> for FullName {
1022 type Error = GitError;
1023
1024 fn try_from(value: String) -> Result<Self> {
1025 validate_full_name(&value)?;
1026 Ok(Self(value))
1027 }
1028}
1029
1030impl PartialEq<&str> for FullName {
1031 fn eq(&self, other: &&str) -> bool {
1032 self.0 == *other
1033 }
1034}
1035
1036impl PartialEq<FullName> for &str {
1037 fn eq(&self, other: &FullName) -> bool {
1038 *self == other.0
1039 }
1040}
1041
1042fn validate_full_name(name: &str) -> Result<()> {
1043 if name.is_empty() {
1044 return Err(GitError::InvalidFormat("ref name must not be empty".into()));
1045 }
1046 if name.chars().next().is_some_and(|ch| ch.is_whitespace())
1047 || name.chars().last().is_some_and(|ch| ch.is_whitespace())
1048 {
1049 return Err(GitError::InvalidFormat(
1050 "ref name must not have leading or trailing whitespace".into(),
1051 ));
1052 }
1053 if name.contains("//") {
1054 return Err(GitError::InvalidFormat(
1055 "ref name must not contain consecutive slashes".into(),
1056 ));
1057 }
1058 if name.bytes().any(|byte| byte.is_ascii_control()) {
1059 return Err(GitError::InvalidFormat(
1060 "ref name must not contain control characters".into(),
1061 ));
1062 }
1063 Ok(())
1064}
1065
1066#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
1068pub struct BString(Vec<u8>);
1069
1070impl BString {
1071 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
1072 Self(bytes.into())
1073 }
1074 pub fn from_bytes(bytes: &[u8]) -> Self {
1075 Self(bytes.to_vec())
1076 }
1077 pub fn as_bytes(&self) -> &[u8] {
1078 &self.0
1079 }
1080 pub fn len(&self) -> usize {
1081 self.0.len()
1082 }
1083 pub fn is_empty(&self) -> bool {
1084 self.0.is_empty()
1085 }
1086 pub fn into_bytes(self) -> Vec<u8> {
1087 self.0
1088 }
1089}
1090
1091impl From<&str> for BString {
1092 fn from(v: &str) -> Self {
1093 Self::from_bytes(v.as_bytes())
1094 }
1095}
1096impl From<&[u8]> for BString {
1097 fn from(v: &[u8]) -> Self {
1098 Self::from_bytes(v)
1099 }
1100}
1101impl<const N: usize> From<&[u8; N]> for BString {
1102 fn from(v: &[u8; N]) -> Self {
1103 Self::from_bytes(v.as_slice())
1104 }
1105}
1106impl From<Vec<u8>> for BString {
1107 fn from(v: Vec<u8>) -> Self {
1108 Self(v)
1109 }
1110}
1111impl PartialEq<&[u8]> for BString {
1112 fn eq(&self, o: &&[u8]) -> bool {
1113 self.0.as_slice() == *o
1114 }
1115}
1116impl<const N: usize> PartialEq<&[u8; N]> for BString {
1117 fn eq(&self, o: &&[u8; N]) -> bool {
1118 self.as_bytes() == o.as_slice()
1119 }
1120}
1121impl PartialEq<BString> for &[u8] {
1122 fn eq(&self, o: &BString) -> bool {
1123 *self == o.as_bytes()
1124 }
1125}
1126impl<const N: usize> PartialEq<BString> for &[u8; N] {
1127 fn eq(&self, o: &BString) -> bool {
1128 self.as_slice() == o.as_bytes()
1129 }
1130}
1131impl PartialEq<Vec<u8>> for BString {
1132 fn eq(&self, o: &Vec<u8>) -> bool {
1133 self.0 == *o
1134 }
1135}
1136impl PartialEq<BString> for Vec<u8> {
1137 fn eq(&self, o: &BString) -> bool {
1138 *self == o.0
1139 }
1140}
1141
1142impl fmt::Display for BString {
1143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1144 write!(f, "{}", String::from_utf8_lossy(&self.0))
1145 }
1146}
1147
1148impl Borrow<[u8]> for BString {
1149 fn borrow(&self) -> &[u8] {
1150 self.as_bytes()
1151 }
1152}
1153
1154impl Deref for BString {
1155 type Target = [u8];
1156
1157 fn deref(&self) -> &[u8] {
1158 self.as_bytes()
1159 }
1160}
1161
1162impl AsRef<[u8]> for BString {
1163 fn as_ref(&self) -> &[u8] {
1164 self.as_bytes()
1165 }
1166}
1167
1168#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1169pub struct RepoPath(PathBuf);
1170
1171impl RepoPath {
1172 pub fn new(path: impl Into<PathBuf>) -> Result<Self> {
1173 let path = path.into();
1174 if path.is_absolute() {
1175 return Err(GitError::InvalidPath(
1176 "repository paths must be relative".into(),
1177 ));
1178 }
1179 if path.components().any(|component| {
1180 matches!(
1181 component,
1182 std::path::Component::ParentDir | std::path::Component::Prefix(_)
1183 )
1184 }) {
1185 return Err(GitError::InvalidPath(
1186 "repository paths must not escape".into(),
1187 ));
1188 }
1189 Ok(Self(path))
1190 }
1191
1192 pub fn as_path(&self) -> &Path {
1193 &self.0
1194 }
1195}
1196
1197#[derive(Debug, Clone, PartialEq, Eq)]
1212pub struct Signature {
1213 pub name: ByteString,
1216 pub email: ByteString,
1219 pub time: GitTime,
1221 pub raw: Vec<u8>,
1225}
1226
1227impl Signature {
1228 pub fn from_ident_line(line: &[u8]) -> Option<Self> {
1242 let mail_end = line.iter().rposition(|byte| *byte == b'>')?;
1246 let mail_begin = line[..mail_end].iter().rposition(|byte| *byte == b'<')? + 1;
1247 let email = &line[mail_begin..mail_end];
1248
1249 let mut name_end = mail_begin.saturating_sub(1);
1252 if name_end > 0 && line[name_end - 1] == b' ' {
1253 name_end -= 1;
1254 }
1255 let name = &line[..name_end];
1256
1257 let rest = line.get(mail_end + 1..)?;
1260 let rest = rest.strip_prefix(b" ")?;
1261 let time = GitTime::from_time_fields(rest)?;
1262
1263 Some(Self {
1264 name: ByteString::new(name.to_vec()),
1265 email: ByteString::new(email.to_vec()),
1266 time,
1267 raw: line.to_vec(),
1268 })
1269 }
1270
1271 pub fn to_ident_bytes(&self) -> Vec<u8> {
1278 self.raw.clone()
1279 }
1280
1281 pub fn to_canonical_ident_bytes(&self) -> Vec<u8> {
1290 let mut out = Vec::with_capacity(self.raw.len());
1291 out.extend_from_slice(self.name.as_bytes());
1292 out.extend_from_slice(b" <");
1293 out.extend_from_slice(self.email.as_bytes());
1294 out.extend_from_slice(b"> ");
1295 out.extend_from_slice(self.time.to_ident_suffix().as_bytes());
1296 out
1297 }
1298}
1299
1300pub struct IdentFields<'a> {
1309 pub name: &'a [u8],
1311 pub email: &'a [u8],
1313 pub date: Option<&'a [u8]>,
1316 pub tz: Option<&'a [u8]>,
1318}
1319
1320fn ident_isspace(byte: u8) -> bool {
1325 matches!(byte, b' ' | b'\t' | b'\n' | b'\r')
1326}
1327
1328pub fn split_ident_line(line: &[u8]) -> Option<IdentFields<'_>> {
1333 let len = line.len();
1334 let lt = line.iter().position(|&byte| byte == b'<')?;
1336 let mail_begin = lt + 1;
1337
1338 let mut name_end = mail_begin - 1;
1341 if mail_begin >= 2 {
1342 let mut i = mail_begin - 2;
1343 loop {
1344 if !ident_isspace(line[i]) {
1345 name_end = i + 1;
1346 break;
1347 }
1348 if i == 0 {
1349 break;
1350 }
1351 i -= 1;
1352 }
1353 }
1354 let name = &line[..name_end];
1355
1356 let gt = line[mail_begin..].iter().position(|&byte| byte == b'>')? + mail_begin;
1358 let email = &line[mail_begin..gt];
1359
1360 let person_only = IdentFields {
1361 name,
1362 email,
1363 date: None,
1364 tz: None,
1365 };
1366
1367 let mut cp = len - 1;
1370 while line[cp] != b'>' {
1371 if cp == 0 {
1372 return Some(person_only);
1373 }
1374 cp -= 1;
1375 }
1376 let mut i = cp + 1;
1377 while i < len && ident_isspace(line[i]) {
1378 i += 1;
1379 }
1380 let date_begin = i;
1381 while i < len && line[i].is_ascii_digit() {
1382 i += 1;
1383 }
1384 if i == date_begin {
1385 return Some(person_only);
1386 }
1387 let date = &line[date_begin..i];
1388
1389 while i < len && ident_isspace(line[i]) {
1390 i += 1;
1391 }
1392 if i >= len || (line[i] != b'+' && line[i] != b'-') {
1393 return Some(person_only);
1394 }
1395 let tz_begin = i;
1396 i += 1;
1397 let tz_digits = i;
1398 while i < len && line[i].is_ascii_digit() {
1399 i += 1;
1400 }
1401 if i == tz_digits {
1402 return Some(person_only);
1403 }
1404 Some(IdentFields {
1405 name,
1406 email,
1407 date: Some(date),
1408 tz: Some(&line[tz_begin..i]),
1409 })
1410}
1411
1412fn ident_date_overflows(seconds: u64) -> bool {
1415 seconds >= i64::MAX as u64
1416}
1417
1418pub fn ident_render_date(date: &[u8], tz: &[u8], mode: &DateMode) -> String {
1425 let parsed = std::str::from_utf8(date)
1426 .ok()
1427 .and_then(|text| text.parse::<u64>().ok());
1428 let (seconds, tz_text) = match parsed {
1429 Some(value) if !ident_date_overflows(value) => {
1430 (value as i64, std::str::from_utf8(tz).unwrap_or("+0000"))
1431 }
1432 _ => (0, "+0000"),
1435 };
1436 mode.render(seconds, tz_text).unwrap_or_default()
1437}
1438
1439impl fmt::Display for Signature {
1440 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1444 write!(f, "{}", String::from_utf8_lossy(&self.raw))
1445 }
1446}
1447
1448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1461pub struct GitTime {
1462 pub seconds: i64,
1464 pub timezone_offset_minutes: i16,
1468 pub negative_utc: bool,
1472}
1473
1474impl GitTime {
1475 pub const fn new(seconds: i64, timezone_offset_minutes: i16) -> Self {
1479 Self {
1480 seconds,
1481 timezone_offset_minutes,
1482 negative_utc: false,
1483 }
1484 }
1485
1486 pub const fn with_negative_utc(seconds: i64) -> Self {
1489 Self {
1490 seconds,
1491 timezone_offset_minutes: 0,
1492 negative_utc: true,
1493 }
1494 }
1495
1496 fn from_time_fields(bytes: &[u8]) -> Option<Self> {
1500 let text = std::str::from_utf8(bytes).ok()?;
1501 let (seconds_text, tz_text) = text.split_once(' ')?;
1502 let seconds = seconds_text.parse::<i64>().ok()?;
1503 let (timezone_offset_minutes, negative_utc) = parse_timezone_token(tz_text)?;
1504 Some(Self {
1505 seconds,
1506 timezone_offset_minutes,
1507 negative_utc,
1508 })
1509 }
1510
1511 fn to_ident_suffix(self) -> String {
1514 format!("{} {}", self.seconds, self.offset_token())
1515 }
1516
1517 pub fn offset_token(self) -> String {
1521 let sign = if self.negative_utc || self.timezone_offset_minutes < 0 {
1522 '-'
1523 } else {
1524 '+'
1525 };
1526 let magnitude = self.timezone_offset_minutes.unsigned_abs();
1527 format!("{sign}{:02}{:02}", magnitude / 60, magnitude % 60)
1528 }
1529}
1530
1531fn parse_timezone_token(token: &str) -> Option<(i16, bool)> {
1537 let bytes = token.as_bytes();
1538 if bytes.len() != 5 {
1539 return None;
1540 }
1541 let negative = match bytes[0] {
1542 b'+' => false,
1543 b'-' => true,
1544 _ => return None,
1545 };
1546 if !bytes[1..].iter().all(u8::is_ascii_digit) {
1547 return None;
1548 }
1549 let hours = i16::from(bytes[1] - b'0') * 10 + i16::from(bytes[2] - b'0');
1550 let minutes = i16::from(bytes[3] - b'0') * 10 + i16::from(bytes[4] - b'0');
1551 let total = hours * 60 + minutes;
1552 let negative_utc = negative && total == 0;
1553 let signed = if negative { -total } else { total };
1554 Some((signed, negative_utc))
1555}
1556
1557#[derive(Debug, Clone, PartialEq, Eq)]
1558pub struct Capability {
1559 pub name: String,
1560 pub value: Option<String>,
1561}
1562
1563#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1564pub enum MissingObjectKind {
1565 Object,
1566 Blob,
1567 Tree,
1568 Commit,
1569 Tag,
1570}
1571
1572impl MissingObjectKind {
1573 pub const fn as_str(self) -> &'static str {
1574 match self {
1575 Self::Object => "object",
1576 Self::Blob => "blob",
1577 Self::Tree => "tree",
1578 Self::Commit => "commit",
1579 Self::Tag => "tag",
1580 }
1581 }
1582}
1583
1584impl fmt::Display for MissingObjectKind {
1585 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1586 f.write_str(self.as_str())
1587 }
1588}
1589
1590#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1591pub enum MissingObjectContext {
1592 Read,
1593 Traversal,
1594 PackInstall,
1595 RevisionWalk,
1596 WorktreeMaterialize,
1597 RemoteBoundary,
1598}
1599
1600impl MissingObjectContext {
1601 pub const fn as_str(self) -> &'static str {
1602 match self {
1603 Self::Read => "read",
1604 Self::Traversal => "traversal",
1605 Self::PackInstall => "pack-install",
1606 Self::RevisionWalk => "revision-walk",
1607 Self::WorktreeMaterialize => "worktree-materialize",
1608 Self::RemoteBoundary => "remote-boundary",
1609 }
1610 }
1611}
1612
1613impl fmt::Display for MissingObjectContext {
1614 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1615 f.write_str(self.as_str())
1616 }
1617}
1618
1619#[derive(Debug, Clone, PartialEq, Eq)]
1620pub enum NotFoundKind {
1621 Message(String),
1622 Remote {
1623 name: String,
1624 },
1625 Object {
1626 oid: ObjectId,
1627 kind: MissingObjectKind,
1628 context: Option<MissingObjectContext>,
1629 },
1630 Reference {
1631 name: String,
1632 },
1633 Repository {
1634 path: String,
1635 },
1636}
1637
1638impl fmt::Display for NotFoundKind {
1639 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1640 match self {
1641 Self::Message(msg) => write!(f, "{msg}"),
1642 Self::Remote { name } => write!(f, "remote {name}"),
1643 Self::Object {
1644 oid,
1645 kind: MissingObjectKind::Object,
1646 ..
1647 } => write!(f, "object {oid}"),
1648 Self::Object { oid, kind, .. } => write!(f, "{kind} object {oid}"),
1649 Self::Reference { name } => write!(f, "{name}"),
1650 Self::Repository { path } => write!(f, "{path}"),
1651 }
1652 }
1653}
1654
1655impl NotFoundKind {
1656 pub fn object_id(&self) -> Option<ObjectId> {
1657 match self {
1658 Self::Object { oid, .. } => Some(*oid),
1659 _ => None,
1660 }
1661 }
1662
1663 pub fn missing_object_kind(&self) -> Option<MissingObjectKind> {
1664 match self {
1665 Self::Object { kind, .. } => Some(*kind),
1666 _ => None,
1667 }
1668 }
1669
1670 pub fn missing_object_context(&self) -> Option<MissingObjectContext> {
1671 match self {
1672 Self::Object { context, .. } => *context,
1673 _ => None,
1674 }
1675 }
1676}
1677
1678#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1680pub enum CliExit {
1681 Ok,
1683 UserError,
1685 Usage,
1687 Custom(i32),
1689}
1690
1691impl CliExit {
1692 pub const fn code(self) -> i32 {
1693 match self {
1694 Self::Ok => 0,
1695 Self::UserError => 128,
1696 Self::Usage => 129,
1697 Self::Custom(code) => code,
1698 }
1699 }
1700}
1701
1702#[derive(Debug, Clone, PartialEq, Eq)]
1703pub enum GitError {
1704 Io(String),
1705 InvalidObjectId(String),
1706 InvalidObject(String),
1707 InvalidFormat(String),
1708 InvalidPath(String),
1709 Unsupported(String),
1710 NotFound(NotFoundKind),
1711 Transaction(String),
1712 Command(String),
1713 Cli(CliExit, String),
1715 Exit(i32),
1717}
1718
1719pub type Result<T> = std::result::Result<T, GitError>;
1720
1721impl fmt::Display for GitError {
1722 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1723 match self {
1724 Self::Io(msg) => write!(f, "io error: {msg}"),
1725 Self::InvalidObjectId(msg) => write!(f, "invalid object id: {msg}"),
1726 Self::InvalidObject(msg) => write!(f, "invalid object: {msg}"),
1727 Self::InvalidFormat(msg) => write!(f, "invalid format: {msg}"),
1728 Self::InvalidPath(msg) => write!(f, "invalid path: {msg}"),
1729 Self::Unsupported(msg) => write!(f, "unsupported: {msg}"),
1730 Self::NotFound(kind) => write!(f, "not found: {kind}"),
1731 Self::Transaction(msg) => write!(f, "transaction failed: {msg}"),
1732 Self::Command(msg) => write!(f, "command failed: {msg}"),
1733 Self::Cli(_, msg) => f.write_str(msg),
1734 Self::Exit(code) => write!(f, "exit {code}"),
1735 }
1736 }
1737}
1738
1739impl Error for GitError {}
1740
1741impl GitError {
1742 pub fn usage(msg: impl Into<String>) -> Self {
1743 Self::Cli(CliExit::Usage, msg.into())
1744 }
1745
1746 pub fn user_error(msg: impl Into<String>) -> Self {
1747 Self::Cli(CliExit::UserError, msg.into())
1748 }
1749
1750 pub fn cli_exit(kind: CliExit, msg: impl Into<String>) -> Self {
1751 Self::Cli(kind, msg.into())
1752 }
1753
1754 pub fn cli_exit_code(&self) -> i32 {
1755 cli_exit_code(self)
1756 }
1757
1758 pub fn not_found(msg: impl Into<String>) -> Self {
1759 Self::NotFound(NotFoundKind::Message(msg.into()))
1760 }
1761
1762 pub fn remote_not_found(name: impl Into<String>) -> Self {
1763 Self::NotFound(NotFoundKind::Remote { name: name.into() })
1764 }
1765
1766 pub fn object_not_found(oid: ObjectId) -> Self {
1767 Self::object_kind_not_found(oid, MissingObjectKind::Object)
1768 }
1769
1770 pub fn object_kind_not_found(oid: ObjectId, kind: MissingObjectKind) -> Self {
1771 Self::NotFound(NotFoundKind::Object {
1772 oid,
1773 kind,
1774 context: None,
1775 })
1776 }
1777
1778 pub fn object_not_found_in(oid: ObjectId, context: MissingObjectContext) -> Self {
1779 Self::object_kind_not_found_in(oid, MissingObjectKind::Object, context)
1780 }
1781
1782 pub fn object_kind_not_found_in(
1783 oid: ObjectId,
1784 kind: MissingObjectKind,
1785 context: MissingObjectContext,
1786 ) -> Self {
1787 Self::NotFound(NotFoundKind::Object {
1788 oid,
1789 kind,
1790 context: Some(context),
1791 })
1792 }
1793
1794 pub fn reference_not_found(name: impl Into<String>) -> Self {
1795 Self::NotFound(NotFoundKind::Reference { name: name.into() })
1796 }
1797
1798 pub fn repository_not_found(path: impl Into<String>) -> Self {
1799 Self::NotFound(NotFoundKind::Repository { path: path.into() })
1800 }
1801
1802 pub fn not_found_kind(&self) -> Option<&NotFoundKind> {
1803 match self {
1804 Self::NotFound(kind) => Some(kind),
1805 _ => None,
1806 }
1807 }
1808}
1809
1810impl From<std::io::Error> for GitError {
1811 fn from(value: std::io::Error) -> Self {
1812 Self::Io(value.to_string())
1813 }
1814}
1815
1816pub fn cli_exit_code(err: &GitError) -> i32 {
1818 match err {
1819 GitError::Exit(code) => *code,
1820 GitError::Cli(kind, _) => kind.code(),
1821 GitError::Command(_) => 1,
1824 _ => 1,
1825 }
1826}
1827
1828pub fn object_id_for_bytes(
1829 format: ObjectFormat,
1830 object_type: &str,
1831 body: &[u8],
1832) -> Result<ObjectId> {
1833 match format {
1834 ObjectFormat::Sha1 => ObjectId::from_raw(format, &sha1_object_digest(object_type, body)),
1838 ObjectFormat::Sha256 => {
1839 let mut framed = Vec::with_capacity(object_type.len() + body.len() + 32);
1840 framed.extend_from_slice(object_type.as_bytes());
1841 framed.push(b' ');
1842 framed.extend_from_slice(body.len().to_string().as_bytes());
1843 framed.push(0);
1844 framed.extend_from_slice(body);
1845 ObjectId::from_raw(format, &sha256(&framed))
1846 }
1847 }
1848}
1849
1850pub fn digest_bytes(format: ObjectFormat, bytes: &[u8]) -> Result<ObjectId> {
1851 match format {
1852 ObjectFormat::Sha1 => ObjectId::from_raw(format, &sha1(bytes)),
1853 ObjectFormat::Sha256 => ObjectId::from_raw(format, &sha256(bytes)),
1854 }
1855}
1856
1857pub struct StreamingDigest {
1858 format: ObjectFormat,
1859 inner: StreamingDigestInner,
1860}
1861
1862enum StreamingDigestInner {
1863 #[cfg(not(feature = "fast-sha1"))]
1864 Sha1(Sha1Hasher),
1865 #[cfg(feature = "fast-sha1")]
1866 Sha1(sha1::Sha1),
1867 Sha256(Sha256Hasher),
1868}
1869
1870impl StreamingDigest {
1871 pub fn new(format: ObjectFormat) -> Self {
1872 let inner = match format {
1873 #[cfg(not(feature = "fast-sha1"))]
1874 ObjectFormat::Sha1 => StreamingDigestInner::Sha1(Sha1Hasher::new()),
1875 #[cfg(feature = "fast-sha1")]
1876 ObjectFormat::Sha1 => {
1877 use sha1::Digest;
1878 StreamingDigestInner::Sha1(sha1::Sha1::new())
1879 }
1880 ObjectFormat::Sha256 => StreamingDigestInner::Sha256(Sha256Hasher::new()),
1881 };
1882 Self { format, inner }
1883 }
1884
1885 pub fn update(&mut self, data: &[u8]) {
1886 match &mut self.inner {
1887 #[cfg(not(feature = "fast-sha1"))]
1888 StreamingDigestInner::Sha1(hasher) => hasher.update(data),
1889 #[cfg(feature = "fast-sha1")]
1890 StreamingDigestInner::Sha1(hasher) => {
1891 use sha1::Digest;
1892 hasher.update(data);
1893 }
1894 StreamingDigestInner::Sha256(hasher) => hasher.update(data),
1895 }
1896 }
1897
1898 pub fn finalize(self) -> Result<ObjectId> {
1899 match self.inner {
1900 #[cfg(not(feature = "fast-sha1"))]
1901 StreamingDigestInner::Sha1(hasher) => {
1902 ObjectId::from_raw(self.format, &hasher.finalize())
1903 }
1904 #[cfg(feature = "fast-sha1")]
1905 StreamingDigestInner::Sha1(hasher) => {
1906 use sha1::Digest;
1907 let bytes: [u8; 20] = hasher.finalize().into();
1908 ObjectId::from_raw(self.format, &bytes)
1909 }
1910 StreamingDigestInner::Sha256(hasher) => {
1911 ObjectId::from_raw(self.format, &hasher.finalize())
1912 }
1913 }
1914 }
1915}
1916
1917pub fn to_hex(bytes: &[u8]) -> String {
1918 let mut out = String::with_capacity(bytes.len() * 2);
1919 write_hex_bytes(bytes, &mut out).expect("writing hex to a String cannot fail");
1920 out
1921}
1922
1923fn write_hex_bytes(bytes: &[u8], out: &mut impl fmt::Write) -> fmt::Result {
1924 const HEX: &[u8; 16] = b"0123456789abcdef";
1925 for byte in bytes {
1926 out.write_char(HEX[(byte >> 4) as usize] as char)?;
1927 out.write_char(HEX[(byte & 0x0f) as usize] as char)?;
1928 }
1929 Ok(())
1930}
1931
1932fn hex_nibble_value(byte: u8) -> Option<u8> {
1933 match byte {
1934 b'0'..=b'9' => Some(byte - b'0'),
1935 b'a'..=b'f' => Some(byte - b'a' + 10),
1936 b'A'..=b'F' => Some(byte - b'A' + 10),
1937 _ => None,
1938 }
1939}
1940
1941fn hex_nibble(byte: u8) -> Result<u8> {
1942 hex_nibble_value(byte)
1943 .ok_or_else(|| GitError::InvalidObjectId(format!("non-hex byte {:?}", byte as char)))
1944}
1945
1946#[cfg(not(feature = "fast-sha1"))]
1958fn sha1(input: &[u8]) -> [u8; 20] {
1959 let mut hasher = Sha1Hasher::new();
1960 hasher.update(input);
1961 hasher.finalize()
1962}
1963
1964#[cfg(feature = "fast-sha1")]
1966fn sha1(input: &[u8]) -> [u8; 20] {
1967 use sha1::{Digest, Sha1};
1968 let mut hasher = Sha1::new();
1969 hasher.update(input);
1970 hasher.finalize().into()
1971}
1972
1973#[cfg(not(feature = "fast-sha1"))]
1976fn sha1_object_digest(object_type: &str, body: &[u8]) -> [u8; 20] {
1977 let mut hasher = Sha1Hasher::new();
1978 hasher.update(object_type.as_bytes());
1979 hasher.update(b" ");
1980 hasher.update(body.len().to_string().as_bytes());
1981 hasher.update(&[0u8]);
1982 hasher.update(body);
1983 hasher.finalize()
1984}
1985
1986#[cfg(feature = "fast-sha1")]
1987fn sha1_object_digest(object_type: &str, body: &[u8]) -> [u8; 20] {
1988 use sha1::{Digest, Sha1};
1989 let mut hasher = Sha1::new();
1990 hasher.update(object_type.as_bytes());
1991 hasher.update(b" ");
1992 hasher.update(body.len().to_string().as_bytes());
1993 hasher.update([0u8]);
1994 hasher.update(body);
1995 hasher.finalize().into()
1996}
1997
1998#[cfg(not(feature = "fast-sha1"))]
2002struct Sha1Hasher {
2003 state: [u32; 5],
2004 block: [u8; 64],
2005 block_len: usize,
2006 total_len: u64,
2007}
2008
2009#[cfg(not(feature = "fast-sha1"))]
2010impl Sha1Hasher {
2011 fn new() -> Self {
2012 Self {
2013 state: [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0],
2014 block: [0u8; 64],
2015 block_len: 0,
2016 total_len: 0,
2017 }
2018 }
2019
2020 fn update(&mut self, mut data: &[u8]) {
2021 self.total_len = self.total_len.wrapping_add(data.len() as u64);
2022 if self.block_len > 0 {
2023 let take = (64 - self.block_len).min(data.len());
2024 self.block[self.block_len..self.block_len + take].copy_from_slice(&data[..take]);
2025 self.block_len += take;
2026 data = &data[take..];
2027 if self.block_len == 64 {
2028 let block = self.block;
2029 sha1_compress(&mut self.state, &block);
2030 self.block_len = 0;
2031 }
2032 }
2033 while data.len() >= 64 {
2034 sha1_compress(&mut self.state, &data[..64]);
2035 data = &data[64..];
2036 }
2037 if !data.is_empty() {
2038 self.block[..data.len()].copy_from_slice(data);
2039 self.block_len = data.len();
2040 }
2041 }
2042
2043 fn finalize(mut self) -> [u8; 20] {
2044 let bit_len = self.total_len.wrapping_mul(8);
2045 let mut tail = [0u8; 128];
2048 tail[..self.block_len].copy_from_slice(&self.block[..self.block_len]);
2049 tail[self.block_len] = 0x80;
2050 let total = if self.block_len < 56 { 64 } else { 128 };
2051 tail[total - 8..total].copy_from_slice(&bit_len.to_be_bytes());
2052 sha1_compress(&mut self.state, &tail[..64]);
2053 if total == 128 {
2054 sha1_compress(&mut self.state, &tail[64..128]);
2055 }
2056 let mut out = [0u8; 20];
2057 out[0..4].copy_from_slice(&self.state[0].to_be_bytes());
2058 out[4..8].copy_from_slice(&self.state[1].to_be_bytes());
2059 out[8..12].copy_from_slice(&self.state[2].to_be_bytes());
2060 out[12..16].copy_from_slice(&self.state[3].to_be_bytes());
2061 out[16..20].copy_from_slice(&self.state[4].to_be_bytes());
2062 out
2063 }
2064}
2065
2066#[cfg(not(feature = "fast-sha1"))]
2068fn sha1_compress(state: &mut [u32; 5], block: &[u8]) {
2069 let mut w = [0u32; 80];
2070 for (i, word) in w.iter_mut().take(16).enumerate() {
2071 let offset = i * 4;
2072 *word = u32::from_be_bytes([
2073 block[offset],
2074 block[offset + 1],
2075 block[offset + 2],
2076 block[offset + 3],
2077 ]);
2078 }
2079 for i in 16..80 {
2080 w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
2081 }
2082
2083 let mut a = state[0];
2084 let mut b = state[1];
2085 let mut c = state[2];
2086 let mut d = state[3];
2087 let mut e = state[4];
2088
2089 for (i, word) in w.iter().enumerate() {
2090 let (f, k) = match i {
2091 0..=19 => ((b & c) | ((!b) & d), 0x5a827999u32),
2092 20..=39 => (b ^ c ^ d, 0x6ed9eba1),
2093 40..=59 => ((b & c) | (b & d) | (c & d), 0x8f1bbcdc),
2094 _ => (b ^ c ^ d, 0xca62c1d6),
2095 };
2096 let temp = a
2097 .rotate_left(5)
2098 .wrapping_add(f)
2099 .wrapping_add(e)
2100 .wrapping_add(k)
2101 .wrapping_add(*word);
2102 e = d;
2103 d = c;
2104 c = b.rotate_left(30);
2105 b = a;
2106 a = temp;
2107 }
2108
2109 state[0] = state[0].wrapping_add(a);
2110 state[1] = state[1].wrapping_add(b);
2111 state[2] = state[2].wrapping_add(c);
2112 state[3] = state[3].wrapping_add(d);
2113 state[4] = state[4].wrapping_add(e);
2114}
2115
2116fn sha256(input: &[u8]) -> [u8; 32] {
2117 let mut hasher = Sha256Hasher::new();
2118 hasher.update(input);
2119 hasher.finalize()
2120}
2121
2122struct Sha256Hasher {
2123 state: [u32; 8],
2124 block: [u8; 64],
2125 block_len: usize,
2126 total_len: u64,
2127}
2128
2129impl Sha256Hasher {
2130 const K: [u32; 64] = [
2131 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
2132 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
2133 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
2134 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
2135 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
2136 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
2137 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
2138 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
2139 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
2140 0xc67178f2,
2141 ];
2142
2143 fn new() -> Self {
2144 Self {
2145 state: [
2146 0x6a09e667u32,
2147 0xbb67ae85,
2148 0x3c6ef372,
2149 0xa54ff53a,
2150 0x510e527f,
2151 0x9b05688c,
2152 0x1f83d9ab,
2153 0x5be0cd19,
2154 ],
2155 block: [0u8; 64],
2156 block_len: 0,
2157 total_len: 0,
2158 }
2159 }
2160
2161 fn update(&mut self, mut data: &[u8]) {
2162 self.total_len = self.total_len.wrapping_add(data.len() as u64);
2163 if self.block_len > 0 {
2164 let take = (64 - self.block_len).min(data.len());
2165 self.block[self.block_len..self.block_len + take].copy_from_slice(&data[..take]);
2166 self.block_len += take;
2167 data = &data[take..];
2168 if self.block_len == 64 {
2169 let block = self.block;
2170 self.compress(&block);
2171 self.block_len = 0;
2172 }
2173 }
2174 while data.len() >= 64 {
2175 self.compress(&data[..64]);
2176 data = &data[64..];
2177 }
2178 if !data.is_empty() {
2179 self.block[..data.len()].copy_from_slice(data);
2180 self.block_len = data.len();
2181 }
2182 }
2183
2184 fn finalize(mut self) -> [u8; 32] {
2185 let bit_len = self.total_len.wrapping_mul(8);
2186 let mut tail = [0u8; 128];
2187 tail[..self.block_len].copy_from_slice(&self.block[..self.block_len]);
2188 tail[self.block_len] = 0x80;
2189 let total = if self.block_len < 56 { 64 } else { 128 };
2190 tail[total - 8..total].copy_from_slice(&bit_len.to_be_bytes());
2191 self.compress(&tail[..64]);
2192 if total == 128 {
2193 self.compress(&tail[64..128]);
2194 }
2195
2196 let mut out = [0; 32];
2197 for (idx, word) in self.state.iter().enumerate() {
2198 out[idx * 4..idx * 4 + 4].copy_from_slice(&word.to_be_bytes());
2199 }
2200 out
2201 }
2202
2203 fn compress(&mut self, chunk: &[u8]) {
2204 let mut w = [0u32; 64];
2205 for (i, word) in w.iter_mut().take(16).enumerate() {
2206 let offset = i * 4;
2207 *word = u32::from_be_bytes([
2208 chunk[offset],
2209 chunk[offset + 1],
2210 chunk[offset + 2],
2211 chunk[offset + 3],
2212 ]);
2213 }
2214 for i in 16..64 {
2215 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
2216 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
2217 w[i] = w[i - 16]
2218 .wrapping_add(s0)
2219 .wrapping_add(w[i - 7])
2220 .wrapping_add(s1);
2221 }
2222
2223 let mut a = self.state[0];
2224 let mut b = self.state[1];
2225 let mut c = self.state[2];
2226 let mut d = self.state[3];
2227 let mut e = self.state[4];
2228 let mut f = self.state[5];
2229 let mut g = self.state[6];
2230 let mut hh = self.state[7];
2231
2232 for (&word, &constant) in w.iter().zip(Self::K.iter()) {
2233 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
2234 let ch = (e & f) ^ ((!e) & g);
2235 let temp1 = hh
2236 .wrapping_add(s1)
2237 .wrapping_add(ch)
2238 .wrapping_add(constant)
2239 .wrapping_add(word);
2240 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
2241 let maj = (a & b) ^ (a & c) ^ (b & c);
2242 let temp2 = s0.wrapping_add(maj);
2243
2244 hh = g;
2245 g = f;
2246 f = e;
2247 e = d.wrapping_add(temp1);
2248 d = c;
2249 c = b;
2250 b = a;
2251 a = temp1.wrapping_add(temp2);
2252 }
2253
2254 self.state[0] = self.state[0].wrapping_add(a);
2255 self.state[1] = self.state[1].wrapping_add(b);
2256 self.state[2] = self.state[2].wrapping_add(c);
2257 self.state[3] = self.state[3].wrapping_add(d);
2258 self.state[4] = self.state[4].wrapping_add(e);
2259 self.state[5] = self.state[5].wrapping_add(f);
2260 self.state[6] = self.state[6].wrapping_add(g);
2261 self.state[7] = self.state[7].wrapping_add(hh);
2262 }
2263}
2264
2265#[cfg(test)]
2266mod tests {
2267 use super::*;
2268
2269 #[test]
2270 fn sha1_blob_matches_git_known_value() {
2271 let oid = object_id_for_bytes(ObjectFormat::Sha1, "blob", b"hello\n")
2272 .expect("known blob should hash as sha1");
2273 assert_eq!(oid.to_hex(), "ce013625030ba8dba906f756967f9e9ca394464a");
2274 }
2275
2276 #[test]
2277 fn sha256_blob_matches_git_known_value() {
2278 let oid = object_id_for_bytes(ObjectFormat::Sha256, "blob", b"hello\n")
2279 .expect("known blob should hash as sha256");
2280 assert_eq!(
2281 oid.to_hex(),
2282 "2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4"
2283 );
2284 }
2285
2286 #[test]
2287 fn object_id_round_trips_hex() {
2288 let oid = ObjectId::from_hex(
2289 ObjectFormat::Sha1,
2290 "ce013625030ba8dba906f756967f9e9ca394464a",
2291 )
2292 .expect("valid sha1 hex");
2293 assert_eq!(oid.to_hex(), "ce013625030ba8dba906f756967f9e9ca394464a");
2294 }
2295
2296 #[test]
2297 fn object_id_writes_hex_without_allocating_in_the_writer() {
2298 let oid = ObjectId::from_hex(
2299 ObjectFormat::Sha1,
2300 "CE013625030BA8DBA906F756967F9E9CA394464A",
2301 )
2302 .expect("valid uppercase sha1 hex");
2303
2304 let mut out = String::new();
2305 oid.write_hex(&mut out)
2306 .expect("writing object id hex to a String should not fail");
2307
2308 assert_eq!(out, "ce013625030ba8dba906f756967f9e9ca394464a");
2309 assert_eq!(oid.to_hex(), out);
2310 assert_eq!(format!("{oid}"), out);
2311 }
2312
2313 #[test]
2314 fn object_id_matches_hex_prefixes_by_nibble() {
2315 let oid = ObjectId::from_hex(
2316 ObjectFormat::Sha1,
2317 "ce013625030ba8dba906f756967f9e9ca394464a",
2318 )
2319 .expect("valid sha1 hex");
2320
2321 assert!(oid.hex_prefix_matches(b""));
2322 assert!(oid.hex_prefix_matches(b"c"));
2323 assert!(oid.hex_prefix_matches(b"ce013"));
2324 assert!(oid.hex_prefix_matches(b"CE013625"));
2325 assert!(oid.hex_prefix_matches(b"ce013625030ba8dba906f756967f9e9ca394464a"));
2326
2327 assert!(!oid.hex_prefix_matches(b"d"));
2328 assert!(!oid.hex_prefix_matches(b"ce014"));
2329 assert!(!oid.hex_prefix_matches(b"ce01x"));
2330
2331 let mut too_long = oid.to_hex();
2332 too_long.push('0');
2333 assert!(!oid.hex_prefix_matches(too_long.as_bytes()));
2334 }
2335
2336 #[test]
2337 fn object_id_abbrev_hex_len_clamps_to_format_width() {
2338 let sha1 = ObjectId::null(ObjectFormat::Sha1);
2339 let sha256 = ObjectId::null(ObjectFormat::Sha256);
2340
2341 assert_eq!(sha1.abbrev_hex_len(0), 0);
2342 assert_eq!(sha1.abbrev_hex_len(12), 12);
2343 assert_eq!(sha1.abbrev_hex_len(80), ObjectFormat::Sha1.hex_len());
2344 assert_eq!(sha256.abbrev_hex_len(80), ObjectFormat::Sha256.hex_len());
2345 }
2346
2347 #[test]
2348 fn signature_parses_a_normal_ident_and_round_trips() {
2349 let line = b"A U Thor <author@example.com> 1700000000 +0000";
2350 let sig = Signature::from_ident_line(line).expect("well-formed ident parses");
2351 assert_eq!(sig.name.as_bytes(), b"A U Thor");
2352 assert_eq!(sig.email.as_bytes(), b"author@example.com");
2353 assert_eq!(sig.time.seconds, 1_700_000_000);
2354 assert_eq!(sig.time.timezone_offset_minutes, 0);
2355 assert!(!sig.time.negative_utc);
2356 assert_eq!(sig.to_ident_bytes(), line);
2358 assert_eq!(sig.to_canonical_ident_bytes(), line);
2359 }
2360
2361 #[test]
2362 fn signature_parses_positive_half_hour_offset() {
2363 let line = b"Half Hour <hh@example.com> 1500000000 +0530";
2364 let sig = Signature::from_ident_line(line).expect("offset ident parses");
2365 assert_eq!(sig.time.timezone_offset_minutes, 330);
2366 assert!(!sig.time.negative_utc);
2367 assert_eq!(sig.time.offset_token(), "+0530");
2368 assert_eq!(sig.to_ident_bytes(), line);
2369 assert_eq!(sig.to_canonical_ident_bytes(), line);
2370 }
2371
2372 #[test]
2373 fn signature_parses_negative_offset() {
2374 let line = b"Western <w@example.com> 1500000000 -0500";
2375 let sig = Signature::from_ident_line(line).expect("negative offset parses");
2376 assert_eq!(sig.time.timezone_offset_minutes, -300);
2377 assert!(!sig.time.negative_utc);
2378 assert_eq!(sig.time.offset_token(), "-0500");
2379 assert_eq!(sig.to_ident_bytes(), line);
2380 }
2381
2382 #[test]
2383 fn signature_preserves_negative_zero_timezone_distinct_from_positive_zero() {
2384 let negative = b"Unknown Zone <uz@example.com> 1500000000 -0000";
2385 let positive = b"Known Zone <kz@example.com> 1500000000 +0000";
2386
2387 let neg = Signature::from_ident_line(negative).expect("-0000 parses");
2388 let pos = Signature::from_ident_line(positive).expect("+0000 parses");
2389
2390 assert_eq!(neg.time.timezone_offset_minutes, 0);
2392 assert_eq!(pos.time.timezone_offset_minutes, 0);
2393 assert!(neg.time.negative_utc);
2395 assert!(!pos.time.negative_utc);
2396 assert_ne!(neg.time, pos.time);
2397
2398 assert_eq!(neg.time.offset_token(), "-0000");
2400 assert_eq!(pos.time.offset_token(), "+0000");
2401 assert_eq!(neg.to_ident_bytes(), negative);
2402 assert_eq!(pos.to_ident_bytes(), positive);
2403 assert_eq!(neg.to_canonical_ident_bytes(), negative);
2404 assert_eq!(pos.to_canonical_ident_bytes(), positive);
2405 assert_ne!(neg.to_ident_bytes(), pos.to_ident_bytes());
2406 }
2407
2408 #[test]
2409 fn signature_handles_empty_name_and_email() {
2410 let line = b" <> 0 +0000";
2413 let sig = Signature::from_ident_line(line).expect("empty name/email parses");
2414 assert_eq!(sig.name.as_bytes(), b"");
2415 assert_eq!(sig.email.as_bytes(), b"");
2416 assert_eq!(sig.time.seconds, 0);
2417 assert_eq!(sig.to_ident_bytes(), line);
2418 }
2419
2420 #[test]
2421 fn signature_keeps_angle_brackets_inside_the_name() {
2422 let line = b"Weird <Name> <weird@example.com> 1 +0000";
2426 let sig = Signature::from_ident_line(line).expect("bracketed name parses");
2427 assert_eq!(sig.name.as_bytes(), b"Weird <Name>");
2428 assert_eq!(sig.email.as_bytes(), b"weird@example.com");
2429 assert_eq!(sig.to_ident_bytes(), line);
2430 }
2431
2432 #[test]
2433 fn signature_round_trips_non_canonical_whitespace_via_raw() {
2434 let line = b"Spaced <spaced@example.com> 5 +0000";
2438 let sig = Signature::from_ident_line(line).expect("non-canonical ident parses");
2439 assert_eq!(sig.name.as_bytes(), b"Spaced ");
2441 assert_eq!(sig.to_ident_bytes(), line);
2442 }
2443
2444 #[test]
2445 fn signature_rejects_malformed_idents() {
2446 assert!(Signature::from_ident_line(b"No Email Here 0 +0000").is_none());
2448 assert!(Signature::from_ident_line(b"A U Thor <a@example.com>").is_none());
2450 assert!(Signature::from_ident_line(b"A U Thor <a@example.com> later +0000").is_none());
2452 assert!(Signature::from_ident_line(b"A U Thor <a@example.com> 0 +00").is_none());
2454 assert!(Signature::from_ident_line(b"A U Thor <a@example.com> 0 0000").is_none());
2456 }
2457
2458 #[test]
2459 fn git_time_constructors_set_the_sentinel() {
2460 assert!(!GitTime::new(0, 0).negative_utc);
2461 assert_eq!(GitTime::new(0, 330).offset_token(), "+0530");
2462 let unknown = GitTime::with_negative_utc(42);
2463 assert!(unknown.negative_utc);
2464 assert_eq!(unknown.seconds, 42);
2465 assert_eq!(unknown.offset_token(), "-0000");
2466 }
2467
2468 #[test]
2469 fn full_name_accepts_valid_ref_names() {
2470 let name = FullName::new("refs/heads/main").expect("valid ref name");
2471 assert_eq!(name.as_str(), "refs/heads/main");
2472 assert_eq!(name, "refs/heads/main");
2473 assert_eq!(format!("{name}"), "refs/heads/main");
2474 assert_eq!(String::from(name.clone()), "refs/heads/main");
2475 let borrowed: &str = name.borrow();
2476 assert_eq!(borrowed, "refs/heads/main");
2477 }
2478
2479 #[test]
2480 fn full_name_rejects_invalid_ref_names() {
2481 assert!(FullName::new("").is_err());
2482 assert!(FullName::new(" refs/heads/main").is_err());
2483 assert!(FullName::new("refs/heads/main ").is_err());
2484 assert!(FullName::new("refs//heads/main").is_err());
2485 assert!(FullName::new("refs/heads/\nmain").is_err());
2486 }
2487
2488 #[test]
2489 fn cli_exit_codes_match_git_taxonomy() {
2490 assert_eq!(CliExit::Ok.code(), 0);
2491 assert_eq!(CliExit::UserError.code(), 128);
2492 assert_eq!(CliExit::Usage.code(), 129);
2493 assert_eq!(CliExit::Custom(1).code(), 1);
2494 assert_eq!(CliExit::Custom(5).code(), 5);
2495 }
2496
2497 #[test]
2498 fn git_error_cli_exit_code_mapping() {
2499 assert_eq!(GitError::Exit(129).cli_exit_code(), 129);
2500 assert_eq!(GitError::Exit(128).cli_exit_code(), 128);
2501 assert_eq!(GitError::usage("unknown option").cli_exit_code(), 129);
2502 assert_eq!(
2503 GitError::user_error("not a git repository").cli_exit_code(),
2504 128
2505 );
2506 assert_eq!(
2507 GitError::cli_exit(CliExit::Custom(2), "diff found changes").cli_exit_code(),
2508 2
2509 );
2510 assert_eq!(GitError::Command("bad value".into()).cli_exit_code(), 1);
2511 assert_eq!(GitError::not_found("missing ref").cli_exit_code(), 1);
2512 }
2513
2514 #[test]
2515 fn git_error_cli_displays_message_only() {
2516 let err = GitError::usage("unknown option `--foo'");
2517 assert_eq!(err.to_string(), "unknown option `--foo'");
2518 }
2519
2520 #[test]
2521 fn bstring_round_trips_bytes_and_displays_lossily() {
2522 let path = BString::from_bytes(b"src/\xFF.txt");
2523 assert_eq!(path.as_bytes(), b"src/\xFF.txt");
2524 let borrowed: &[u8] = path.borrow();
2525 assert_eq!(borrowed, b"src/\xFF.txt".as_slice());
2526 assert_eq!(format!("{path}"), "src/\u{FFFD}.txt");
2527 assert_eq!(path, b"src/\xFF.txt");
2528 assert_eq!(path.clone().into_bytes(), b"src/\xFF.txt".to_vec());
2529 }
2530
2531 #[test]
2532 fn split_ident_line_parses_well_formed_ident() {
2533 let f = split_ident_line(b"A U Thor <author@example.com> 1112911993 -0700")
2534 .expect("well formed ident should parse");
2535 assert_eq!(f.name, b"A U Thor");
2536 assert_eq!(f.email, b"author@example.com");
2537 assert_eq!(f.date, Some(&b"1112911993"[..]));
2538 assert_eq!(f.tz, Some(&b"-0700"[..]));
2539 }
2540
2541 #[test]
2542 fn split_ident_line_recovers_broken_email() {
2543 let f = split_ident_line(b"A U Thor <author@example.com>-<> 1112911993 -0700")
2546 .expect("broken-email ident should parse");
2547 assert_eq!(f.name, b"A U Thor");
2548 assert_eq!(f.email, b"author@example.com");
2549 assert_eq!(f.date, Some(&b"1112911993"[..]));
2550 assert_eq!(f.tz, Some(&b"-0700"[..]));
2551 }
2552
2553 #[test]
2554 fn split_ident_line_non_numeric_date_is_person_only() {
2555 let f = split_ident_line(b"A U Thor <author@example.com> totally_bogus -0700")
2556 .expect("ident without numeric date should still parse person");
2557 assert_eq!(f.email, b"author@example.com");
2558 assert_eq!(f.date, None);
2559 assert_eq!(f.tz, None);
2560 }
2561
2562 #[test]
2563 fn split_ident_line_whitespace_date_is_person_only() {
2564 let f = split_ident_line(b"A U Thor <author@example.com> ")
2566 .expect("ident with trailing whitespace should parse person");
2567 assert_eq!(f.date, None);
2568 let f = split_ident_line(b"A U Thor <author@example.com> \x0b")
2571 .expect("ident with non-git-whitespace suffix should parse person");
2572 assert_eq!(f.date, None);
2573 }
2574
2575 #[test]
2576 fn split_ident_line_requires_angle_brackets() {
2577 assert!(split_ident_line(b"no brackets here 123 +0000").is_none());
2578 }
2579
2580 #[test]
2581 fn ident_render_date_overflow_is_epoch_sentinel() {
2582 assert_eq!(
2585 ident_render_date(b"18446744073709551617", b"-0700", &DateMode::Default),
2586 "Thu Jan 1 00:00:00 1970 +0000"
2587 );
2588 assert_eq!(
2589 ident_render_date(b"18446744073709551614", b"-0700", &DateMode::Default),
2590 "Thu Jan 1 00:00:00 1970 +0000"
2591 );
2592 }
2593
2594 #[test]
2595 fn ident_render_date_valid_value_uses_original_timezone() {
2596 assert_eq!(
2597 ident_render_date(b"0", b"+0000", &DateMode::Default),
2598 "Thu Jan 1 00:00:00 1970 +0000"
2599 );
2600 }
2601}