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