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