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