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