1use std::borrow::Borrow;
2use std::error::Error;
3use std::fmt;
4use std::ops::Deref;
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7use std::sync::Mutex;
8
9pub const UPSTREAM_GIT_COMPAT_VERSION: &str = "2.54.0";
10
11static ORIGINAL_CWD: Mutex<Option<PathBuf>> = Mutex::new(None);
12
13pub fn set_original_cwd(path: Option<PathBuf>) {
14 if let Ok(mut original) = ORIGINAL_CWD.lock() {
15 *original = path;
16 }
17}
18
19pub fn original_cwd() -> Option<PathBuf> {
20 ORIGINAL_CWD.lock().ok()?.clone()
21}
22
23#[derive(Debug, Default, Clone, PartialEq, Eq)]
24pub enum DateMode {
25 #[default]
26 Default,
27 Local,
28 Raw,
29 RawLocal,
30 Unix,
31 Short,
32 ShortLocal,
33 Iso,
34 IsoLocal,
35 IsoStrict,
36 IsoStrictLocal,
37 Rfc2822,
38 Rfc2822Local,
39 Relative,
40 Human,
41 HumanLocal,
42 Strftime {
43 template: String,
44 local: bool,
45 },
46}
47
48impl DateMode {
49 pub fn parse(value: &str) -> Option<Self> {
50 if let Some(template) = value.strip_prefix("format:") {
51 return Some(Self::Strftime {
52 template: template.to_string(),
53 local: false,
54 });
55 }
56 if let Some(template) = value.strip_prefix("format-local:") {
57 return Some(Self::Strftime {
58 template: template.to_string(),
59 local: true,
60 });
61 }
62 if value == "tformat:" || value.starts_with("tformat:") {
63 return Some(Self::Strftime {
64 template: value["tformat:".len()..].to_string(),
65 local: false,
66 });
67 }
68 if value == "auto:" || value.starts_with("auto:") {
69 return Some(Self::Default);
70 }
71 Some(match value {
72 "default" => Self::Default,
73 "default-local" | "local" => Self::Local,
74 "raw" => Self::Raw,
75 "raw-local" => Self::RawLocal,
76 "unix" => Self::Unix,
77 "short" => Self::Short,
78 "short-local" => Self::ShortLocal,
79 "iso" | "iso8601" => Self::Iso,
80 "iso-local" | "iso8601-local" => Self::IsoLocal,
81 "iso-strict" | "iso8601-strict" => Self::IsoStrict,
82 "iso-strict-local" | "iso8601-strict-local" => Self::IsoStrictLocal,
83 "rfc" | "rfc2822" => Self::Rfc2822,
84 "rfc-local" | "rfc2822-local" => Self::Rfc2822Local,
85 "relative" | "relative-local" => Self::Relative,
86 "human" => Self::Human,
87 "human-local" => Self::HumanLocal,
88 _ => return None,
89 })
90 }
91
92 pub fn parse_atom_modifier(modifier: Option<&str>) -> Option<Self> {
93 modifier.map_or(Some(Self::Default), Self::parse)
94 }
95
96 pub fn render(&self, timestamp: i64, timezone: &str) -> Option<String> {
97 let tz = if self.is_local() { "+0000" } else { timezone };
98 let parts = DateParts::from_timestamp(timestamp, tz)?;
99 Some(match self {
100 Self::Default | Self::Local => {
101 let base = format!(
102 "{} {} {} {:02}:{:02}:{:02} {}",
103 parts.weekday,
104 MONTHS_ABBR[(parts.month - 1) as usize],
105 parts.day,
106 parts.hour,
107 parts.minute,
108 parts.second,
109 parts.year,
110 );
111 if self.is_local() {
112 base
113 } else {
114 format!("{base} {}", parts.timezone)
115 }
116 }
117 Self::Raw | Self::RawLocal => format!("{} {}", parts.timestamp, parts.timezone),
118 Self::Unix => parts.timestamp.to_string(),
119 Self::Short | Self::ShortLocal => {
120 format!("{:04}-{:02}-{:02}", parts.year, parts.month, parts.day)
121 }
122 Self::Iso | Self::IsoLocal => format!(
123 "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {}",
124 parts.year,
125 parts.month,
126 parts.day,
127 parts.hour,
128 parts.minute,
129 parts.second,
130 parts.timezone,
131 ),
132 Self::IsoStrict | Self::IsoStrictLocal => format!(
133 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}",
134 parts.year,
135 parts.month,
136 parts.day,
137 parts.hour,
138 parts.minute,
139 parts.second,
140 strict_timezone(parts.timezone),
141 ),
142 Self::Rfc2822 | Self::Rfc2822Local => format!(
143 "{}, {} {} {:04} {:02}:{:02}:{:02} {}",
144 parts.weekday,
145 parts.day,
146 MONTHS_ABBR[(parts.month - 1) as usize],
147 parts.year,
148 parts.hour,
149 parts.minute,
150 parts.second,
151 parts.timezone,
152 ),
153 Self::Relative => relative_date(parts.timestamp),
154 Self::Human | Self::HumanLocal => format!(
155 "{} {} {} {:02}:{:02}:{:02} {} {}",
156 parts.weekday,
157 MONTHS_ABBR[(parts.month - 1) as usize],
158 parts.day,
159 parts.hour,
160 parts.minute,
161 parts.second,
162 parts.year,
163 parts.timezone,
164 ),
165 Self::Strftime { template, .. } => strftime(template, &parts),
166 })
167 }
168
169 pub fn is_local(&self) -> bool {
170 matches!(
171 self,
172 Self::Local
173 | Self::RawLocal
174 | Self::ShortLocal
175 | Self::IsoLocal
176 | Self::IsoStrictLocal
177 | Self::Rfc2822Local
178 | Self::HumanLocal
179 | Self::Strftime { local: true, .. }
180 )
181 }
182}
183
184const MONTHS_ABBR: [&str; 12] = [
185 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
186];
187
188const MONTHS_FULL: [&str; 12] = [
189 "January",
190 "February",
191 "March",
192 "April",
193 "May",
194 "June",
195 "July",
196 "August",
197 "September",
198 "October",
199 "November",
200 "December",
201];
202
203const WEEKDAYS_FULL: [&str; 7] = [
204 "Sunday",
205 "Monday",
206 "Tuesday",
207 "Wednesday",
208 "Thursday",
209 "Friday",
210 "Saturday",
211];
212
213struct DateParts<'a> {
214 timestamp: i64,
215 timezone: &'a str,
216 weekday: &'static str,
217 year: i64,
218 month: u32,
219 day: u32,
220 hour: i64,
221 minute: i64,
222 second: i64,
223}
224
225impl<'a> DateParts<'a> {
226 fn from_timestamp(timestamp: i64, timezone: &'a str) -> Option<Self> {
227 const WEEKDAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
228 let offset_seconds = timezone_offset_seconds(timezone)?;
229 let local = timestamp + offset_seconds;
230 let days = local.div_euclid(86_400);
231 let seconds = local.rem_euclid(86_400);
232 let (year, month, day) = civil_from_days(days);
233 Some(Self {
234 timestamp,
235 timezone,
236 weekday: WEEKDAYS[(days + 4).rem_euclid(7) as usize],
237 year,
238 month,
239 day,
240 hour: seconds / 3_600,
241 minute: (seconds % 3_600) / 60,
242 second: seconds % 60,
243 })
244 }
245}
246
247fn timezone_offset_seconds(timezone: &str) -> Option<i64> {
248 if timezone.len() != 5 {
249 return None;
250 }
251 let sign = match timezone.as_bytes()[0] {
252 b'+' => 1,
253 b'-' => -1,
254 _ => return None,
255 };
256 let hours = timezone[1..3].parse::<i64>().ok()?;
257 let minutes = timezone[3..5].parse::<i64>().ok()?;
258 Some(sign * (hours * 3_600 + minutes * 60))
259}
260
261fn strict_timezone(timezone: &str) -> String {
262 let digits = timezone.strip_prefix(['+', '-']).unwrap_or(timezone);
263 if digits == "0000" {
264 "Z".to_string()
265 } else if timezone.len() == 5 {
266 format!("{}{}:{}", &timezone[..1], &timezone[1..3], &timezone[3..5])
267 } else {
268 timezone.to_string()
269 }
270}
271
272fn strftime(template: &str, parts: &DateParts<'_>) -> String {
273 let weekday_index = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
274 .iter()
275 .position(|day| *day == parts.weekday)
276 .unwrap_or(0);
277 let mut out = String::with_capacity(template.len());
278 let mut chars = template.chars().peekable();
279 while let Some(ch) = chars.next() {
280 if ch != '%' {
281 out.push(ch);
282 continue;
283 }
284 match chars.next() {
285 Some('Y') => out.push_str(&format!("{:04}", parts.year)),
286 Some('y') => out.push_str(&format!("{:02}", parts.year.rem_euclid(100))),
287 Some('m') => out.push_str(&format!("{:02}", parts.month)),
288 Some('d') => out.push_str(&format!("{:02}", parts.day)),
289 Some('e') => out.push_str(&format!("{:2}", parts.day)),
290 Some('H') => out.push_str(&format!("{:02}", parts.hour)),
291 Some('M') => out.push_str(&format!("{:02}", parts.minute)),
292 Some('S') => out.push_str(&format!("{:02}", parts.second)),
293 Some('b') | Some('h') => out.push_str(MONTHS_ABBR[(parts.month - 1) as usize]),
294 Some('B') => out.push_str(MONTHS_FULL[(parts.month - 1) as usize]),
295 Some('a') => out.push_str(parts.weekday),
296 Some('A') => out.push_str(WEEKDAYS_FULL[weekday_index]),
297 Some('%') => out.push('%'),
298 Some('n') => out.push('\n'),
299 Some('t') => out.push('\t'),
300 Some(other) => {
301 out.push('%');
302 out.push(other);
303 }
304 None => out.push('%'),
305 }
306 }
307 out
308}
309
310fn relative_date(timestamp: i64) -> String {
311 let now = std::time::SystemTime::now()
312 .duration_since(std::time::UNIX_EPOCH)
313 .map(|duration| duration.as_secs() as i64)
314 .unwrap_or(timestamp);
315 if timestamp > now {
316 return "in the future".to_string();
317 }
318 let diff = (now - timestamp) as u64;
319 if diff < 90 {
320 return format!("{diff} seconds ago");
321 }
322 let minutes = (diff + 30) / 60;
323 if minutes < 90 {
324 return format!("{minutes} minutes ago");
325 }
326 let hours = (diff + 1800) / 3600;
327 if hours < 36 {
328 return format!("{hours} hours ago");
329 }
330 let days = (diff + 43200) / 86400;
331 if days < 14 {
332 return format!("{days} days ago");
333 }
334 if days < 70 {
335 return format!("{} weeks ago", (days + 3) / 7);
336 }
337 if days < 365 {
338 return format!("{} months ago", (days + 15) / 30);
339 }
340 let years_scaled = (days * 10 + 183) / 365;
341 if days < 365 * 2 {
342 let months = ((days - 365) + 15) / 30;
343 if months > 0 {
344 return format!("1 year, {months} months ago");
345 }
346 return "1 year ago".to_string();
347 }
348 if years_scaled.is_multiple_of(10) {
349 format!("{} years ago", years_scaled / 10)
350 } else {
351 format!("{}.{} years ago", years_scaled / 10, years_scaled % 10)
352 }
353}
354
355fn civil_from_days(days: i64) -> (i64, u32, u32) {
356 let days = days + 719_468;
357 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
358 let day_of_era = days - era * 146_097;
359 let year_of_era =
360 (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
361 let year = year_of_era + era * 400;
362 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
363 let month_prime = (5 * day_of_year + 2) / 153;
364 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
365 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
366 let year = year + i64::from(month <= 2);
367 (year, month as u32, day as u32)
368}
369
370pub mod trace2 {
379 use std::fmt::Display;
380 use std::fmt::Write as _;
381 use std::io::Write;
382
383 fn escape_json(raw: &str) -> String {
384 let mut out = String::with_capacity(raw.len());
385 for ch in raw.chars() {
386 match ch {
387 '"' => out.push_str("\\\""),
388 '\\' => out.push_str("\\\\"),
389 '\n' => out.push_str("\\n"),
390 '\t' => out.push_str("\\t"),
391 ch if (ch as u32) < 0x20 => {
392 let _ = write!(out, "\\u{:04x}", ch as u32);
393 }
394 ch => out.push(ch),
395 }
396 }
397 out
398 }
399
400 pub fn touch() {
403 let Some(target) = std::env::var_os("GIT_TRACE2_EVENT") else {
404 return;
405 };
406 let target = target.to_string_lossy().into_owned();
407 if !target.starts_with('/') {
408 return;
409 }
410 let _ = std::fs::OpenOptions::new()
411 .create(true)
412 .append(true)
413 .open(target);
414 }
415
416 pub fn data(category: &str, key: &str, value: impl Display) {
420 let Some(target) = std::env::var_os("GIT_TRACE2_EVENT") else {
421 return;
422 };
423 let target = target.to_string_lossy().into_owned();
424 if !target.starts_with('/') {
427 return;
428 }
429 let line = format!(
430 "{{\"event\":\"data\",\"sid\":\"sley\",\"thread\":\"main\",\"nesting\":1,\"category\":\"{}\",\"key\":\"{}\",\"value\":\"{}\"}}\n",
431 escape_json(category),
432 escape_json(key),
433 escape_json(&value.to_string()),
434 );
435 if let Ok(mut file) = std::fs::OpenOptions::new()
436 .create(true)
437 .append(true)
438 .open(&target)
439 {
440 let _ = file.write_all(line.as_bytes());
441 }
442 }
443
444 pub fn counter(category: &str, name: &str, count: impl Display) {
447 let Some(target) = std::env::var_os("GIT_TRACE2_EVENT") else {
448 return;
449 };
450 let target = target.to_string_lossy().into_owned();
451 if !target.starts_with('/') {
452 return;
453 }
454 let line = format!(
455 "{{\"event\":\"counter\",\"sid\":\"sley\",\"thread\":\"main\",\"category\":\"{}\",\"name\":\"{}\",\"count\":{}}}\n",
456 escape_json(category),
457 escape_json(name),
458 count,
459 );
460 if let Ok(mut file) = std::fs::OpenOptions::new()
461 .create(true)
462 .append(true)
463 .open(&target)
464 {
465 let _ = file.write_all(line.as_bytes());
466 }
467 }
468
469 pub fn region(category: &str, label: &str) {
473 region_event("region_enter", category, label);
474 region_event("region_leave", category, label);
475 }
476
477 fn region_event(event: &str, category: &str, label: &str) {
478 let Some(target) = std::env::var_os("GIT_TRACE2_EVENT") else {
479 return;
480 };
481 let target = target.to_string_lossy().into_owned();
482 if !target.starts_with('/') {
483 return;
484 }
485 let line = format!(
486 "{{\"event\":\"{}\",\"sid\":\"sley\",\"thread\":\"main\",\"nesting\":1,\"category\":\"{}\",\"label\":\"{}\"}}\n",
487 escape_json(event),
488 escape_json(category),
489 escape_json(label),
490 );
491 if let Ok(mut file) = std::fs::OpenOptions::new()
492 .create(true)
493 .append(true)
494 .open(&target)
495 {
496 let _ = file.write_all(line.as_bytes());
497 }
498 }
499
500 pub fn bloom_statistics(
503 filter_not_present: usize,
504 maybe: usize,
505 definitely_not: usize,
506 false_positive: usize,
507 ) {
508 let Some(target) = std::env::var_os("GIT_TRACE2_PERF") else {
509 return;
510 };
511 let target = target.to_string_lossy().into_owned();
512 if !target.starts_with('/') {
513 return;
514 }
515 let line = format!(
516 "statistics:{{\"filter_not_present\":{filter_not_present},\"maybe\":{maybe},\"definitely_not\":{definitely_not},\"false_positive\":{false_positive}}}\n"
517 );
518 if let Ok(mut file) = std::fs::OpenOptions::new()
519 .create(true)
520 .append(true)
521 .open(&target)
522 {
523 let _ = file.write_all(line.as_bytes());
524 }
525 }
526
527 pub fn perf_read_directory_data(key: &str, value: impl Display) {
530 let Some(target) = std::env::var_os("GIT_TRACE2_PERF") else {
531 return;
532 };
533 let target = target.to_string_lossy().into_owned();
534 if !target.starts_with('/') {
535 return;
536 }
537 let line = format!(
538 "19:00:00.000000 file.c:1 | d0 | main | data | r1 | ? | ? | read_directory | ....{key}:{value}\n"
539 );
540 if let Ok(mut file) = std::fs::OpenOptions::new()
541 .create(true)
542 .append(true)
543 .open(&target)
544 {
545 let _ = file.write_all(line.as_bytes());
546 }
547 }
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
551pub enum ObjectFormat {
552 Sha1,
553 Sha256,
554}
555
556impl ObjectFormat {
557 pub const fn raw_len(self) -> usize {
558 match self {
559 Self::Sha1 => 20,
560 Self::Sha256 => 32,
561 }
562 }
563
564 pub const fn hex_len(self) -> usize {
565 self.raw_len() * 2
566 }
567
568 pub const fn name(self) -> &'static str {
569 match self {
570 Self::Sha1 => "sha1",
571 Self::Sha256 => "sha256",
572 }
573 }
574}
575
576impl FromStr for ObjectFormat {
577 type Err = GitError;
578
579 fn from_str(value: &str) -> Result<Self> {
580 match value {
581 "sha1" => Ok(Self::Sha1),
582 "sha256" => Ok(Self::Sha256),
583 other => Err(GitError::Unsupported(format!("object format {other}"))),
584 }
585 }
586}
587
588#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
589pub struct ObjectId {
590 format: ObjectFormat,
591 bytes: [u8; 32],
592}
593
594impl ObjectId {
595 pub fn from_raw(format: ObjectFormat, raw: &[u8]) -> Result<Self> {
596 if raw.len() != format.raw_len() {
597 return Err(GitError::InvalidObjectId(format!(
598 "expected {} bytes for {}, got {}",
599 format.raw_len(),
600 format.name(),
601 raw.len()
602 )));
603 }
604 let mut bytes = [0; 32];
605 bytes[..raw.len()].copy_from_slice(raw);
606 Ok(Self { format, bytes })
607 }
608
609 pub fn from_hex(format: ObjectFormat, hex: &str) -> Result<Self> {
610 if hex.len() != format.hex_len() {
611 return Err(GitError::InvalidObjectId(format!(
612 "expected {} hex digits for {}, got {}",
613 format.hex_len(),
614 format.name(),
615 hex.len()
616 )));
617 }
618 let mut raw = [0; 32];
619 for (i, pair) in hex.as_bytes().chunks_exact(2).enumerate() {
620 raw[i] = (hex_nibble(pair[0])? << 4) | hex_nibble(pair[1])?;
621 }
622 Ok(Self { format, bytes: raw })
623 }
624
625 pub const fn format(&self) -> ObjectFormat {
626 self.format
627 }
628
629 pub fn as_bytes(&self) -> &[u8] {
630 &self.bytes[..self.format.raw_len()]
631 }
632
633 pub fn to_hex(&self) -> String {
634 let mut out = String::with_capacity(self.format.hex_len());
635 self.write_hex(&mut out)
636 .expect("writing object id hex to a String cannot fail");
637 out
638 }
639
640 pub fn write_hex(&self, out: &mut impl fmt::Write) -> fmt::Result {
641 write_hex_bytes(self.as_bytes(), out)
642 }
643
644 pub fn hex_prefix_matches(&self, prefix: &[u8]) -> bool {
645 if prefix.len() > self.format.hex_len() {
646 return false;
647 }
648
649 prefix.iter().enumerate().all(|(index, expected)| {
650 let Some(expected) = hex_nibble_value(*expected) else {
651 return false;
652 };
653 let byte = self.as_bytes()[index / 2];
654 let actual = if index % 2 == 0 {
655 byte >> 4
656 } else {
657 byte & 0x0f
658 };
659 actual == expected
660 })
661 }
662
663 pub const fn abbrev_hex_len(&self, width: usize) -> usize {
664 let hex_len = self.format.hex_len();
665 if width < hex_len { width } else { hex_len }
666 }
667
668 pub fn null(format: ObjectFormat) -> Self {
670 Self {
671 format,
672 bytes: [0; 32],
673 }
674 }
675
676 pub fn is_null(&self) -> bool {
678 self.as_bytes().iter().all(|byte| *byte == 0)
679 }
680
681 pub fn empty_tree(format: ObjectFormat) -> Self {
683 Self::digest_object(format, "tree", b"")
684 }
685
686 pub fn empty_blob(format: ObjectFormat) -> Self {
688 Self::digest_object(format, "blob", b"")
689 }
690
691 fn digest_object(format: ObjectFormat, object_type: &str, body: &[u8]) -> Self {
695 let mut framed = Vec::with_capacity(object_type.len() + body.len() + 32);
696 framed.extend_from_slice(object_type.as_bytes());
697 framed.push(b' ');
698 framed.extend_from_slice(body.len().to_string().as_bytes());
699 framed.push(0);
700 framed.extend_from_slice(body);
701 let mut bytes = [0u8; 32];
702 match format {
703 ObjectFormat::Sha1 => bytes[..20].copy_from_slice(&sha1(&framed)),
704 ObjectFormat::Sha256 => bytes[..32].copy_from_slice(&sha256(&framed)),
705 }
706 Self { format, bytes }
707 }
708}
709
710impl fmt::Debug for ObjectId {
711 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
712 f.debug_tuple("ObjectId").field(&self.to_hex()).finish()
713 }
714}
715
716impl fmt::Display for ObjectId {
717 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
718 self.write_hex(f)
719 }
720}
721
722impl FromStr for ObjectId {
723 type Err = GitError;
724
725 fn from_str(text: &str) -> Result<Self> {
728 let format = match text.len() {
729 40 => ObjectFormat::Sha1,
730 64 => ObjectFormat::Sha256,
731 other => {
732 return Err(GitError::InvalidObjectId(format!(
733 "expected 40 or 64 hex digits, got {other}"
734 )));
735 }
736 };
737 Self::from_hex(format, text)
738 }
739}
740
741#[derive(Debug, Clone, PartialEq, Eq)]
742pub struct ByteString(Vec<u8>);
743
744impl ByteString {
745 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
746 Self(bytes.into())
747 }
748
749 pub fn as_bytes(&self) -> &[u8] {
750 &self.0
751 }
752}
753
754impl From<&str> for ByteString {
755 fn from(value: &str) -> Self {
756 Self(value.as_bytes().to_vec())
757 }
758}
759
760#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
762pub struct FullName(String);
763
764impl FullName {
765 pub fn new(name: impl AsRef<str>) -> Result<Self> {
768 let name = name.as_ref();
769 validate_full_name(name)?;
770 Ok(Self(name.to_string()))
771 }
772
773 pub fn as_str(&self) -> &str {
774 &self.0
775 }
776}
777
778impl fmt::Debug for FullName {
779 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
780 f.debug_tuple("FullName").field(&self.0).finish()
781 }
782}
783
784impl fmt::Display for FullName {
785 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
786 f.write_str(&self.0)
787 }
788}
789
790impl From<FullName> for String {
791 fn from(value: FullName) -> Self {
792 value.0
793 }
794}
795
796impl Borrow<str> for FullName {
797 fn borrow(&self) -> &str {
798 &self.0
799 }
800}
801
802impl AsRef<str> for FullName {
803 fn as_ref(&self) -> &str {
804 &self.0
805 }
806}
807
808impl TryFrom<&str> for FullName {
809 type Error = GitError;
810
811 fn try_from(value: &str) -> Result<Self> {
812 Self::new(value)
813 }
814}
815
816impl TryFrom<String> for FullName {
817 type Error = GitError;
818
819 fn try_from(value: String) -> Result<Self> {
820 validate_full_name(&value)?;
821 Ok(Self(value))
822 }
823}
824
825impl PartialEq<&str> for FullName {
826 fn eq(&self, other: &&str) -> bool {
827 self.0 == *other
828 }
829}
830
831impl PartialEq<FullName> for &str {
832 fn eq(&self, other: &FullName) -> bool {
833 *self == other.0
834 }
835}
836
837fn validate_full_name(name: &str) -> Result<()> {
838 if name.is_empty() {
839 return Err(GitError::InvalidFormat("ref name must not be empty".into()));
840 }
841 if name.chars().next().is_some_and(|ch| ch.is_whitespace())
842 || name.chars().last().is_some_and(|ch| ch.is_whitespace())
843 {
844 return Err(GitError::InvalidFormat(
845 "ref name must not have leading or trailing whitespace".into(),
846 ));
847 }
848 if name.contains("//") {
849 return Err(GitError::InvalidFormat(
850 "ref name must not contain consecutive slashes".into(),
851 ));
852 }
853 if name.bytes().any(|byte| byte.is_ascii_control()) {
854 return Err(GitError::InvalidFormat(
855 "ref name must not contain control characters".into(),
856 ));
857 }
858 Ok(())
859}
860
861#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
863pub struct BString(Vec<u8>);
864
865impl BString {
866 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
867 Self(bytes.into())
868 }
869 pub fn from_bytes(bytes: &[u8]) -> Self {
870 Self(bytes.to_vec())
871 }
872 pub fn as_bytes(&self) -> &[u8] {
873 &self.0
874 }
875 pub fn len(&self) -> usize {
876 self.0.len()
877 }
878 pub fn is_empty(&self) -> bool {
879 self.0.is_empty()
880 }
881 pub fn into_bytes(self) -> Vec<u8> {
882 self.0
883 }
884}
885
886impl From<&str> for BString {
887 fn from(v: &str) -> Self {
888 Self::from_bytes(v.as_bytes())
889 }
890}
891impl From<&[u8]> for BString {
892 fn from(v: &[u8]) -> Self {
893 Self::from_bytes(v)
894 }
895}
896impl<const N: usize> From<&[u8; N]> for BString {
897 fn from(v: &[u8; N]) -> Self {
898 Self::from_bytes(v.as_slice())
899 }
900}
901impl From<Vec<u8>> for BString {
902 fn from(v: Vec<u8>) -> Self {
903 Self(v)
904 }
905}
906impl PartialEq<&[u8]> for BString {
907 fn eq(&self, o: &&[u8]) -> bool {
908 self.0.as_slice() == *o
909 }
910}
911impl<const N: usize> PartialEq<&[u8; N]> for BString {
912 fn eq(&self, o: &&[u8; N]) -> bool {
913 self.as_bytes() == o.as_slice()
914 }
915}
916impl PartialEq<BString> for &[u8] {
917 fn eq(&self, o: &BString) -> bool {
918 *self == o.as_bytes()
919 }
920}
921impl<const N: usize> PartialEq<BString> for &[u8; N] {
922 fn eq(&self, o: &BString) -> bool {
923 self.as_slice() == o.as_bytes()
924 }
925}
926impl PartialEq<Vec<u8>> for BString {
927 fn eq(&self, o: &Vec<u8>) -> bool {
928 self.0 == *o
929 }
930}
931impl PartialEq<BString> for Vec<u8> {
932 fn eq(&self, o: &BString) -> bool {
933 *self == o.0
934 }
935}
936
937impl fmt::Display for BString {
938 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
939 write!(f, "{}", String::from_utf8_lossy(&self.0))
940 }
941}
942
943impl Borrow<[u8]> for BString {
944 fn borrow(&self) -> &[u8] {
945 self.as_bytes()
946 }
947}
948
949impl Deref for BString {
950 type Target = [u8];
951
952 fn deref(&self) -> &[u8] {
953 self.as_bytes()
954 }
955}
956
957impl AsRef<[u8]> for BString {
958 fn as_ref(&self) -> &[u8] {
959 self.as_bytes()
960 }
961}
962
963#[derive(Debug, Clone, PartialEq, Eq, Hash)]
964pub struct RepoPath(PathBuf);
965
966impl RepoPath {
967 pub fn new(path: impl Into<PathBuf>) -> Result<Self> {
968 let path = path.into();
969 if path.is_absolute() {
970 return Err(GitError::InvalidPath(
971 "repository paths must be relative".into(),
972 ));
973 }
974 if path.components().any(|component| {
975 matches!(
976 component,
977 std::path::Component::ParentDir | std::path::Component::Prefix(_)
978 )
979 }) {
980 return Err(GitError::InvalidPath(
981 "repository paths must not escape".into(),
982 ));
983 }
984 Ok(Self(path))
985 }
986
987 pub fn as_path(&self) -> &Path {
988 &self.0
989 }
990}
991
992#[derive(Debug, Clone, PartialEq, Eq)]
1007pub struct Signature {
1008 pub name: ByteString,
1011 pub email: ByteString,
1014 pub time: GitTime,
1016 pub raw: Vec<u8>,
1020}
1021
1022impl Signature {
1023 pub fn from_ident_line(line: &[u8]) -> Option<Self> {
1037 let mail_end = line.iter().rposition(|byte| *byte == b'>')?;
1041 let mail_begin = line[..mail_end].iter().rposition(|byte| *byte == b'<')? + 1;
1042 let email = &line[mail_begin..mail_end];
1043
1044 let mut name_end = mail_begin.saturating_sub(1);
1047 if name_end > 0 && line[name_end - 1] == b' ' {
1048 name_end -= 1;
1049 }
1050 let name = &line[..name_end];
1051
1052 let rest = line.get(mail_end + 1..)?;
1055 let rest = rest.strip_prefix(b" ")?;
1056 let time = GitTime::from_time_fields(rest)?;
1057
1058 Some(Self {
1059 name: ByteString::new(name.to_vec()),
1060 email: ByteString::new(email.to_vec()),
1061 time,
1062 raw: line.to_vec(),
1063 })
1064 }
1065
1066 pub fn to_ident_bytes(&self) -> Vec<u8> {
1073 self.raw.clone()
1074 }
1075
1076 pub fn to_canonical_ident_bytes(&self) -> Vec<u8> {
1085 let mut out = Vec::with_capacity(self.raw.len());
1086 out.extend_from_slice(self.name.as_bytes());
1087 out.extend_from_slice(b" <");
1088 out.extend_from_slice(self.email.as_bytes());
1089 out.extend_from_slice(b"> ");
1090 out.extend_from_slice(self.time.to_ident_suffix().as_bytes());
1091 out
1092 }
1093}
1094
1095impl fmt::Display for Signature {
1096 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1100 write!(f, "{}", String::from_utf8_lossy(&self.raw))
1101 }
1102}
1103
1104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1117pub struct GitTime {
1118 pub seconds: i64,
1120 pub timezone_offset_minutes: i16,
1124 pub negative_utc: bool,
1128}
1129
1130impl GitTime {
1131 pub const fn new(seconds: i64, timezone_offset_minutes: i16) -> Self {
1135 Self {
1136 seconds,
1137 timezone_offset_minutes,
1138 negative_utc: false,
1139 }
1140 }
1141
1142 pub const fn with_negative_utc(seconds: i64) -> Self {
1145 Self {
1146 seconds,
1147 timezone_offset_minutes: 0,
1148 negative_utc: true,
1149 }
1150 }
1151
1152 fn from_time_fields(bytes: &[u8]) -> Option<Self> {
1156 let text = std::str::from_utf8(bytes).ok()?;
1157 let (seconds_text, tz_text) = text.split_once(' ')?;
1158 let seconds = seconds_text.parse::<i64>().ok()?;
1159 let (timezone_offset_minutes, negative_utc) = parse_timezone_token(tz_text)?;
1160 Some(Self {
1161 seconds,
1162 timezone_offset_minutes,
1163 negative_utc,
1164 })
1165 }
1166
1167 fn to_ident_suffix(self) -> String {
1170 format!("{} {}", self.seconds, self.offset_token())
1171 }
1172
1173 pub fn offset_token(self) -> String {
1177 let sign = if self.negative_utc || self.timezone_offset_minutes < 0 {
1178 '-'
1179 } else {
1180 '+'
1181 };
1182 let magnitude = self.timezone_offset_minutes.unsigned_abs();
1183 format!("{sign}{:02}{:02}", magnitude / 60, magnitude % 60)
1184 }
1185}
1186
1187fn parse_timezone_token(token: &str) -> Option<(i16, bool)> {
1193 let bytes = token.as_bytes();
1194 if bytes.len() != 5 {
1195 return None;
1196 }
1197 let negative = match bytes[0] {
1198 b'+' => false,
1199 b'-' => true,
1200 _ => return None,
1201 };
1202 if !bytes[1..].iter().all(u8::is_ascii_digit) {
1203 return None;
1204 }
1205 let hours = i16::from(bytes[1] - b'0') * 10 + i16::from(bytes[2] - b'0');
1206 let minutes = i16::from(bytes[3] - b'0') * 10 + i16::from(bytes[4] - b'0');
1207 let total = hours * 60 + minutes;
1208 let negative_utc = negative && total == 0;
1209 let signed = if negative { -total } else { total };
1210 Some((signed, negative_utc))
1211}
1212
1213#[derive(Debug, Clone, PartialEq, Eq)]
1214pub struct Capability {
1215 pub name: String,
1216 pub value: Option<String>,
1217}
1218
1219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1220pub enum MissingObjectKind {
1221 Object,
1222 Blob,
1223 Tree,
1224 Commit,
1225 Tag,
1226}
1227
1228impl MissingObjectKind {
1229 pub const fn as_str(self) -> &'static str {
1230 match self {
1231 Self::Object => "object",
1232 Self::Blob => "blob",
1233 Self::Tree => "tree",
1234 Self::Commit => "commit",
1235 Self::Tag => "tag",
1236 }
1237 }
1238}
1239
1240impl fmt::Display for MissingObjectKind {
1241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1242 f.write_str(self.as_str())
1243 }
1244}
1245
1246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1247pub enum MissingObjectContext {
1248 Read,
1249 Traversal,
1250 PackInstall,
1251 RevisionWalk,
1252 WorktreeMaterialize,
1253 RemoteBoundary,
1254}
1255
1256impl MissingObjectContext {
1257 pub const fn as_str(self) -> &'static str {
1258 match self {
1259 Self::Read => "read",
1260 Self::Traversal => "traversal",
1261 Self::PackInstall => "pack-install",
1262 Self::RevisionWalk => "revision-walk",
1263 Self::WorktreeMaterialize => "worktree-materialize",
1264 Self::RemoteBoundary => "remote-boundary",
1265 }
1266 }
1267}
1268
1269impl fmt::Display for MissingObjectContext {
1270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1271 f.write_str(self.as_str())
1272 }
1273}
1274
1275#[derive(Debug, Clone, PartialEq, Eq)]
1276pub enum NotFoundKind {
1277 Message(String),
1278 Remote {
1279 name: String,
1280 },
1281 Object {
1282 oid: ObjectId,
1283 kind: MissingObjectKind,
1284 context: Option<MissingObjectContext>,
1285 },
1286 Reference {
1287 name: String,
1288 },
1289 Repository {
1290 path: String,
1291 },
1292}
1293
1294impl fmt::Display for NotFoundKind {
1295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1296 match self {
1297 Self::Message(msg) => write!(f, "{msg}"),
1298 Self::Remote { name } => write!(f, "remote {name}"),
1299 Self::Object {
1300 oid,
1301 kind: MissingObjectKind::Object,
1302 ..
1303 } => write!(f, "object {oid}"),
1304 Self::Object { oid, kind, .. } => write!(f, "{kind} object {oid}"),
1305 Self::Reference { name } => write!(f, "{name}"),
1306 Self::Repository { path } => write!(f, "{path}"),
1307 }
1308 }
1309}
1310
1311impl NotFoundKind {
1312 pub fn object_id(&self) -> Option<ObjectId> {
1313 match self {
1314 Self::Object { oid, .. } => Some(*oid),
1315 _ => None,
1316 }
1317 }
1318
1319 pub fn missing_object_kind(&self) -> Option<MissingObjectKind> {
1320 match self {
1321 Self::Object { kind, .. } => Some(*kind),
1322 _ => None,
1323 }
1324 }
1325
1326 pub fn missing_object_context(&self) -> Option<MissingObjectContext> {
1327 match self {
1328 Self::Object { context, .. } => *context,
1329 _ => None,
1330 }
1331 }
1332}
1333
1334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1336pub enum CliExit {
1337 Ok,
1339 UserError,
1341 Usage,
1343 Custom(i32),
1345}
1346
1347impl CliExit {
1348 pub const fn code(self) -> i32 {
1349 match self {
1350 Self::Ok => 0,
1351 Self::UserError => 128,
1352 Self::Usage => 129,
1353 Self::Custom(code) => code,
1354 }
1355 }
1356}
1357
1358#[derive(Debug, Clone, PartialEq, Eq)]
1359pub enum GitError {
1360 Io(String),
1361 InvalidObjectId(String),
1362 InvalidObject(String),
1363 InvalidFormat(String),
1364 InvalidPath(String),
1365 Unsupported(String),
1366 NotFound(NotFoundKind),
1367 Transaction(String),
1368 Command(String),
1369 Cli(CliExit, String),
1371 Exit(i32),
1373}
1374
1375pub type Result<T> = std::result::Result<T, GitError>;
1376
1377impl fmt::Display for GitError {
1378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1379 match self {
1380 Self::Io(msg) => write!(f, "io error: {msg}"),
1381 Self::InvalidObjectId(msg) => write!(f, "invalid object id: {msg}"),
1382 Self::InvalidObject(msg) => write!(f, "invalid object: {msg}"),
1383 Self::InvalidFormat(msg) => write!(f, "invalid format: {msg}"),
1384 Self::InvalidPath(msg) => write!(f, "invalid path: {msg}"),
1385 Self::Unsupported(msg) => write!(f, "unsupported: {msg}"),
1386 Self::NotFound(kind) => write!(f, "not found: {kind}"),
1387 Self::Transaction(msg) => write!(f, "transaction failed: {msg}"),
1388 Self::Command(msg) => write!(f, "command failed: {msg}"),
1389 Self::Cli(_, msg) => f.write_str(msg),
1390 Self::Exit(code) => write!(f, "exit {code}"),
1391 }
1392 }
1393}
1394
1395impl Error for GitError {}
1396
1397impl GitError {
1398 pub fn usage(msg: impl Into<String>) -> Self {
1399 Self::Cli(CliExit::Usage, msg.into())
1400 }
1401
1402 pub fn user_error(msg: impl Into<String>) -> Self {
1403 Self::Cli(CliExit::UserError, msg.into())
1404 }
1405
1406 pub fn cli_exit(kind: CliExit, msg: impl Into<String>) -> Self {
1407 Self::Cli(kind, msg.into())
1408 }
1409
1410 pub fn cli_exit_code(&self) -> i32 {
1411 cli_exit_code(self)
1412 }
1413
1414 pub fn not_found(msg: impl Into<String>) -> Self {
1415 Self::NotFound(NotFoundKind::Message(msg.into()))
1416 }
1417
1418 pub fn remote_not_found(name: impl Into<String>) -> Self {
1419 Self::NotFound(NotFoundKind::Remote { name: name.into() })
1420 }
1421
1422 pub fn object_not_found(oid: ObjectId) -> Self {
1423 Self::object_kind_not_found(oid, MissingObjectKind::Object)
1424 }
1425
1426 pub fn object_kind_not_found(oid: ObjectId, kind: MissingObjectKind) -> Self {
1427 Self::NotFound(NotFoundKind::Object {
1428 oid,
1429 kind,
1430 context: None,
1431 })
1432 }
1433
1434 pub fn object_not_found_in(oid: ObjectId, context: MissingObjectContext) -> Self {
1435 Self::object_kind_not_found_in(oid, MissingObjectKind::Object, context)
1436 }
1437
1438 pub fn object_kind_not_found_in(
1439 oid: ObjectId,
1440 kind: MissingObjectKind,
1441 context: MissingObjectContext,
1442 ) -> Self {
1443 Self::NotFound(NotFoundKind::Object {
1444 oid,
1445 kind,
1446 context: Some(context),
1447 })
1448 }
1449
1450 pub fn reference_not_found(name: impl Into<String>) -> Self {
1451 Self::NotFound(NotFoundKind::Reference { name: name.into() })
1452 }
1453
1454 pub fn repository_not_found(path: impl Into<String>) -> Self {
1455 Self::NotFound(NotFoundKind::Repository { path: path.into() })
1456 }
1457
1458 pub fn not_found_kind(&self) -> Option<&NotFoundKind> {
1459 match self {
1460 Self::NotFound(kind) => Some(kind),
1461 _ => None,
1462 }
1463 }
1464}
1465
1466impl From<std::io::Error> for GitError {
1467 fn from(value: std::io::Error) -> Self {
1468 Self::Io(value.to_string())
1469 }
1470}
1471
1472pub fn cli_exit_code(err: &GitError) -> i32 {
1474 match err {
1475 GitError::Exit(code) => *code,
1476 GitError::Cli(kind, _) => kind.code(),
1477 GitError::Command(_) => 1,
1480 _ => 1,
1481 }
1482}
1483
1484pub fn object_id_for_bytes(
1485 format: ObjectFormat,
1486 object_type: &str,
1487 body: &[u8],
1488) -> Result<ObjectId> {
1489 match format {
1490 ObjectFormat::Sha1 => ObjectId::from_raw(format, &sha1_object_digest(object_type, body)),
1494 ObjectFormat::Sha256 => {
1495 let mut framed = Vec::with_capacity(object_type.len() + body.len() + 32);
1496 framed.extend_from_slice(object_type.as_bytes());
1497 framed.push(b' ');
1498 framed.extend_from_slice(body.len().to_string().as_bytes());
1499 framed.push(0);
1500 framed.extend_from_slice(body);
1501 ObjectId::from_raw(format, &sha256(&framed))
1502 }
1503 }
1504}
1505
1506pub fn digest_bytes(format: ObjectFormat, bytes: &[u8]) -> Result<ObjectId> {
1507 match format {
1508 ObjectFormat::Sha1 => ObjectId::from_raw(format, &sha1(bytes)),
1509 ObjectFormat::Sha256 => ObjectId::from_raw(format, &sha256(bytes)),
1510 }
1511}
1512
1513pub fn to_hex(bytes: &[u8]) -> String {
1514 let mut out = String::with_capacity(bytes.len() * 2);
1515 write_hex_bytes(bytes, &mut out).expect("writing hex to a String cannot fail");
1516 out
1517}
1518
1519fn write_hex_bytes(bytes: &[u8], out: &mut impl fmt::Write) -> fmt::Result {
1520 const HEX: &[u8; 16] = b"0123456789abcdef";
1521 for byte in bytes {
1522 out.write_char(HEX[(byte >> 4) as usize] as char)?;
1523 out.write_char(HEX[(byte & 0x0f) as usize] as char)?;
1524 }
1525 Ok(())
1526}
1527
1528fn hex_nibble_value(byte: u8) -> Option<u8> {
1529 match byte {
1530 b'0'..=b'9' => Some(byte - b'0'),
1531 b'a'..=b'f' => Some(byte - b'a' + 10),
1532 b'A'..=b'F' => Some(byte - b'A' + 10),
1533 _ => None,
1534 }
1535}
1536
1537fn hex_nibble(byte: u8) -> Result<u8> {
1538 hex_nibble_value(byte)
1539 .ok_or_else(|| GitError::InvalidObjectId(format!("non-hex byte {:?}", byte as char)))
1540}
1541
1542#[cfg(not(feature = "fast-sha1"))]
1554fn sha1(input: &[u8]) -> [u8; 20] {
1555 let mut hasher = Sha1Hasher::new();
1556 hasher.update(input);
1557 hasher.finalize()
1558}
1559
1560#[cfg(feature = "fast-sha1")]
1562fn sha1(input: &[u8]) -> [u8; 20] {
1563 use sha1::{Digest, Sha1};
1564 let mut hasher = Sha1::new();
1565 hasher.update(input);
1566 hasher.finalize().into()
1567}
1568
1569#[cfg(not(feature = "fast-sha1"))]
1572fn sha1_object_digest(object_type: &str, body: &[u8]) -> [u8; 20] {
1573 let mut hasher = Sha1Hasher::new();
1574 hasher.update(object_type.as_bytes());
1575 hasher.update(b" ");
1576 hasher.update(body.len().to_string().as_bytes());
1577 hasher.update(&[0u8]);
1578 hasher.update(body);
1579 hasher.finalize()
1580}
1581
1582#[cfg(feature = "fast-sha1")]
1583fn sha1_object_digest(object_type: &str, body: &[u8]) -> [u8; 20] {
1584 use sha1::{Digest, Sha1};
1585 let mut hasher = Sha1::new();
1586 hasher.update(object_type.as_bytes());
1587 hasher.update(b" ");
1588 hasher.update(body.len().to_string().as_bytes());
1589 hasher.update([0u8]);
1590 hasher.update(body);
1591 hasher.finalize().into()
1592}
1593
1594#[cfg(not(feature = "fast-sha1"))]
1598struct Sha1Hasher {
1599 state: [u32; 5],
1600 block: [u8; 64],
1601 block_len: usize,
1602 total_len: u64,
1603}
1604
1605#[cfg(not(feature = "fast-sha1"))]
1606impl Sha1Hasher {
1607 fn new() -> Self {
1608 Self {
1609 state: [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0],
1610 block: [0u8; 64],
1611 block_len: 0,
1612 total_len: 0,
1613 }
1614 }
1615
1616 fn update(&mut self, mut data: &[u8]) {
1617 self.total_len = self.total_len.wrapping_add(data.len() as u64);
1618 if self.block_len > 0 {
1619 let take = (64 - self.block_len).min(data.len());
1620 self.block[self.block_len..self.block_len + take].copy_from_slice(&data[..take]);
1621 self.block_len += take;
1622 data = &data[take..];
1623 if self.block_len == 64 {
1624 let block = self.block;
1625 sha1_compress(&mut self.state, &block);
1626 self.block_len = 0;
1627 }
1628 }
1629 while data.len() >= 64 {
1630 sha1_compress(&mut self.state, &data[..64]);
1631 data = &data[64..];
1632 }
1633 if !data.is_empty() {
1634 self.block[..data.len()].copy_from_slice(data);
1635 self.block_len = data.len();
1636 }
1637 }
1638
1639 fn finalize(mut self) -> [u8; 20] {
1640 let bit_len = self.total_len.wrapping_mul(8);
1641 let mut tail = [0u8; 128];
1644 tail[..self.block_len].copy_from_slice(&self.block[..self.block_len]);
1645 tail[self.block_len] = 0x80;
1646 let total = if self.block_len < 56 { 64 } else { 128 };
1647 tail[total - 8..total].copy_from_slice(&bit_len.to_be_bytes());
1648 sha1_compress(&mut self.state, &tail[..64]);
1649 if total == 128 {
1650 sha1_compress(&mut self.state, &tail[64..128]);
1651 }
1652 let mut out = [0u8; 20];
1653 out[0..4].copy_from_slice(&self.state[0].to_be_bytes());
1654 out[4..8].copy_from_slice(&self.state[1].to_be_bytes());
1655 out[8..12].copy_from_slice(&self.state[2].to_be_bytes());
1656 out[12..16].copy_from_slice(&self.state[3].to_be_bytes());
1657 out[16..20].copy_from_slice(&self.state[4].to_be_bytes());
1658 out
1659 }
1660}
1661
1662#[cfg(not(feature = "fast-sha1"))]
1664fn sha1_compress(state: &mut [u32; 5], block: &[u8]) {
1665 let mut w = [0u32; 80];
1666 for (i, word) in w.iter_mut().take(16).enumerate() {
1667 let offset = i * 4;
1668 *word = u32::from_be_bytes([
1669 block[offset],
1670 block[offset + 1],
1671 block[offset + 2],
1672 block[offset + 3],
1673 ]);
1674 }
1675 for i in 16..80 {
1676 w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
1677 }
1678
1679 let mut a = state[0];
1680 let mut b = state[1];
1681 let mut c = state[2];
1682 let mut d = state[3];
1683 let mut e = state[4];
1684
1685 for (i, word) in w.iter().enumerate() {
1686 let (f, k) = match i {
1687 0..=19 => ((b & c) | ((!b) & d), 0x5a827999u32),
1688 20..=39 => (b ^ c ^ d, 0x6ed9eba1),
1689 40..=59 => ((b & c) | (b & d) | (c & d), 0x8f1bbcdc),
1690 _ => (b ^ c ^ d, 0xca62c1d6),
1691 };
1692 let temp = a
1693 .rotate_left(5)
1694 .wrapping_add(f)
1695 .wrapping_add(e)
1696 .wrapping_add(k)
1697 .wrapping_add(*word);
1698 e = d;
1699 d = c;
1700 c = b.rotate_left(30);
1701 b = a;
1702 a = temp;
1703 }
1704
1705 state[0] = state[0].wrapping_add(a);
1706 state[1] = state[1].wrapping_add(b);
1707 state[2] = state[2].wrapping_add(c);
1708 state[3] = state[3].wrapping_add(d);
1709 state[4] = state[4].wrapping_add(e);
1710}
1711
1712fn sha256(input: &[u8]) -> [u8; 32] {
1713 const K: [u32; 64] = [
1714 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
1715 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
1716 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
1717 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
1718 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
1719 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
1720 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
1721 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
1722 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
1723 0xc67178f2,
1724 ];
1725
1726 let mut h = [
1727 0x6a09e667u32,
1728 0xbb67ae85,
1729 0x3c6ef372,
1730 0xa54ff53a,
1731 0x510e527f,
1732 0x9b05688c,
1733 0x1f83d9ab,
1734 0x5be0cd19,
1735 ];
1736
1737 let bit_len = (input.len() as u64) * 8;
1738 let mut msg = input.to_vec();
1739 msg.push(0x80);
1740 while msg.len() % 64 != 56 {
1741 msg.push(0);
1742 }
1743 msg.extend_from_slice(&bit_len.to_be_bytes());
1744
1745 for chunk in msg.chunks_exact(64) {
1746 let mut w = [0u32; 64];
1747 for (i, word) in w.iter_mut().take(16).enumerate() {
1748 let offset = i * 4;
1749 *word = u32::from_be_bytes([
1750 chunk[offset],
1751 chunk[offset + 1],
1752 chunk[offset + 2],
1753 chunk[offset + 3],
1754 ]);
1755 }
1756 for i in 16..64 {
1757 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
1758 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
1759 w[i] = w[i - 16]
1760 .wrapping_add(s0)
1761 .wrapping_add(w[i - 7])
1762 .wrapping_add(s1);
1763 }
1764
1765 let mut a = h[0];
1766 let mut b = h[1];
1767 let mut c = h[2];
1768 let mut d = h[3];
1769 let mut e = h[4];
1770 let mut f = h[5];
1771 let mut g = h[6];
1772 let mut hh = h[7];
1773
1774 for i in 0..64 {
1775 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
1776 let ch = (e & f) ^ ((!e) & g);
1777 let temp1 = hh
1778 .wrapping_add(s1)
1779 .wrapping_add(ch)
1780 .wrapping_add(K[i])
1781 .wrapping_add(w[i]);
1782 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
1783 let maj = (a & b) ^ (a & c) ^ (b & c);
1784 let temp2 = s0.wrapping_add(maj);
1785
1786 hh = g;
1787 g = f;
1788 f = e;
1789 e = d.wrapping_add(temp1);
1790 d = c;
1791 c = b;
1792 b = a;
1793 a = temp1.wrapping_add(temp2);
1794 }
1795
1796 h[0] = h[0].wrapping_add(a);
1797 h[1] = h[1].wrapping_add(b);
1798 h[2] = h[2].wrapping_add(c);
1799 h[3] = h[3].wrapping_add(d);
1800 h[4] = h[4].wrapping_add(e);
1801 h[5] = h[5].wrapping_add(f);
1802 h[6] = h[6].wrapping_add(g);
1803 h[7] = h[7].wrapping_add(hh);
1804 }
1805
1806 let mut out = [0; 32];
1807 for (idx, word) in h.iter().enumerate() {
1808 out[idx * 4..idx * 4 + 4].copy_from_slice(&word.to_be_bytes());
1809 }
1810 out
1811}
1812
1813#[cfg(test)]
1814mod tests {
1815 use super::*;
1816
1817 #[test]
1818 fn sha1_blob_matches_git_known_value() {
1819 let oid = object_id_for_bytes(ObjectFormat::Sha1, "blob", b"hello\n")
1820 .expect("known blob should hash as sha1");
1821 assert_eq!(oid.to_hex(), "ce013625030ba8dba906f756967f9e9ca394464a");
1822 }
1823
1824 #[test]
1825 fn sha256_blob_matches_git_known_value() {
1826 let oid = object_id_for_bytes(ObjectFormat::Sha256, "blob", b"hello\n")
1827 .expect("known blob should hash as sha256");
1828 assert_eq!(
1829 oid.to_hex(),
1830 "2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4"
1831 );
1832 }
1833
1834 #[test]
1835 fn object_id_round_trips_hex() {
1836 let oid = ObjectId::from_hex(
1837 ObjectFormat::Sha1,
1838 "ce013625030ba8dba906f756967f9e9ca394464a",
1839 )
1840 .expect("valid sha1 hex");
1841 assert_eq!(oid.to_hex(), "ce013625030ba8dba906f756967f9e9ca394464a");
1842 }
1843
1844 #[test]
1845 fn object_id_writes_hex_without_allocating_in_the_writer() {
1846 let oid = ObjectId::from_hex(
1847 ObjectFormat::Sha1,
1848 "CE013625030BA8DBA906F756967F9E9CA394464A",
1849 )
1850 .expect("valid uppercase sha1 hex");
1851
1852 let mut out = String::new();
1853 oid.write_hex(&mut out)
1854 .expect("writing object id hex to a String should not fail");
1855
1856 assert_eq!(out, "ce013625030ba8dba906f756967f9e9ca394464a");
1857 assert_eq!(oid.to_hex(), out);
1858 assert_eq!(format!("{oid}"), out);
1859 }
1860
1861 #[test]
1862 fn object_id_matches_hex_prefixes_by_nibble() {
1863 let oid = ObjectId::from_hex(
1864 ObjectFormat::Sha1,
1865 "ce013625030ba8dba906f756967f9e9ca394464a",
1866 )
1867 .expect("valid sha1 hex");
1868
1869 assert!(oid.hex_prefix_matches(b""));
1870 assert!(oid.hex_prefix_matches(b"c"));
1871 assert!(oid.hex_prefix_matches(b"ce013"));
1872 assert!(oid.hex_prefix_matches(b"CE013625"));
1873 assert!(oid.hex_prefix_matches(b"ce013625030ba8dba906f756967f9e9ca394464a"));
1874
1875 assert!(!oid.hex_prefix_matches(b"d"));
1876 assert!(!oid.hex_prefix_matches(b"ce014"));
1877 assert!(!oid.hex_prefix_matches(b"ce01x"));
1878
1879 let mut too_long = oid.to_hex();
1880 too_long.push('0');
1881 assert!(!oid.hex_prefix_matches(too_long.as_bytes()));
1882 }
1883
1884 #[test]
1885 fn object_id_abbrev_hex_len_clamps_to_format_width() {
1886 let sha1 = ObjectId::null(ObjectFormat::Sha1);
1887 let sha256 = ObjectId::null(ObjectFormat::Sha256);
1888
1889 assert_eq!(sha1.abbrev_hex_len(0), 0);
1890 assert_eq!(sha1.abbrev_hex_len(12), 12);
1891 assert_eq!(sha1.abbrev_hex_len(80), ObjectFormat::Sha1.hex_len());
1892 assert_eq!(sha256.abbrev_hex_len(80), ObjectFormat::Sha256.hex_len());
1893 }
1894
1895 #[test]
1896 fn signature_parses_a_normal_ident_and_round_trips() {
1897 let line = b"A U Thor <author@example.com> 1700000000 +0000";
1898 let sig = Signature::from_ident_line(line).expect("well-formed ident parses");
1899 assert_eq!(sig.name.as_bytes(), b"A U Thor");
1900 assert_eq!(sig.email.as_bytes(), b"author@example.com");
1901 assert_eq!(sig.time.seconds, 1_700_000_000);
1902 assert_eq!(sig.time.timezone_offset_minutes, 0);
1903 assert!(!sig.time.negative_utc);
1904 assert_eq!(sig.to_ident_bytes(), line);
1906 assert_eq!(sig.to_canonical_ident_bytes(), line);
1907 }
1908
1909 #[test]
1910 fn signature_parses_positive_half_hour_offset() {
1911 let line = b"Half Hour <hh@example.com> 1500000000 +0530";
1912 let sig = Signature::from_ident_line(line).expect("offset ident parses");
1913 assert_eq!(sig.time.timezone_offset_minutes, 330);
1914 assert!(!sig.time.negative_utc);
1915 assert_eq!(sig.time.offset_token(), "+0530");
1916 assert_eq!(sig.to_ident_bytes(), line);
1917 assert_eq!(sig.to_canonical_ident_bytes(), line);
1918 }
1919
1920 #[test]
1921 fn signature_parses_negative_offset() {
1922 let line = b"Western <w@example.com> 1500000000 -0500";
1923 let sig = Signature::from_ident_line(line).expect("negative offset parses");
1924 assert_eq!(sig.time.timezone_offset_minutes, -300);
1925 assert!(!sig.time.negative_utc);
1926 assert_eq!(sig.time.offset_token(), "-0500");
1927 assert_eq!(sig.to_ident_bytes(), line);
1928 }
1929
1930 #[test]
1931 fn signature_preserves_negative_zero_timezone_distinct_from_positive_zero() {
1932 let negative = b"Unknown Zone <uz@example.com> 1500000000 -0000";
1933 let positive = b"Known Zone <kz@example.com> 1500000000 +0000";
1934
1935 let neg = Signature::from_ident_line(negative).expect("-0000 parses");
1936 let pos = Signature::from_ident_line(positive).expect("+0000 parses");
1937
1938 assert_eq!(neg.time.timezone_offset_minutes, 0);
1940 assert_eq!(pos.time.timezone_offset_minutes, 0);
1941 assert!(neg.time.negative_utc);
1943 assert!(!pos.time.negative_utc);
1944 assert_ne!(neg.time, pos.time);
1945
1946 assert_eq!(neg.time.offset_token(), "-0000");
1948 assert_eq!(pos.time.offset_token(), "+0000");
1949 assert_eq!(neg.to_ident_bytes(), negative);
1950 assert_eq!(pos.to_ident_bytes(), positive);
1951 assert_eq!(neg.to_canonical_ident_bytes(), negative);
1952 assert_eq!(pos.to_canonical_ident_bytes(), positive);
1953 assert_ne!(neg.to_ident_bytes(), pos.to_ident_bytes());
1954 }
1955
1956 #[test]
1957 fn signature_handles_empty_name_and_email() {
1958 let line = b" <> 0 +0000";
1961 let sig = Signature::from_ident_line(line).expect("empty name/email parses");
1962 assert_eq!(sig.name.as_bytes(), b"");
1963 assert_eq!(sig.email.as_bytes(), b"");
1964 assert_eq!(sig.time.seconds, 0);
1965 assert_eq!(sig.to_ident_bytes(), line);
1966 }
1967
1968 #[test]
1969 fn signature_keeps_angle_brackets_inside_the_name() {
1970 let line = b"Weird <Name> <weird@example.com> 1 +0000";
1974 let sig = Signature::from_ident_line(line).expect("bracketed name parses");
1975 assert_eq!(sig.name.as_bytes(), b"Weird <Name>");
1976 assert_eq!(sig.email.as_bytes(), b"weird@example.com");
1977 assert_eq!(sig.to_ident_bytes(), line);
1978 }
1979
1980 #[test]
1981 fn signature_round_trips_non_canonical_whitespace_via_raw() {
1982 let line = b"Spaced <spaced@example.com> 5 +0000";
1986 let sig = Signature::from_ident_line(line).expect("non-canonical ident parses");
1987 assert_eq!(sig.name.as_bytes(), b"Spaced ");
1989 assert_eq!(sig.to_ident_bytes(), line);
1990 }
1991
1992 #[test]
1993 fn signature_rejects_malformed_idents() {
1994 assert!(Signature::from_ident_line(b"No Email Here 0 +0000").is_none());
1996 assert!(Signature::from_ident_line(b"A U Thor <a@example.com>").is_none());
1998 assert!(Signature::from_ident_line(b"A U Thor <a@example.com> later +0000").is_none());
2000 assert!(Signature::from_ident_line(b"A U Thor <a@example.com> 0 +00").is_none());
2002 assert!(Signature::from_ident_line(b"A U Thor <a@example.com> 0 0000").is_none());
2004 }
2005
2006 #[test]
2007 fn git_time_constructors_set_the_sentinel() {
2008 assert!(!GitTime::new(0, 0).negative_utc);
2009 assert_eq!(GitTime::new(0, 330).offset_token(), "+0530");
2010 let unknown = GitTime::with_negative_utc(42);
2011 assert!(unknown.negative_utc);
2012 assert_eq!(unknown.seconds, 42);
2013 assert_eq!(unknown.offset_token(), "-0000");
2014 }
2015
2016 #[test]
2017 fn full_name_accepts_valid_ref_names() {
2018 let name = FullName::new("refs/heads/main").expect("valid ref name");
2019 assert_eq!(name.as_str(), "refs/heads/main");
2020 assert_eq!(name, "refs/heads/main");
2021 assert_eq!(format!("{name}"), "refs/heads/main");
2022 assert_eq!(String::from(name.clone()), "refs/heads/main");
2023 let borrowed: &str = name.borrow();
2024 assert_eq!(borrowed, "refs/heads/main");
2025 }
2026
2027 #[test]
2028 fn full_name_rejects_invalid_ref_names() {
2029 assert!(FullName::new("").is_err());
2030 assert!(FullName::new(" refs/heads/main").is_err());
2031 assert!(FullName::new("refs/heads/main ").is_err());
2032 assert!(FullName::new("refs//heads/main").is_err());
2033 assert!(FullName::new("refs/heads/\nmain").is_err());
2034 }
2035
2036 #[test]
2037 fn cli_exit_codes_match_git_taxonomy() {
2038 assert_eq!(CliExit::Ok.code(), 0);
2039 assert_eq!(CliExit::UserError.code(), 128);
2040 assert_eq!(CliExit::Usage.code(), 129);
2041 assert_eq!(CliExit::Custom(1).code(), 1);
2042 assert_eq!(CliExit::Custom(5).code(), 5);
2043 }
2044
2045 #[test]
2046 fn git_error_cli_exit_code_mapping() {
2047 assert_eq!(GitError::Exit(129).cli_exit_code(), 129);
2048 assert_eq!(GitError::Exit(128).cli_exit_code(), 128);
2049 assert_eq!(GitError::usage("unknown option").cli_exit_code(), 129);
2050 assert_eq!(
2051 GitError::user_error("not a git repository").cli_exit_code(),
2052 128
2053 );
2054 assert_eq!(
2055 GitError::cli_exit(CliExit::Custom(2), "diff found changes").cli_exit_code(),
2056 2
2057 );
2058 assert_eq!(GitError::Command("bad value".into()).cli_exit_code(), 1);
2059 assert_eq!(GitError::not_found("missing ref").cli_exit_code(), 1);
2060 }
2061
2062 #[test]
2063 fn git_error_cli_displays_message_only() {
2064 let err = GitError::usage("unknown option `--foo'");
2065 assert_eq!(err.to_string(), "unknown option `--foo'");
2066 }
2067
2068 #[test]
2069 fn bstring_round_trips_bytes_and_displays_lossily() {
2070 let path = BString::from_bytes(b"src/\xFF.txt");
2071 assert_eq!(path.as_bytes(), b"src/\xFF.txt");
2072 let borrowed: &[u8] = path.borrow();
2073 assert_eq!(borrowed, b"src/\xFF.txt".as_slice());
2074 assert_eq!(format!("{path}"), "src/\u{FFFD}.txt");
2075 assert_eq!(path, b"src/\xFF.txt");
2076 assert_eq!(path.clone().into_bytes(), b"src/\xFF.txt".to_vec());
2077 }
2078}