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