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