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