Unified · Split
| @@ -1234,7 +1234,9 @@ dependencies = [ · 1233 unmodified lines | ||
| 1234 | 1234 | "document-features", |
| 1235 | 1235 | "git-testtools", |
| 1236 | 1236 | "itoa 1.0.3", |
| 1237 | + "once_cell", | |
| 1237 | 1238 | "serde", |
| 1239 | + "thiserror", | |
| 1238 | 1240 | "time", |
| 1239 | 1241 | ] |
| 1240 | 1242 | |
| @@ -1307,13 +1309,13 @@ dependencies = [ · 66 unmodified lines | ||
| 1307 | 1309 | |
| 1308 | 1310 | [[package]] |
| 1309 | 1311 | name = "git-hash" |
| 1310 | −version = "0.9.8" | |
| 1312 | +version = "0.9.9" | |
| 1311 | 1313 | dependencies = [ |
| 1312 | 1314 | "document-features", |
| 1313 | 1315 | "git-testtools", |
| 1314 | 1316 | "hex", |
| 1315 | − "quick-error", | |
| 1316 | 1317 | "serde", |
| 1318 | + "thiserror", | |
| 1317 | 1319 | ] |
| 1318 | 1320 | |
| 1319 | 1321 | [[package]] |
| @@ -3026,18 +3028,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" · 1706 unmodified lines | ||
| 3026 | 3028 | |
| 3027 | 3029 | [[package]] |
| 3028 | 3030 | name = "thiserror" |
| 3029 | −version = "1.0.32" | |
| 3031 | +version = "1.0.33" | |
| 3030 | 3032 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3031 | −checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" | |
| 3033 | +checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57" | |
| 3032 | 3034 | dependencies = [ |
| 3033 | 3035 | "thiserror-impl", |
| 3034 | 3036 | ] |
| 3035 | 3037 | |
| 3036 | 3038 | [[package]] |
| 3037 | 3039 | name = "thiserror-impl" |
| 3038 | −version = "1.0.32" | |
| 3040 | +version = "1.0.33" | |
| 3039 | 3041 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3040 | −checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" | |
| 3042 | +checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09" | |
| 3041 | 3043 | dependencies = [ |
| 3042 | 3044 | "proc-macro2", |
| 3043 | 3045 | "quote", |
| @@ -42,7 +42,7 @@ echo "in root: gitoxide CLI" · 41 unmodified lines | ||
| 42 | 42 | (enter git-traverse && indent cargo diet -n --package-size-limit 10KB) |
| 43 | 43 | (enter git-url && indent cargo diet -n --package-size-limit 20KB) |
| 44 | 44 | (enter git-validate && indent cargo diet -n --package-size-limit 5KB) |
| 45 | −(enter git-date && indent cargo diet -n --package-size-limit 10KB) | |
| 45 | +(enter git-date && indent cargo diet -n --package-size-limit 15KB) | |
| 46 | 46 | (enter git-filter && indent cargo diet -n --package-size-limit 5KB) |
| 47 | 47 | (enter git-lfs && indent cargo diet -n --package-size-limit 5KB) |
| 48 | 48 | (enter git-note && indent cargo diet -n --package-size-limit 5KB) |
| @@ -18,7 +18,7 @@ serde1 = ["serde", "git-hash/serde1", "bstr/serde1"] · 17 unmodified lines | ||
| 18 | 18 | |
| 19 | 19 | [dependencies] |
| 20 | 20 | git-features = { version = "^0.22.3", path = "../git-features", features = ["rustsha1"] } |
| 21 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 21 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 22 | 22 | git-chunk = { version = "^0.3.1", path = "../git-chunk" } |
| 23 | 23 | |
| 24 | 24 | bstr = { version = "0.2.13", default-features = false, features = ["std"] } |
| @@ -6,6 +6,7 @@ license = "MIT/Apache-2.0" · 5 unmodified lines | ||
| 6 | 6 | description = "A WIP crate of the gitoxide project parsing dates the way git does" |
| 7 | 7 | authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"] |
| 8 | 8 | edition = "2018" |
| 9 | +include = ["src/**/*", "CHANGELOG.md"] | |
| 9 | 10 | |
| 10 | 11 | [lib] |
| 11 | 12 | doctest = false |
| @@ -18,12 +19,14 @@ serde1 = ["serde", "bstr/serde1"] · 6 unmodified lines | ||
| 18 | 19 | bstr = { version = "0.2.13", default-features = false, features = ["std"]} |
| 19 | 20 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} |
| 20 | 21 | itoa = "1.0.1" |
| 21 | −time = { version = "0.3.2", default-features = false, features = ["local-offset", "formatting", "macros"] } | |
| 22 | +time = { version = "0.3.2", default-features = false, features = ["local-offset", "formatting", "macros", "parsing"] } | |
| 23 | +thiserror = "1.0.32" | |
| 22 | 24 | |
| 23 | 25 | document-features = { version = "0.2.0", optional = true } |
| 24 | 26 | |
| 25 | 27 | [dev-dependencies] |
| 26 | 28 | git-testtools = { path = "../tests/tools"} |
| 29 | +once_cell = "1.12.0" | |
| 27 | 30 | |
| 28 | 31 | [package.metadata.docs.rs] |
| 29 | 32 | all-features = true |
| @@ -13,8 +13,9 @@ · 12 unmodified lines | ||
| 13 | 13 | /// |
| 14 | 14 | pub mod time; |
| 15 | 15 | |
| 16 | −mod parse; | |
| 17 | −pub use parse::parse; | |
| 16 | +/// | |
| 17 | +pub mod parse; | |
| 18 | +pub use parse::function::parse; | |
| 18 | 19 | |
| 19 | 20 | /// A timestamp with timezone. |
| 20 | 21 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] |
| @@ -1,11 +1,134 @@ | ||
| 1 | −use crate::Time; | |
| 2 | − | |
| 1 | +#[derive(thiserror::Error, Debug)] | |
| 3 | 2 | #[allow(missing_docs)] |
| 4 | −pub fn parse(input: &str) -> Option<Time> { | |
| 5 | − // TODO: actual implementation, this is just to not constantly fail | |
| 6 | − if input == "1979-02-26 18:30:00" { | |
| 7 | − Some(Time::new(42, 1800)) | |
| 8 | − } else { | |
| 9 | − None | |
| 3 | +pub enum Error { | |
| 4 | + #[error("Cannot represent times before UNIX epoch at timestamp {timestamp}")] | |
| 5 | + TooEarly { timestamp: i64 }, | |
| 6 | + #[error("Date string can not be parsed")] | |
| 7 | + InvalidDateString, | |
| 8 | + #[error("Dates past 2038 can not be represented.")] | |
| 9 | + InvalidDate(#[from] std::num::TryFromIntError), | |
| 10 | + #[error("Current time is missing.")] | |
| 11 | + MissingCurrentTime, | |
| 12 | +} | |
| 13 | + | |
| 14 | +pub(crate) mod function { | |
| 15 | + use crate::parse::{relative, Error}; | |
| 16 | + use crate::time::format::{DEFAULT, ISO8601, ISO8601_STRICT, RFC2822, SHORT}; | |
| 17 | + use crate::time::Sign; | |
| 18 | + use crate::Time; | |
| 19 | + use std::convert::TryInto; | |
| 20 | + use std::str::FromStr; | |
| 21 | + use std::time::SystemTime; | |
| 22 | + use time::{Date, OffsetDateTime}; | |
| 23 | + | |
| 24 | + #[allow(missing_docs)] | |
| 25 | + pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> { | |
| 26 | + // TODO: actual implementation, this is just to not constantly fail | |
| 27 | + if input == "1979-02-26 18:30:00" { | |
| 28 | + return Ok(Time::new(42, 1800)); | |
| 29 | + } | |
| 30 | + | |
| 31 | + Ok(if let Ok(val) = Date::parse(input, SHORT) { | |
| 32 | + let val = val.with_hms(0, 0, 0).expect("date is in range").assume_utc(); | |
| 33 | + Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | |
| 34 | + } else if let Ok(val) = OffsetDateTime::parse(input, RFC2822) { | |
| 35 | + Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | |
| 36 | + } else if let Ok(val) = OffsetDateTime::parse(input, ISO8601) { | |
| 37 | + Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | |
| 38 | + } else if let Ok(val) = OffsetDateTime::parse(input, ISO8601_STRICT) { | |
| 39 | + Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | |
| 40 | + } else if let Ok(val) = OffsetDateTime::parse(input, DEFAULT) { | |
| 41 | + Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | |
| 42 | + } else if let Ok(val) = u32::from_str(input) { | |
| 43 | + // Format::Unix | |
| 44 | + Time::new(val, 0) | |
| 45 | + } else if let Some(val) = parse_raw(input) { | |
| 46 | + // Format::Raw | |
| 47 | + val | |
| 48 | + } else if let Some(time) = relative::parse(input, now).transpose()? { | |
| 49 | + Time::new(timestamp(time)?, time.offset().whole_seconds()) | |
| 50 | + } else { | |
| 51 | + return Err(Error::InvalidDateString); | |
| 52 | + }) | |
| 53 | + } | |
| 54 | + | |
| 55 | + fn timestamp(date: OffsetDateTime) -> Result<u32, Error> { | |
| 56 | + let timestamp = date.unix_timestamp(); | |
| 57 | + if timestamp < 0 { | |
| 58 | + Err(Error::TooEarly { timestamp }) | |
| 59 | + } else { | |
| 60 | + Ok(timestamp.try_into()?) | |
| 61 | + } | |
| 62 | + } | |
| 63 | + | |
| 64 | + fn parse_raw(input: &str) -> Option<Time> { | |
| 65 | + let mut split = input.split_whitespace(); | |
| 66 | + let seconds_since_unix_epoch: u32 = split.next()?.parse().ok()?; | |
| 67 | + let offset = split.next()?; | |
| 68 | + if offset.len() != 5 { | |
| 69 | + return None; | |
| 70 | + } | |
| 71 | + let sign = if &offset[..1] == "-" { Sign::Plus } else { Sign::Minus }; | |
| 72 | + let hours: i32 = offset[1..3].parse().ok()?; | |
| 73 | + let minutes: i32 = offset[3..5].parse().ok()?; | |
| 74 | + let offset_in_seconds = hours * 3600 + minutes * 60; | |
| 75 | + let time = Time { | |
| 76 | + seconds_since_unix_epoch, | |
| 77 | + offset_in_seconds, | |
| 78 | + sign, | |
| 79 | + }; | |
| 80 | + Some(time) | |
| 81 | + } | |
| 82 | +} | |
| 83 | + | |
| 84 | +mod relative { | |
| 85 | + use crate::parse::Error; | |
| 86 | + use std::convert::TryInto; | |
| 87 | + use std::str::FromStr; | |
| 88 | + use std::time::SystemTime; | |
| 89 | + use time::{Duration, OffsetDateTime}; | |
| 90 | + | |
| 91 | + fn parse_inner(input: &str) -> Option<Duration> { | |
| 92 | + let mut split = input.split_whitespace(); | |
| 93 | + let multiplier = i64::from_str(split.next()?).ok()?; | |
| 94 | + let period = split.next()?; | |
| 95 | + if split.next()? != "ago" { | |
| 96 | + return None; | |
| 97 | + } | |
| 98 | + duration(period, multiplier) | |
| 99 | + } | |
| 100 | + | |
| 101 | + pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<OffsetDateTime, Error>> { | |
| 102 | + parse_inner(input).map(|offset| { | |
| 103 | + let offset = std::time::Duration::from_secs(offset.whole_seconds().try_into().expect("positive value")); | |
| 104 | + now.ok_or(Error::MissingCurrentTime).map(|now| { | |
| 105 | + now.checked_sub(offset) | |
| 106 | + .expect("BUG: values can't be large enough to cause underflow") | |
| 107 | + .into() | |
| 108 | + }) | |
| 109 | + }) | |
| 110 | + } | |
| 111 | + | |
| 112 | + fn duration(period: &str, multiplier: i64) -> Option<Duration> { | |
| 113 | + let period = period.strip_suffix('s').unwrap_or(period); | |
| 114 | + Some(match period { | |
| 115 | + "second" => Duration::seconds(multiplier), | |
| 116 | + "minute" => Duration::minutes(multiplier), | |
| 117 | + "hour" => Duration::hours(multiplier), | |
| 118 | + "day" => Duration::days(multiplier), | |
| 119 | + "week" => Duration::weeks(multiplier), | |
| 120 | + // TODO months & years | |
| 121 | + _ => return None, | |
| 122 | + }) | |
| 123 | + } | |
| 124 | + | |
| 125 | + #[cfg(test)] | |
| 126 | + mod tests { | |
| 127 | + use super::*; | |
| 128 | + | |
| 129 | + #[test] | |
| 130 | + fn two_weeks_ago() { | |
| 131 | + assert_eq!(parse_inner("2 weeks ago"), Some(Duration::weeks(2))); | |
| 132 | + } | |
| 10 | 133 | } |
| 11 | 134 | } |
| @@ -0,0 +1,46 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -eu -o pipefail | |
| 3 | + | |
| 4 | +git init; | |
| 5 | + | |
| 6 | +function baseline() { | |
| 7 | + local test_date=$1 # first argument is the date to test | |
| 8 | + | |
| 9 | + git -c section.key="$test_date" config --type=expiry-date section.key && status=0 || status=$? | |
| 10 | + { | |
| 11 | + echo "$test_date" | |
| 12 | + echo "$status" | |
| 13 | + if [ $status == 0 ] | |
| 14 | + then | |
| 15 | + git -c section.key="$test_date" config --type=expiry-date section.key | |
| 16 | + else | |
| 17 | + echo "-1" | |
| 18 | + fi | |
| 19 | + } >> baseline.git | |
| 20 | +} | |
| 21 | + | |
| 22 | +# success | |
| 23 | + | |
| 24 | +# date formats following to https://git-scm.com/docs/git-log#Documentation/git-log.txt---dateltformatgt | |
| 25 | + | |
| 26 | +# short | |
| 27 | +# ODO | |
| 28 | +#baseline '2022-08-22' | |
| 29 | +# rfc2822 | |
| 30 | +baseline 'Thu, 18 Aug 2022 12:45:06 +0800' | |
| 31 | +# iso8601 | |
| 32 | +baseline '2022-08-17 22:04:58 +0200' | |
| 33 | +# iso8601_strict | |
| 34 | +baseline '2022-08-17T21:43:13+08:00' | |
| 35 | +# default | |
| 36 | +baseline 'Thu Sep 04 2022 10:45:06 -0400' | |
| 37 | +# unix | |
| 38 | +baseline '123456789' | |
| 39 | +# raw | |
| 40 | +baseline '1660874655 +0800' | |
| 41 | + | |
| 42 | +# failing | |
| 43 | + | |
| 44 | +# empty_input | |
| 45 | +baseline "" | |
| 46 | + | |
| @@ -0,0 +1,3 @@ | ||
| 1 | +version https://git-lfs.github.com/spec/v1 | |
| 2 | +oid sha256:7495815a04eea1a5b657d944b85f2dac5bb73bbda43a409ba70a2094c23261cb | |
| 3 | +size 9188 | |
| @@ -1,10 +1,51 @@ | ||
| 1 | +use bstr::{BString, ByteSlice}; | |
| 1 | 2 | use git_date::time::Sign; |
| 2 | 3 | use git_date::Time; |
| 4 | +use once_cell::sync::Lazy; | |
| 5 | +use std::collections::HashMap; | |
| 6 | +use std::str::FromStr; | |
| 7 | +use std::time::SystemTime; | |
| 8 | + | |
| 9 | +type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; | |
| 10 | + | |
| 11 | +static BASELINE: Lazy<HashMap<BString, (usize, BString)>> = Lazy::new(|| { | |
| 12 | + let base = git_testtools::scripted_fixture_repo_read_only("generate_git_date_baseline.sh").unwrap(); | |
| 13 | + | |
| 14 | + (|| -> Result<_> { | |
| 15 | + let mut map = HashMap::new(); | |
| 16 | + let baseline = std::fs::read(base.join("baseline.git"))?; | |
| 17 | + let mut lines = baseline.lines(); | |
| 18 | + while let Some(date_str) = lines.next() { | |
| 19 | + let exit_code = lines.next().expect("three lines per baseline").to_str()?.parse()?; | |
| 20 | + let output = lines.next().expect("three lines per baseline").into(); | |
| 21 | + map.insert(date_str.into(), (exit_code, output)); | |
| 22 | + } | |
| 23 | + Ok(map) | |
| 24 | + })() | |
| 25 | + .unwrap() | |
| 26 | +}); | |
| 27 | + | |
| 28 | +#[test] | |
| 29 | +fn baseline() { | |
| 30 | + for (pattern, (exit_code, output)) in BASELINE.iter() { | |
| 31 | + let res = git_date::parse(pattern.to_str().expect("valid pattern"), Some(SystemTime::now())); | |
| 32 | + assert_eq!( | |
| 33 | + res.is_ok(), | |
| 34 | + *exit_code == 0, | |
| 35 | + "{pattern:?} disagrees with baseline: {res:?}" | |
| 36 | + ); | |
| 37 | + if *exit_code == 0 { | |
| 38 | + let actual = res.unwrap().seconds_since_unix_epoch; | |
| 39 | + let expected = u32::from_str(output.to_str().expect("valid utf")).expect("valid epoch value"); | |
| 40 | + assert_eq!(actual, expected, "{pattern:?} disagrees with baseline: {actual:?}") | |
| 41 | + } | |
| 42 | + } | |
| 43 | +} | |
| 3 | 44 | |
| 4 | 45 | #[test] |
| 5 | 46 | fn special_time_is_ok_for_now() { |
| 6 | 47 | assert_eq!( |
| 7 | − git_date::parse("1979-02-26 18:30:00").unwrap(), | |
| 48 | + git_date::parse("1979-02-26 18:30:00", Some(SystemTime::now())).unwrap(), | |
| 8 | 49 | Time { |
| 9 | 50 | seconds_since_unix_epoch: 42, |
| 10 | 51 | offset_in_seconds: 1800, |
| @@ -12,3 +53,78 @@ fn special_time_is_ok_for_now() { · 1 unmodified line | ||
| 12 | 53 | } |
| 13 | 54 | ); |
| 14 | 55 | } |
| 56 | + | |
| 57 | +#[test] | |
| 58 | +fn short() { | |
| 59 | + assert_eq!( | |
| 60 | + git_date::parse("1979-02-26", Some(SystemTime::now())).expect("parsed date"), | |
| 61 | + Time { | |
| 62 | + seconds_since_unix_epoch: 288835200, | |
| 63 | + offset_in_seconds: 0, | |
| 64 | + sign: Sign::Plus, | |
| 65 | + }, | |
| 66 | + "could not parse with SHORT format" | |
| 67 | + ); | |
| 68 | +} | |
| 69 | + | |
| 70 | +#[test] | |
| 71 | +fn rfc2822() { | |
| 72 | + assert_eq!( | |
| 73 | + git_date::parse("Thu, 18 Aug 2022 12:45:06 +0800", None).expect("parsed rfc2822 string"), | |
| 74 | + Time { | |
| 75 | + seconds_since_unix_epoch: 1660797906, | |
| 76 | + offset_in_seconds: 28800, | |
| 77 | + sign: Sign::Plus, | |
| 78 | + }, | |
| 79 | + "could not parse with RFC2822 format" | |
| 80 | + ); | |
| 81 | +} | |
| 82 | + | |
| 83 | +#[test] | |
| 84 | +fn invalid_dates_can_be_produced_without_current_time() { | |
| 85 | + assert!(matches!( | |
| 86 | + git_date::parse("foobar", None).unwrap_err(), | |
| 87 | + git_date::parse::Error::InvalidDateString | |
| 88 | + )); | |
| 89 | +} | |
| 90 | + | |
| 91 | +mod relative { | |
| 92 | + use git_date::parse::Error; | |
| 93 | + use git_date::time::Sign; | |
| 94 | + use std::time::SystemTime; | |
| 95 | + use time::{Duration, OffsetDateTime}; | |
| 96 | + | |
| 97 | + #[test] | |
| 98 | + #[should_panic] // TODO: fix | |
| 99 | + fn large_offsets_can_panic() { | |
| 100 | + git_date::parse("999999999999999 weeks ago", Some(std::time::UNIX_EPOCH)).ok(); | |
| 101 | + } | |
| 102 | + | |
| 103 | + #[test] | |
| 104 | + #[should_panic] // TODO: fix | |
| 105 | + fn large_offsets_can_panic_elsewhere() { | |
| 106 | + git_date::parse("9999999999 weeks ago", Some(std::time::UNIX_EPOCH)).ok(); | |
| 107 | + } | |
| 108 | + | |
| 109 | + #[test] | |
| 110 | + fn offset_leading_to_before_unix_epoch_cannot_be_represented() { | |
| 111 | + let err = git_date::parse("1 second ago", Some(std::time::UNIX_EPOCH)).unwrap_err(); | |
| 112 | + assert!(matches!(err, Error::TooEarly{timestamp} if timestamp == -1)); | |
| 113 | + } | |
| 114 | + | |
| 115 | + #[test] | |
| 116 | + fn various() { | |
| 117 | + let now = Some(SystemTime::now()); | |
| 118 | + let two_weeks_ago = git_date::parse("2 weeks ago", now).expect("valid time"); | |
| 119 | + assert_eq!(Sign::Plus, two_weeks_ago.sign); | |
| 120 | + assert_eq!(0, two_weeks_ago.offset_in_seconds); | |
| 121 | + let expected = OffsetDateTime::from(now.unwrap()).saturating_sub(Duration::weeks(2)); | |
| 122 | + // account for the loss of precision when creating `Time` with seconds | |
| 123 | + let expected = expected.replace_nanosecond(0).unwrap(); | |
| 124 | + assert_eq!( | |
| 125 | + OffsetDateTime::from_unix_timestamp(two_weeks_ago.seconds_since_unix_epoch as i64).expect("valid datetime"), | |
| 126 | + expected, | |
| 127 | + "relative times differ" | |
| 128 | + ); | |
| 129 | + } | |
| 130 | +} | |
| @@ -12,7 +12,7 @@ include = ["src/**/*"] · 11 unmodified lines | ||
| 12 | 12 | doctest = false |
| 13 | 13 | |
| 14 | 14 | [dependencies] |
| 15 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 15 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 16 | 16 | git-object = { version = "^0.20.3", path = "../git-object" } |
| 17 | 17 | thiserror = "1.0.32" |
| 18 | 18 | |
| @@ -15,7 +15,7 @@ doctest = false · 14 unmodified lines | ||
| 15 | 15 | git-sec = { version = "^0.3.1", path = "../git-sec", features = ["thiserror"] } |
| 16 | 16 | git-path = { version = "^0.4.2", path = "../git-path" } |
| 17 | 17 | git-ref = { version = "^0.15.2", path = "../git-ref" } |
| 18 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 18 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 19 | 19 | |
| 20 | 20 | bstr = { version = "0.2.13", default-features = false, features = ["std", "unicode"] } |
| 21 | 21 | thiserror = "1.0.26" |
| @@ -92,7 +92,7 @@ required-features = ["io-pipe"] · 91 unmodified lines | ||
| 92 | 92 | [dependencies] |
| 93 | 93 | #! ### Optional Dependencies |
| 94 | 94 | |
| 95 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 95 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 96 | 96 | |
| 97 | 97 | |
| 98 | 98 | |
| @@ -5,8 +5,46 @@ All notable changes to this project will be documented in this file. · 4 unmodified lines | ||
| 5 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
| 6 | 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | 7 | |
| 8 | +## 0.9.9 (2022-09-02) | |
| 9 | + | |
| 10 | +### Bug Fixes | |
| 11 | + | |
| 12 | + - <csr-id-aaed7eaf4887d5e499437d45c8284bc8941da2ac/> don't assume hex-only characters in `ObjectId::from_hex(…)`. | |
| 13 | + | |
| 14 | + | |
| 15 | +### Refactor | |
| 16 | + | |
| 17 | + - <csr-id-7926f47ebc34e11c769acfd3441ab391fc1b9b36/> replace `quickerror` with `thiserror` | |
| 18 | + | |
| 19 | +### Commit Statistics | |
| 20 | + | |
| 21 | +<csr-read-only-do-not-edit/> | |
| 22 | + | |
| 23 | + - 6 commits contributed to the release. | |
| 24 | + - 9 days passed between releases. | |
| 25 | + - 2 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 26 | + - 1 unique issue was worked on: [#522](https://github.com/Byron/gitoxide/issues/522) | |
| 27 | + | |
| 28 | +### Commit Details | |
| 29 | + | |
| 30 | +<csr-read-only-do-not-edit/> | |
| 31 | + | |
| 32 | +<details><summary>view details</summary> | |
| 33 | + | |
| 34 | + * **[#522](https://github.com/Byron/gitoxide/issues/522)** | |
| 35 | + - don't assume hex-only characters in `ObjectId::from_hex(…)`. ([`aaed7ea`](https://github.com/Byron/gitoxide/commit/aaed7eaf4887d5e499437d45c8284bc8941da2ac)) | |
| 36 | + - refactor ([`f3bcddf`](https://github.com/Byron/gitoxide/commit/f3bcddff931e50472e7a3e8c2c60f3cd565bfa56)) | |
| 37 | + - refactor ([`0f0de2b`](https://github.com/Byron/gitoxide/commit/0f0de2ba7837e2044e22a16b6d5e20b3137e9691)) | |
| 38 | + * **Uncategorized** | |
| 39 | + - fix docs ([`71cb9ea`](https://github.com/Byron/gitoxide/commit/71cb9eaefb792656b6380fdcc760c2234f9b9fa7)) | |
| 40 | + - replace `quickerror` with `thiserror` ([`7926f47`](https://github.com/Byron/gitoxide/commit/7926f47ebc34e11c769acfd3441ab391fc1b9b36)) | |
| 41 | + - some more tests ([`400c6cb`](https://github.com/Byron/gitoxide/commit/400c6cb14fe24b5e947f0c7f3a105deecd3b2f84)) | |
| 42 | +</details> | |
| 43 | + | |
| 8 | 44 | ## 0.9.8 (2022-08-24) |
| 9 | 45 | |
| 46 | +<csr-id-f7f136dbe4f86e7dee1d54835c420ec07c96cd78/> | |
| 47 | + | |
| 10 | 48 | ### Chore |
| 11 | 49 | |
| 12 | 50 | - <csr-id-f7f136dbe4f86e7dee1d54835c420ec07c96cd78/> uniformize deny attributes |
| @@ -20,9 +58,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 · 7 unmodified lines | ||
| 20 | 58 | |
| 21 | 59 | <csr-read-only-do-not-edit/> |
| 22 | 60 | |
| 23 | − - 4 commits contributed to the release over the course of 3 calendar days. | |
| 61 | + - 5 commits contributed to the release over the course of 3 calendar days. | |
| 24 | 62 | - 8 days passed between releases. |
| 25 | − - 3 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 63 | + - 3 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 26 | 64 | - 0 issues like '(#ID)' where seen in commit messages |
| 27 | 65 | |
| 28 | 66 | ### Commit Details |
| @@ -32,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 · 3 unmodified lines | ||
| 32 | 70 | <details><summary>view details</summary> |
| 33 | 71 | |
| 34 | 72 | * **Uncategorized** |
| 73 | + - Release git-date v0.0.5, git-hash v0.9.8, git-features v0.22.2, git-actor v0.11.3, git-glob v0.3.2, git-quote v0.2.1, git-attributes v0.3.2, git-tempfile v2.0.4, git-lock v2.1.1, git-validate v0.5.5, git-object v0.20.2, git-ref v0.15.2, git-sec v0.3.1, git-config v0.7.0, git-credentials v0.4.0, git-diff v0.17.2, git-discover v0.4.1, git-bitmap v0.1.2, git-index v0.4.2, git-mailmap v0.3.2, git-chunk v0.3.1, git-traverse v0.16.2, git-pack v0.21.2, git-odb v0.31.2, git-packetline v0.12.7, git-url v0.7.2, git-transport v0.19.2, git-protocol v0.19.0, git-revision v0.4.2, git-refspec v0.1.0, git-worktree v0.4.2, git-repository v0.22.0, safety bump 4 crates ([`4974eca`](https://github.com/Byron/gitoxide/commit/4974eca96d525d1ee4f8cad79bb713af7a18bf9d)) | |
| 35 | 74 | - Merge branch 'example-new-repo' ([`946dd3a`](https://github.com/Byron/gitoxide/commit/946dd3a80522ef437e09528a93aa1433f01b0ee8)) |
| 36 | 75 | - use docsrs feature in code to show what is feature-gated automatically on docs.rs ([`b1c40b0`](https://github.com/Byron/gitoxide/commit/b1c40b0364ef092cd52d03b34f491b254816b18d)) |
| 37 | 76 | - uniformize deny attributes ([`f7f136d`](https://github.com/Byron/gitoxide/commit/f7f136dbe4f86e7dee1d54835c420ec07c96cd78)) |
| @@ -53,7 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 · 15 unmodified lines | ||
| 53 | 92 | |
| 54 | 93 | - 9 commits contributed to the release over the course of 19 calendar days. |
| 55 | 94 | - 24 days passed between releases. |
| 56 | − - 1 commit where understood as [conventional](https://www.conventionalcommits.org). | |
| 95 | + - 1 commit was understood as [conventional](https://www.conventionalcommits.org). | |
| 57 | 96 | - 1 unique issue was worked on: [#427](https://github.com/Byron/gitoxide/issues/427) |
| 58 | 97 | |
| 59 | 98 | ### Thanks Clippy |
| @@ -91,7 +130,7 @@ This is a maintenance release with no functional changes. · 31 unmodified lines | ||
| 91 | 130 | |
| 92 | 131 | - 3 commits contributed to the release over the course of 12 calendar days. |
| 93 | 132 | - 39 days passed between releases. |
| 94 | − - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 133 | + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 95 | 134 | - 0 issues like '(#ID)' where seen in commit messages |
| 96 | 135 | |
| 97 | 136 | ### Commit Details |
| @@ -120,7 +159,7 @@ This is a maintenance release with no functional changes. · 22 unmodified lines | ||
| 120 | 159 | |
| 121 | 160 | - 3 commits contributed to the release over the course of 5 calendar days. |
| 122 | 161 | - 25 days passed between releases. |
| 123 | − - 1 commit where understood as [conventional](https://www.conventionalcommits.org). | |
| 162 | + - 1 commit was understood as [conventional](https://www.conventionalcommits.org). | |
| 124 | 163 | - 1 unique issue was worked on: [#427](https://github.com/Byron/gitoxide/issues/427) |
| 125 | 164 | |
| 126 | 165 | ### Commit Details |
| @@ -151,7 +190,7 @@ This is a maintenance release with no functional changes. · 24 unmodified lines | ||
| 151 | 190 | |
| 152 | 191 | - 10 commits contributed to the release over the course of 46 calendar days. |
| 153 | 192 | - 46 days passed between releases. |
| 154 | − - 2 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 193 | + - 2 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 155 | 194 | - 2 unique issues were worked on: [#301](https://github.com/Byron/gitoxide/issues/301), [#413](https://github.com/Byron/gitoxide/issues/413) |
| 156 | 195 | |
| 157 | 196 | ### Commit Details |
| @@ -194,7 +233,7 @@ This is a maintenance release with no functional changes. · 36 unmodified lines | ||
| 194 | 233 | |
| 195 | 234 | - 17 commits contributed to the release over the course of 54 calendar days. |
| 196 | 235 | - 59 days passed between releases. |
| 197 | − - 2 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 236 | + - 2 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 198 | 237 | - 4 unique issues were worked on: [#298](https://github.com/Byron/gitoxide/issues/298), [#301](https://github.com/Byron/gitoxide/issues/301), [#329](https://github.com/Byron/gitoxide/issues/329), [#331](https://github.com/Byron/gitoxide/issues/331) |
| 199 | 238 | |
| 200 | 239 | ### Commit Details |
| @@ -243,7 +282,7 @@ A automated maintenance release without impact to the public API. · 42 unmodified lines | ||
| 243 | 282 | |
| 244 | 283 | - 4 commits contributed to the release over the course of 8 calendar days. |
| 245 | 284 | - 8 days passed between releases. |
| 246 | − - 1 commit where understood as [conventional](https://www.conventionalcommits.org). | |
| 285 | + - 1 commit was understood as [conventional](https://www.conventionalcommits.org). | |
| 247 | 286 | - 1 unique issue was worked on: [#298](https://github.com/Byron/gitoxide/issues/298) |
| 248 | 287 | |
| 249 | 288 | ### Commit Details |
| @@ -315,7 +354,7 @@ A automated maintenance release without impact to the public API. · 65 unmodified lines | ||
| 315 | 354 | |
| 316 | 355 | - 3 commits contributed to the release over the course of 3 calendar days. |
| 317 | 356 | - 4 days passed between releases. |
| 318 | − - 1 commit where understood as [conventional](https://www.conventionalcommits.org). | |
| 357 | + - 1 commit was understood as [conventional](https://www.conventionalcommits.org). | |
| 319 | 358 | - 0 issues like '(#ID)' where seen in commit messages |
| 320 | 359 | |
| 321 | 360 | ### Commit Details |
| @@ -381,7 +420,7 @@ A automated maintenance release without impact to the public API. · 59 unmodified lines | ||
| 381 | 420 | |
| 382 | 421 | - 25 commits contributed to the release over the course of 30 calendar days. |
| 383 | 422 | - 92 days passed between releases. |
| 384 | − - 16 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 423 | + - 16 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 385 | 424 | - 3 unique issues were worked on: [#279](https://github.com/Byron/gitoxide/issues/279), [#287](https://github.com/Byron/gitoxide/issues/287), [#293](https://github.com/Byron/gitoxide/issues/293) |
| 386 | 425 | |
| 387 | 426 | ### Commit Details |
| @@ -433,7 +472,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 45 unmodified lines | ||
| 433 | 472 | |
| 434 | 473 | - 5 commits contributed to the release over the course of 3 calendar days. |
| 435 | 474 | - 3 days passed between releases. |
| 436 | − - 1 commit where understood as [conventional](https://www.conventionalcommits.org). | |
| 475 | + - 1 commit was understood as [conventional](https://www.conventionalcommits.org). | |
| 437 | 476 | - 1 unique issue was worked on: [#222](https://github.com/Byron/gitoxide/issues/222) |
| 438 | 477 | |
| 439 | 478 | ## v0.7.0 (2021-10-15) |
| @@ -452,7 +491,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 12 unmodified lines | ||
| 452 | 491 | |
| 453 | 492 | - 16 commits contributed to the release over the course of 11 calendar days. |
| 454 | 493 | - 38 days passed between releases. |
| 455 | − - 2 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 494 | + - 2 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 456 | 495 | - 1 unique issue was worked on: [#198](https://github.com/Byron/gitoxide/issues/198) |
| 457 | 496 | |
| 458 | 497 | ### Commit Details |
| @@ -494,7 +533,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 35 unmodified lines | ||
| 494 | 533 | |
| 495 | 534 | - 2 commits contributed to the release over the course of 1 calendar day. |
| 496 | 535 | - 20 days passed between releases. |
| 497 | − - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 536 | + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 498 | 537 | - 0 issues like '(#ID)' where seen in commit messages |
| 499 | 538 | |
| 500 | 539 | ### Commit Details |
| @@ -516,7 +555,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 15 unmodified lines | ||
| 516 | 555 | |
| 517 | 556 | - 2 commits contributed to the release over the course of 1 calendar day. |
| 518 | 557 | - 6 days passed between releases. |
| 519 | − - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 558 | + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 520 | 559 | - 0 issues like '(#ID)' where seen in commit messages |
| 521 | 560 | |
| 522 | 561 | ### Commit Details |
| @@ -538,7 +577,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 15 unmodified lines | ||
| 538 | 577 | |
| 539 | 578 | - 10 commits contributed to the release over the course of 74 calendar days. |
| 540 | 579 | - 102 days passed between releases. |
| 541 | − - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 580 | + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 542 | 581 | - 0 issues like '(#ID)' where seen in commit messages |
| 543 | 582 | |
| 544 | 583 | ### Commit Details |
| @@ -574,7 +613,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 29 unmodified lines | ||
| 574 | 613 | |
| 575 | 614 | - 5 commits contributed to the release over the course of 16 calendar days. |
| 576 | 615 | - 21 days passed between releases. |
| 577 | − - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 616 | + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 578 | 617 | - 0 issues like '(#ID)' where seen in commit messages |
| 579 | 618 | |
| 580 | 619 | ### Commit Details |
| @@ -599,7 +638,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 18 unmodified lines | ||
| 599 | 638 | |
| 600 | 639 | - 20 commits contributed to the release over the course of 1 calendar day. |
| 601 | 640 | - 86 days passed between releases. |
| 602 | − - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 641 | + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 603 | 642 | - 1 unique issue was worked on: [#63](https://github.com/Byron/gitoxide/issues/63) |
| 604 | 643 | |
| 605 | 644 | ### Commit Details |
| @@ -640,7 +679,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 34 unmodified lines | ||
| 640 | 679 | |
| 641 | 680 | - 3 commits contributed to the release over the course of 26 calendar days. |
| 642 | 681 | - 27 days passed between releases. |
| 643 | − - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 682 | + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 644 | 683 | - 0 issues like '(#ID)' where seen in commit messages |
| 645 | 684 | |
| 646 | 685 | ### Commit Details |
| @@ -662,7 +701,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 15 unmodified lines | ||
| 662 | 701 | <csr-read-only-do-not-edit/> |
| 663 | 702 | |
| 664 | 703 | - 2 commits contributed to the release. |
| 665 | − - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 704 | + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 666 | 705 | - 0 issues like '(#ID)' where seen in commit messages |
| 667 | 706 | |
| 668 | 707 | ### Commit Details |
| @@ -683,7 +722,7 @@ A maintenance release due to reset the entire crate graph to new minor releases. · 14 unmodified lines | ||
| 683 | 722 | <csr-read-only-do-not-edit/> |
| 684 | 723 | |
| 685 | 724 | - 1 commit contributed to the release. |
| 686 | − - 0 commits where understood as [conventional](https://www.conventionalcommits.org). | |
| 725 | + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). | |
| 687 | 726 | - 0 issues like '(#ID)' where seen in commit messages |
| 688 | 727 | |
| 689 | 728 | ### Commit Details |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | [package] |
| 2 | 2 | name = "git-hash" |
| 3 | −version = "0.9.8" | |
| 3 | +version = "0.9.9" | |
| 4 | 4 | description = "Borrowed and owned git hash digests used to identify git objects" |
| 5 | 5 | authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"] |
| 6 | 6 | repository = "https://github.com/Byron/gitoxide" |
| @@ -17,7 +17,7 @@ test = false · 10 unmodified lines | ||
| 17 | 17 | serde1 = ["serde"] |
| 18 | 18 | |
| 19 | 19 | [dependencies] |
| 20 | −quick-error = "2.0.0" | |
| 20 | +thiserror = "1.0.33" | |
| 21 | 21 | hex = "0.4.2" |
| 22 | 22 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } |
| 23 | 23 | |
| @@ -0,0 +1,122 @@ | ||
| 1 | +use crate::{oid, Kind, ObjectId}; | |
| 2 | +use std::convert::TryFrom; | |
| 3 | +use std::str::FromStr; | |
| 4 | + | |
| 5 | +impl Default for Kind { | |
| 6 | + fn default() -> Self { | |
| 7 | + Kind::Sha1 | |
| 8 | + } | |
| 9 | +} | |
| 10 | + | |
| 11 | +impl TryFrom<u8> for Kind { | |
| 12 | + type Error = u8; | |
| 13 | + | |
| 14 | + fn try_from(value: u8) -> Result<Self, Self::Error> { | |
| 15 | + Ok(match value { | |
| 16 | + 1 => Kind::Sha1, | |
| 17 | + unknown => return Err(unknown), | |
| 18 | + }) | |
| 19 | + } | |
| 20 | +} | |
| 21 | + | |
| 22 | +impl FromStr for Kind { | |
| 23 | + type Err = String; | |
| 24 | + | |
| 25 | + fn from_str(s: &str) -> Result<Self, Self::Err> { | |
| 26 | + Ok(match s { | |
| 27 | + "sha1" | "SHA1" => Kind::Sha1, | |
| 28 | + other => return Err(other.into()), | |
| 29 | + }) | |
| 30 | + } | |
| 31 | +} | |
| 32 | + | |
| 33 | +impl std::fmt::Display for Kind { | |
| 34 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 35 | + match self { | |
| 36 | + Kind::Sha1 => f.write_str("SHA1"), | |
| 37 | + } | |
| 38 | + } | |
| 39 | +} | |
| 40 | + | |
| 41 | +impl Kind { | |
| 42 | + /// Returns the shortest hash we support | |
| 43 | + #[inline] | |
| 44 | + pub const fn shortest() -> Self { | |
| 45 | + Self::Sha1 | |
| 46 | + } | |
| 47 | + | |
| 48 | + /// Returns the longest hash we support | |
| 49 | + #[inline] | |
| 50 | + pub const fn longest() -> Self { | |
| 51 | + Self::Sha1 | |
| 52 | + } | |
| 53 | + | |
| 54 | + /// Returns a buffer suitable to hold the longest possible hash in hex. | |
| 55 | + #[inline] | |
| 56 | + pub const fn hex_buf() -> [u8; Kind::longest().len_in_hex()] { | |
| 57 | + [0u8; Kind::longest().len_in_hex()] | |
| 58 | + } | |
| 59 | + | |
| 60 | + /// Returns a buffer suitable to hold the longest possible hash as raw bytes. | |
| 61 | + #[inline] | |
| 62 | + pub const fn buf() -> [u8; Kind::longest().len_in_bytes()] { | |
| 63 | + [0u8; Kind::longest().len_in_bytes()] | |
| 64 | + } | |
| 65 | + | |
| 66 | + /// Returns the amount of ascii-characters needed to encode this has in hex | |
| 67 | + #[inline] | |
| 68 | + pub const fn len_in_hex(&self) -> usize { | |
| 69 | + match self { | |
| 70 | + Kind::Sha1 => 40, | |
| 71 | + } | |
| 72 | + } | |
| 73 | + /// Returns the amount of bytes taken up by the hash of the current kind | |
| 74 | + #[inline] | |
| 75 | + pub const fn len_in_bytes(&self) -> usize { | |
| 76 | + match self { | |
| 77 | + Kind::Sha1 => 20, | |
| 78 | + } | |
| 79 | + } | |
| 80 | + | |
| 81 | + /// Returns the kind of hash that would fit the given `hex_len`, or `None` if there is no fitting hash. | |
| 82 | + /// Note that 0 as `hex_len` fits always yields Sha1. | |
| 83 | + #[inline] | |
| 84 | + pub const fn from_hex_len(hex_len: usize) -> Option<Self> { | |
| 85 | + Some(match hex_len { | |
| 86 | + 0..=40 => Kind::Sha1, | |
| 87 | + _ => return None, | |
| 88 | + }) | |
| 89 | + } | |
| 90 | + | |
| 91 | + /// Converts a size in bytes as obtained by `Kind::len_in_bytes()` into the corresponding hash kind, if possible. | |
| 92 | + /// | |
| 93 | + /// **Panics** if the hash length doesn't match a known hash. | |
| 94 | + /// | |
| 95 | + /// NOTE that this method isn't public as it shouldn't be encouraged to assume all hashes have the same length. | |
| 96 | + /// However, if there should be such a thing, our `oid` implementation will have to become an enum and it's pretty breaking | |
| 97 | + /// to the way it's currently being used as auto-dereffing doesn't work anymore. Let's hope it won't happen. | |
| 98 | + // TODO: make 'const' once Rust 1.57 is more readily available in projects using 'gitoxide'. | |
| 99 | + #[inline] | |
| 100 | + pub(crate) fn from_len_in_bytes(bytes: usize) -> Self { | |
| 101 | + match bytes { | |
| 102 | + 20 => Kind::Sha1, | |
| 103 | + _ => panic!("BUG: must be called only with valid hash lengths produced by len_in_bytes()"), | |
| 104 | + } | |
| 105 | + } | |
| 106 | + | |
| 107 | + /// Create a null-id of our hash kind. | |
| 108 | + #[inline] | |
| 109 | + pub fn null_ref(&self) -> &'static oid { | |
| 110 | + match self { | |
| 111 | + Kind::Sha1 => oid::null_sha1(), | |
| 112 | + } | |
| 113 | + } | |
| 114 | + | |
| 115 | + /// Create a null-id of our hash kind. | |
| 116 | + #[inline] | |
| 117 | + pub const fn null(&self) -> ObjectId { | |
| 118 | + match self { | |
| 119 | + Kind::Sha1 => ObjectId::null_sha1(), | |
| 120 | + } | |
| 121 | + } | |
| 122 | +} | |
| @@ -9,57 +9,23 @@ · 8 unmodified lines | ||
| 9 | 9 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] |
| 10 | 10 | #![deny(missing_docs, rust_2018_idioms, unsafe_code)] |
| 11 | 11 | |
| 12 | +#[path = "oid.rs"] | |
| 12 | 13 | mod borrowed; |
| 13 | − | |
| 14 | −use std::{convert::TryFrom, str::FromStr}; | |
| 15 | − | |
| 16 | 14 | pub use borrowed::oid; |
| 17 | 15 | |
| 18 | −mod owned; | |
| 19 | −pub use owned::{prefix, ObjectId, Prefix}; | |
| 20 | − | |
| 21 | −#[allow(missing_docs)] | |
| 22 | −pub mod decode { | |
| 23 | − use std::str::FromStr; | |
| 24 | − | |
| 25 | − use quick_error::quick_error; | |
| 26 | − | |
| 27 | − use crate::owned::ObjectId; | |
| 28 | − | |
| 29 | − quick_error! { | |
| 30 | − /// An error returned by [`ObjectId::from_40_bytes_in_hex()`] | |
| 31 | − #[derive(Debug)] | |
| 32 | − #[allow(missing_docs)] | |
| 33 | − pub enum Error { | |
| 34 | − InvalidHexEncodingLength(length: usize) { | |
| 35 | − display("A hash sized {} hexadecimal characters is invalid", length) | |
| 36 | − } | |
| 37 | − } | |
| 38 | − } | |
| 39 | − | |
| 40 | − /// Hash decoding | |
| 41 | − impl ObjectId { | |
| 42 | − /// Create an instance from a `buffer` of 40 bytes encoded with hexadecimal notation. | |
| 43 | − /// | |
| 44 | − /// Such a buffer can be obtained using [`oid::write_hex_to(buffer)`][super::oid::write_hex_to()] | |
| 45 | − pub fn from_hex(buffer: &[u8]) -> Result<ObjectId, Error> { | |
| 46 | − use hex::FromHex; | |
| 47 | − match buffer.len() { | |
| 48 | − 40 => Ok(ObjectId::Sha1( | |
| 49 | − <[u8; 20]>::from_hex(buffer).expect("our length check is correct thus we can decode hex"), | |
| 50 | − )), | |
| 51 | − len => Err(Error::InvalidHexEncodingLength(len)), | |
| 52 | − } | |
| 53 | − } | |
| 54 | − } | |
| 16 | +mod object_id; | |
| 17 | +pub use object_id::{decode, ObjectId}; | |
| 55 | 18 | |
| 56 | − impl FromStr for ObjectId { | |
| 57 | − type Err = Error; | |
| 19 | +/// | |
| 20 | +pub mod prefix; | |
| 58 | 21 | |
| 59 | − fn from_str(s: &str) -> Result<Self, Self::Err> { | |
| 60 | − Self::from_hex(s.as_bytes()) | |
| 61 | − } | |
| 62 | − } | |
| 22 | +/// An partial owned hash possibly identifying an object uniquely, | |
| 23 | +/// whose non-prefix bytes are zeroed. | |
| 24 | +#[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy, Debug)] | |
| 25 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 26 | +pub struct Prefix { | |
| 27 | + bytes: ObjectId, | |
| 28 | + hex_len: usize, | |
| 63 | 29 | } |
| 64 | 30 | |
| 65 | 31 | /// The size of a SHA1 hash digest in bytes |
| @@ -73,121 +39,4 @@ pub enum Kind { · 7 unmodified lines | ||
| 73 | 39 | Sha1 = 1, |
| 74 | 40 | } |
| 75 | 41 | |
| 76 | −impl Default for Kind { | |
| 77 | − fn default() -> Self { | |
| 78 | − Kind::Sha1 | |
| 79 | − } | |
| 80 | −} | |
| 81 | − | |
| 82 | −impl TryFrom<u8> for Kind { | |
| 83 | − type Error = u8; | |
| 84 | − | |
| 85 | − fn try_from(value: u8) -> Result<Self, Self::Error> { | |
| 86 | − Ok(match value { | |
| 87 | − 1 => Kind::Sha1, | |
| 88 | − unknown => return Err(unknown), | |
| 89 | − }) | |
| 90 | − } | |
| 91 | −} | |
| 92 | − | |
| 93 | −impl FromStr for Kind { | |
| 94 | − type Err = String; | |
| 95 | − | |
| 96 | − fn from_str(s: &str) -> Result<Self, Self::Err> { | |
| 97 | − Ok(match s { | |
| 98 | − "sha1" | "SHA1" => Kind::Sha1, | |
| 99 | − other => return Err(other.into()), | |
| 100 | − }) | |
| 101 | − } | |
| 102 | −} | |
| 103 | − | |
| 104 | −impl std::fmt::Display for Kind { | |
| 105 | − fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 106 | − match self { | |
| 107 | − Kind::Sha1 => f.write_str("SHA1"), | |
| 108 | − } | |
| 109 | − } | |
| 110 | −} | |
| 111 | − | |
| 112 | −impl Kind { | |
| 113 | − /// Returns the shortest hash we support | |
| 114 | − #[inline] | |
| 115 | − pub const fn shortest() -> Self { | |
| 116 | − Self::Sha1 | |
| 117 | − } | |
| 118 | − | |
| 119 | − /// Returns the longest hash we support | |
| 120 | − #[inline] | |
| 121 | − pub const fn longest() -> Self { | |
| 122 | − Self::Sha1 | |
| 123 | − } | |
| 124 | − | |
| 125 | − /// Returns a buffer suitable to hold the longest possible hash in hex. | |
| 126 | − #[inline] | |
| 127 | − pub const fn hex_buf() -> [u8; Kind::longest().len_in_hex()] { | |
| 128 | − [0u8; Kind::longest().len_in_hex()] | |
| 129 | − } | |
| 130 | − | |
| 131 | − /// Returns a buffer suitable to hold the longest possible hash as raw bytes. | |
| 132 | − #[inline] | |
| 133 | − pub const fn buf() -> [u8; Kind::longest().len_in_bytes()] { | |
| 134 | − [0u8; Kind::longest().len_in_bytes()] | |
| 135 | − } | |
| 136 | − | |
| 137 | − /// Returns the amount of ascii-characters needed to encode this has in hex | |
| 138 | − #[inline] | |
| 139 | − pub const fn len_in_hex(&self) -> usize { | |
| 140 | − match self { | |
| 141 | − Kind::Sha1 => 40, | |
| 142 | − } | |
| 143 | − } | |
| 144 | − /// Returns the amount of bytes taken up by the hash of the current kind | |
| 145 | − #[inline] | |
| 146 | − pub const fn len_in_bytes(&self) -> usize { | |
| 147 | − match self { | |
| 148 | − Kind::Sha1 => 20, | |
| 149 | − } | |
| 150 | − } | |
| 151 | − | |
| 152 | − /// Returns the kind of hash that would fit the given `hex_len`, or `None` if there is no fitting hash. | |
| 153 | − /// Note that 0 as `hex_len` fits always yields Sha1. | |
| 154 | − #[inline] | |
| 155 | − pub const fn from_hex_len(hex_len: usize) -> Option<Self> { | |
| 156 | − Some(match hex_len { | |
| 157 | − 0..=40 => Kind::Sha1, | |
| 158 | − _ => return None, | |
| 159 | − }) | |
| 160 | − } | |
| 161 | − | |
| 162 | − /// Converts a size in bytes as obtained by `Kind::len_in_bytes()` into the corresponding hash kind, if possible. | |
| 163 | − /// | |
| 164 | − /// **Panics** if the hash length doesn't match a known hash. | |
| 165 | − /// | |
| 166 | − /// NOTE that this method isn't public as it shouldn't be encouraged to assume all hashes have the same length. | |
| 167 | − /// However, if there should be such a thing, our `oid` implementation will have to become an enum and it's pretty breaking | |
| 168 | − /// to the way it's currently being used as auto-dereffing doesn't work anymore. Let's hope it won't happen. | |
| 169 | − // TODO: make 'const' once Rust 1.57 is more readily available in projects using 'gitoxide'. | |
| 170 | − #[inline] | |
| 171 | − pub(crate) fn from_len_in_bytes(bytes: usize) -> Self { | |
| 172 | − match bytes { | |
| 173 | − 20 => Kind::Sha1, | |
| 174 | − _ => panic!("BUG: must be called only with valid hash lengths produced by len_in_bytes()"), | |
| 175 | − } | |
| 176 | − } | |
| 177 | − | |
| 178 | − /// Create a null-id of our hash kind. | |
| 179 | − #[inline] | |
| 180 | − pub fn null_ref(&self) -> &'static oid { | |
| 181 | − match self { | |
| 182 | − Kind::Sha1 => oid::null_sha1(), | |
| 183 | − } | |
| 184 | − } | |
| 185 | − | |
| 186 | − /// Create a null-id of our hash kind. | |
| 187 | − #[inline] | |
| 188 | − pub const fn null(&self) -> ObjectId { | |
| 189 | − match self { | |
| 190 | − Kind::Sha1 => ObjectId::null_sha1(), | |
| 191 | − } | |
| 192 | − } | |
| 193 | −} | |
| 42 | +mod kind; | |
| @@ -2,18 +2,6 @@ use std::{borrow::Borrow, convert::TryInto, fmt, ops::Deref}; · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST}; |
| 4 | 4 | |
| 5 | −/// An partial owned hash possibly identifying an object uniquely, | |
| 6 | −/// whose non-prefix bytes are zeroed. | |
| 7 | −#[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy, Debug)] | |
| 8 | −#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 9 | −pub struct Prefix { | |
| 10 | − bytes: ObjectId, | |
| 11 | − hex_len: usize, | |
| 12 | −} | |
| 13 | − | |
| 14 | −/// | |
| 15 | −pub mod prefix; | |
| 16 | − | |
| 17 | 5 | /// An owned hash identifying objects, most commonly Sha1 |
| 18 | 6 | #[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy)] |
| 19 | 7 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] |
| @@ -22,15 +10,49 @@ pub enum ObjectId { · 2 unmodified lines | ||
| 22 | 10 | Sha1([u8; SIZE_OF_SHA1_DIGEST]), |
| 23 | 11 | } |
| 24 | 12 | |
| 25 | −impl std::fmt::Debug for ObjectId { | |
| 26 | − fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 27 | − match self { | |
| 28 | − ObjectId::Sha1(_hash) => f.write_str("Sha1(")?, | |
| 13 | +#[allow(missing_docs)] | |
| 14 | +pub mod decode { | |
| 15 | + use std::str::FromStr; | |
| 16 | + | |
| 17 | + use crate::object_id::ObjectId; | |
| 18 | + | |
| 19 | + /// An error returned by [`ObjectId::from_40_bytes_in_hex()`][crate::ObjectId::from_40_bytes_in_hex()] | |
| 20 | + #[derive(Debug, thiserror::Error)] | |
| 21 | + #[allow(missing_docs)] | |
| 22 | + pub enum Error { | |
| 23 | + #[error("A hash sized {0} hexadecimal characters is invalid")] | |
| 24 | + InvalidHexEncodingLength(usize), | |
| 25 | + #[error("Invalid character {c} at position {index}")] | |
| 26 | + Invalid { c: char, index: usize }, | |
| 27 | + } | |
| 28 | + | |
| 29 | + /// Hash decoding | |
| 30 | + impl ObjectId { | |
| 31 | + /// Create an instance from a `buffer` of 40 bytes encoded with hexadecimal notation. | |
| 32 | + /// | |
| 33 | + /// Such a buffer can be obtained using [`oid::write_hex_to(buffer)`][super::oid::write_hex_to()] | |
| 34 | + pub fn from_hex(buffer: &[u8]) -> Result<ObjectId, Error> { | |
| 35 | + use hex::FromHex; | |
| 36 | + match buffer.len() { | |
| 37 | + 40 => Ok(ObjectId::Sha1(<[u8; 20]>::from_hex(buffer).map_err( | |
| 38 | + |err| match err { | |
| 39 | + hex::FromHexError::InvalidHexCharacter { c, index } => Error::Invalid { c, index }, | |
| 40 | + hex::FromHexError::OddLength | hex::FromHexError::InvalidStringLength => { | |
| 41 | + unreachable!("BUG: This is already checked") | |
| 42 | + } | |
| 43 | + }, | |
| 44 | + )?)), | |
| 45 | + len => Err(Error::InvalidHexEncodingLength(len)), | |
| 46 | + } | |
| 29 | 47 | } |
| 30 | − for b in self.as_bytes() { | |
| 31 | − write!(f, "{:02x}", b)?; | |
| 48 | + } | |
| 49 | + | |
| 50 | + impl FromStr for ObjectId { | |
| 51 | + type Err = Error; | |
| 52 | + | |
| 53 | + fn from_str(s: &str) -> Result<Self, Self::Err> { | |
| 54 | + Self::from_hex(s.as_bytes()) | |
| 32 | 55 | } |
| 33 | − f.write_str(")") | |
| 34 | 56 | } |
| 35 | 57 | } |
| 36 | 58 | |
| @@ -110,6 +132,18 @@ impl ObjectId { · 73 unmodified lines | ||
| 110 | 132 | } |
| 111 | 133 | } |
| 112 | 134 | |
| 135 | +impl std::fmt::Debug for ObjectId { | |
| 136 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 137 | + match self { | |
| 138 | + ObjectId::Sha1(_hash) => f.write_str("Sha1(")?, | |
| 139 | + } | |
| 140 | + for b in self.as_bytes() { | |
| 141 | + write!(f, "{:02x}", b)?; | |
| 142 | + } | |
| 143 | + f.write_str(")") | |
| 144 | + } | |
| 145 | +} | |
| 146 | + | |
| 113 | 147 | impl From<[u8; SIZE_OF_SHA1_DIGEST]> for ObjectId { |
| 114 | 148 | fn from(v: [u8; 20]) -> Self { |
| 115 | 149 | Self::new_sha1(v) |
| @@ -51,15 +51,10 @@ impl fmt::Debug for oid { · 50 unmodified lines | ||
| 51 | 51 | } |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | −use quick_error::quick_error; | |
| 55 | − | |
| 56 | −quick_error! { | |
| 57 | − #[derive(Debug)] | |
| 58 | − pub enum Error { | |
| 59 | − InvalidByteSliceLength(length: usize) { | |
| 60 | − display("Cannot instantiate git hash from a digest of length {}", length) | |
| 61 | − } | |
| 62 | − } | |
| 54 | +#[derive(Debug, thiserror::Error)] | |
| 55 | +pub enum Error { | |
| 56 | + #[error("Cannot instantiate git hash from a digest of length {0}")] | |
| 57 | + InvalidByteSliceLength(usize), | |
| 63 | 58 | } |
| 64 | 59 | |
| 65 | 60 | /// Conversion |
| @@ -1,41 +1,35 @@ | ||
| 1 | 1 | use std::{cmp::Ordering, convert::TryFrom}; |
| 2 | 2 | |
| 3 | −use quick_error::quick_error; | |
| 4 | − | |
| 5 | 3 | use crate::{oid, ObjectId, Prefix}; |
| 6 | 4 | |
| 7 | −quick_error! { | |
| 8 | − /// The error returned by [Prefix::try_from_id()][super::Prefix::try_from_id()]. | |
| 9 | − #[derive(Debug)] | |
| 10 | − #[allow(missing_docs)] | |
| 11 | − pub enum Error { | |
| 12 | − TooShort { hex_len: usize } { | |
| 13 | − display("The minimum hex length of a short object id is {}, got {}", Prefix::MIN_HEX_LEN, hex_len) | |
| 14 | − } | |
| 15 | − TooLong { object_kind: crate::Kind, hex_len: usize } { | |
| 16 | − display("An object of kind {} cannot be larger than {} in hex, but {} was requested", object_kind, object_kind.len_in_hex(), hex_len) | |
| 17 | − } | |
| 18 | − } | |
| 5 | +/// The error returned by [Prefix::try_from_id()]. | |
| 6 | +#[derive(Debug, thiserror::Error)] | |
| 7 | +#[allow(missing_docs)] | |
| 8 | +pub enum Error { | |
| 9 | + #[error( | |
| 10 | + "The minimum hex length of a short object id is {}, got {hex_len}", | |
| 11 | + Prefix::MIN_HEX_LEN | |
| 12 | + )] | |
| 13 | + TooShort { hex_len: usize }, | |
| 14 | + #[error("An object of kind {} cannot be larger than {object_kind} in hex, but {hex_len} was requested")] | |
| 15 | + TooLong { object_kind: crate::Kind, hex_len: usize }, | |
| 19 | 16 | } |
| 20 | 17 | |
| 21 | 18 | /// |
| 22 | 19 | pub mod from_hex { |
| 23 | − use quick_error::quick_error; | |
| 24 | − quick_error! { | |
| 25 | − /// The error returned by [Prefix::from_hex][super::Prefix::from_hex()]. | |
| 26 | − #[derive(Debug, Eq, PartialEq)] | |
| 27 | − #[allow(missing_docs)] | |
| 28 | − pub enum Error { | |
| 29 | − TooShort { hex_len: usize } { | |
| 30 | − display("The minimum hex length of a short object id is {}, got {}", super::Prefix::MIN_HEX_LEN, hex_len) | |
| 31 | − } | |
| 32 | − TooLong { hex_len: usize } { | |
| 33 | − display("An id cannot be larger than {} chars in hex, but {} was requested", crate::Kind::longest().len_in_hex(), hex_len) | |
| 34 | − } | |
| 35 | − Invalid { c: char, index: usize } { | |
| 36 | − display("Invalid character {} at position {}", c, index) | |
| 37 | − } | |
| 38 | − } | |
| 20 | + /// The error returned by [Prefix::from_hex][super::Prefix::from_hex()]. | |
| 21 | + #[derive(Debug, Eq, PartialEq, thiserror::Error)] | |
| 22 | + #[allow(missing_docs)] | |
| 23 | + pub enum Error { | |
| 24 | + #[error( | |
| 25 | + "The minimum hex length of a short object id is {}, got {hex_len}", | |
| 26 | + super::Prefix::MIN_HEX_LEN | |
| 27 | + )] | |
| 28 | + TooShort { hex_len: usize }, | |
| 29 | + #[error("An id cannot be larger than {} chars in hex, but {hex_len} was requested", crate::Kind::longest().len_in_hex())] | |
| 30 | + TooLong { hex_len: usize }, | |
| 31 | + #[error("Invalid character {c} at position {index}")] | |
| 32 | + Invalid { c: char, index: usize }, | |
| 39 | 33 | } |
| 40 | 34 | } |
| 41 | 35 | |
| @@ -0,0 +1,4 @@ | ||
| 1 | +mod kind; | |
| 2 | +mod object_id; | |
| 3 | +mod oid; | |
| 4 | +mod prefix; | |
| @@ -1,2 +0,0 @@ | ||
| 1 | −mod kind; | |
| 2 | −mod oid; | |
| @@ -0,0 +1,43 @@ | ||
| 1 | +mod from_hex { | |
| 2 | + | |
| 3 | + mod valid { | |
| 4 | + use git_hash::ObjectId; | |
| 5 | + | |
| 6 | + #[test] | |
| 7 | + fn twenty_hex_chars_lowercase() { | |
| 8 | + assert!(ObjectId::from_hex(b"1234567890abcdefaaaaaaaaaaaaaaaaaaaaaaaa").is_ok()); | |
| 9 | + } | |
| 10 | + | |
| 11 | + #[test] | |
| 12 | + fn twenty_hex_chars_uppercase() { | |
| 13 | + assert!(ObjectId::from_hex(b"1234567890ABCDEFAAAAAAAAAAAAAAAAAAAAAAAA").is_ok()); | |
| 14 | + } | |
| 15 | + } | |
| 16 | + | |
| 17 | + mod invalid { | |
| 18 | + use git_hash::{decode, ObjectId}; | |
| 19 | + | |
| 20 | + #[test] | |
| 21 | + fn non_hex_characters() { | |
| 22 | + assert!(matches!( | |
| 23 | + ObjectId::from_hex(b"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz").unwrap_err(), | |
| 24 | + decode::Error::Invalid { index: 0, c: 'z' } | |
| 25 | + )); | |
| 26 | + } | |
| 27 | + | |
| 28 | + #[test] | |
| 29 | + fn too_short() { | |
| 30 | + assert!(matches!( | |
| 31 | + ObjectId::from_hex(b"abcd").unwrap_err(), | |
| 32 | + decode::Error::InvalidHexEncodingLength(4) | |
| 33 | + )); | |
| 34 | + } | |
| 35 | + #[test] | |
| 36 | + fn too_long() { | |
| 37 | + assert!(matches!( | |
| 38 | + ObjectId::from_hex(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaf").unwrap_err(), | |
| 39 | + decode::Error::InvalidHexEncodingLength(41) | |
| 40 | + )); | |
| 41 | + } | |
| 42 | + } | |
| 43 | +} | |
| @@ -1,128 +1,4 @@ | ||
| 1 | −mod prefix { | |
| 2 | − mod cmp_oid { | |
| 3 | − use std::cmp::Ordering; | |
| 4 | − | |
| 5 | − use git_testtools::hex_to_id; | |
| 6 | − | |
| 7 | − #[test] | |
| 8 | − fn it_detects_inequality() { | |
| 9 | − let prefix = git_hash::Prefix::new(hex_to_id("b920bbb055e1efb9080592a409d3975738b6efb3"), 7).unwrap(); | |
| 10 | − assert_eq!( | |
| 11 | − prefix.cmp_oid(&hex_to_id("a920bbb055e1efb9080592a409d3975738b6efb3")), | |
| 12 | − Ordering::Greater | |
| 13 | − ); | |
| 14 | − assert_eq!( | |
| 15 | − prefix.cmp_oid(&hex_to_id("b920bbf055e1efb9080592a409d3975738b6efb3")), | |
| 16 | − Ordering::Less | |
| 17 | − ); | |
| 18 | − assert_eq!(prefix.to_string(), "b920bbb"); | |
| 19 | − } | |
| 20 | − | |
| 21 | − #[test] | |
| 22 | − fn it_detects_equality() { | |
| 23 | − let id = hex_to_id("a920bbb055e1efb9080592a409d3975738b6efb3"); | |
| 24 | − let prefix = git_hash::Prefix::new(id, 6).unwrap(); | |
| 25 | − assert_eq!(prefix.cmp_oid(&id), Ordering::Equal); | |
| 26 | − assert_eq!( | |
| 27 | − prefix.cmp_oid(&hex_to_id("a920bbffffffffffffffffffffffffffffffffff")), | |
| 28 | − Ordering::Equal | |
| 29 | − ); | |
| 30 | − assert_eq!(prefix.to_string(), "a920bb"); | |
| 31 | − } | |
| 32 | − } | |
| 33 | − mod new { | |
| 34 | − use std::cmp::Ordering; | |
| 35 | − | |
| 36 | − use git_hash::{Kind, ObjectId}; | |
| 37 | − use git_testtools::hex_to_id; | |
| 38 | − | |
| 39 | − #[test] | |
| 40 | − fn various_valid_inputs() { | |
| 41 | − let oid_hex = "abcdefabcdefabcdefabcdefabcdefabcdefabcd"; | |
| 42 | − let oid = hex_to_id(oid_hex); | |
| 43 | − | |
| 44 | − for hex_len in 4..oid.kind().len_in_hex() { | |
| 45 | − let mut expected = String::from(&oid_hex[..hex_len]); | |
| 46 | − let num_of_zeros = oid.kind().len_in_hex() - hex_len; | |
| 47 | − expected.extend(std::iter::repeat('0').take(num_of_zeros)); | |
| 48 | − let prefix = git_hash::Prefix::new(oid, hex_len).unwrap(); | |
| 49 | − assert_eq!(prefix.as_oid().to_hex().to_string(), expected, "{}", hex_len); | |
| 50 | − assert_eq!(prefix.hex_len(), hex_len); | |
| 51 | − assert_eq!(prefix.cmp_oid(&oid), Ordering::Equal); | |
| 52 | − } | |
| 53 | − } | |
| 54 | − | |
| 55 | − #[test] | |
| 56 | − fn errors_if_hex_len_is_longer_than_oid_len_in_hex() { | |
| 57 | − let kind = Kind::Sha1; | |
| 58 | − assert!(matches!( | |
| 59 | − git_hash::Prefix::new(ObjectId::null(kind), kind.len_in_hex() + 1), | |
| 60 | − Err(git_hash::prefix::Error::TooLong { .. }) | |
| 61 | − )); | |
| 62 | − } | |
| 63 | − | |
| 64 | − #[test] | |
| 65 | − fn errors_if_hex_len_is_too_short() { | |
| 66 | − let kind = Kind::Sha1; | |
| 67 | − assert!(matches!( | |
| 68 | − git_hash::Prefix::new(ObjectId::null(kind), 3), | |
| 69 | − Err(git_hash::prefix::Error::TooShort { .. }) | |
| 70 | − )); | |
| 71 | − } | |
| 72 | − } | |
| 73 | − | |
| 74 | − mod try_from { | |
| 75 | − use std::{cmp::Ordering, convert::TryFrom}; | |
| 76 | − | |
| 77 | − use git_hash::{prefix::from_hex::Error, Prefix}; | |
| 78 | − use git_testtools::hex_to_id; | |
| 79 | − | |
| 80 | − #[test] | |
| 81 | − fn id_8_chars() { | |
| 82 | − let oid_hex = "abcdefabcdefabcdefabcdefabcdefabcdefabcd"; | |
| 83 | − let input = "abcdef"; | |
| 84 | − | |
| 85 | − let expected = hex_to_id(oid_hex); | |
| 86 | − let actual = Prefix::try_from(input).expect("No errors"); | |
| 87 | − assert_eq!(actual.cmp_oid(&expected), Ordering::Equal); | |
| 88 | − } | |
| 89 | − | |
| 90 | − #[test] | |
| 91 | − fn id_9_chars() { | |
| 92 | − let oid_hex = "abcdefabcdefabcdefabcdefabcdefabcdefabcd"; | |
| 93 | − let input = "abcdefa"; | |
| 94 | − | |
| 95 | − let expected = hex_to_id(oid_hex); | |
| 96 | − let actual = Prefix::try_from(input).expect("No errors"); | |
| 97 | − assert_eq!(actual.cmp_oid(&expected), Ordering::Equal); | |
| 98 | − } | |
| 99 | − #[test] | |
| 100 | − fn id_to_short() { | |
| 101 | − let input = "ab"; | |
| 102 | − let expected = Error::TooShort { hex_len: 2 }; | |
| 103 | − let actual = Prefix::try_from(input).unwrap_err(); | |
| 104 | − assert_eq!(actual, expected); | |
| 105 | − } | |
| 106 | − | |
| 107 | − #[test] | |
| 108 | − fn id_to_long() { | |
| 109 | − let input = "abcdefabcdefabcdefabcdefabcdefabcdefabcd123123123123123123"; | |
| 110 | − let expected = Error::TooLong { hex_len: 58 }; | |
| 111 | − let actual = Prefix::try_from(input).unwrap_err(); | |
| 112 | − assert_eq!(actual, expected); | |
| 113 | − } | |
| 114 | − | |
| 115 | − #[test] | |
| 116 | − fn invalid_chars() { | |
| 117 | − let input = "abcdfOsd"; | |
| 118 | − let expected = Error::Invalid { c: 'O', index: 5 }; | |
| 119 | − let actual = Prefix::try_from(input).unwrap_err(); | |
| 120 | − assert_eq!(actual, expected); | |
| 121 | − } | |
| 122 | − } | |
| 123 | −} | |
| 124 | − | |
| 125 | −mod short_hex { | |
| 1 | +mod to_hex_with_len { | |
| 126 | 2 | #[test] |
| 127 | 3 | fn display_entire_range_sha1() { |
| 128 | 4 | let id_hex = "0123456789abcdef123456789abcdef123456789"; |
| @@ -0,0 +1,123 @@ | ||
| 1 | +mod cmp_oid { | |
| 2 | + use std::cmp::Ordering; | |
| 3 | + | |
| 4 | + use git_testtools::hex_to_id; | |
| 5 | + | |
| 6 | + #[test] | |
| 7 | + fn it_detects_inequality() { | |
| 8 | + let prefix = git_hash::Prefix::new(hex_to_id("b920bbb055e1efb9080592a409d3975738b6efb3"), 7).unwrap(); | |
| 9 | + assert_eq!( | |
| 10 | + prefix.cmp_oid(&hex_to_id("a920bbb055e1efb9080592a409d3975738b6efb3")), | |
| 11 | + Ordering::Greater | |
| 12 | + ); | |
| 13 | + assert_eq!( | |
| 14 | + prefix.cmp_oid(&hex_to_id("b920bbf055e1efb9080592a409d3975738b6efb3")), | |
| 15 | + Ordering::Less | |
| 16 | + ); | |
| 17 | + assert_eq!(prefix.to_string(), "b920bbb"); | |
| 18 | + } | |
| 19 | + | |
| 20 | + #[test] | |
| 21 | + fn it_detects_equality() { | |
| 22 | + let id = hex_to_id("a920bbb055e1efb9080592a409d3975738b6efb3"); | |
| 23 | + let prefix = git_hash::Prefix::new(id, 6).unwrap(); | |
| 24 | + assert_eq!(prefix.cmp_oid(&id), Ordering::Equal); | |
| 25 | + assert_eq!( | |
| 26 | + prefix.cmp_oid(&hex_to_id("a920bbffffffffffffffffffffffffffffffffff")), | |
| 27 | + Ordering::Equal | |
| 28 | + ); | |
| 29 | + assert_eq!(prefix.to_string(), "a920bb"); | |
| 30 | + } | |
| 31 | +} | |
| 32 | + | |
| 33 | +mod new { | |
| 34 | + use std::cmp::Ordering; | |
| 35 | + | |
| 36 | + use git_hash::{Kind, ObjectId}; | |
| 37 | + use git_testtools::hex_to_id; | |
| 38 | + | |
| 39 | + #[test] | |
| 40 | + fn various_valid_inputs() { | |
| 41 | + let oid_hex = "abcdefabcdefabcdefabcdefabcdefabcdefabcd"; | |
| 42 | + let oid = hex_to_id(oid_hex); | |
| 43 | + | |
| 44 | + for hex_len in 4..oid.kind().len_in_hex() { | |
| 45 | + let mut expected = String::from(&oid_hex[..hex_len]); | |
| 46 | + let num_of_zeros = oid.kind().len_in_hex() - hex_len; | |
| 47 | + expected.extend(std::iter::repeat('0').take(num_of_zeros)); | |
| 48 | + let prefix = git_hash::Prefix::new(oid, hex_len).unwrap(); | |
| 49 | + assert_eq!(prefix.as_oid().to_hex().to_string(), expected, "{}", hex_len); | |
| 50 | + assert_eq!(prefix.hex_len(), hex_len); | |
| 51 | + assert_eq!(prefix.cmp_oid(&oid), Ordering::Equal); | |
| 52 | + } | |
| 53 | + } | |
| 54 | + | |
| 55 | + #[test] | |
| 56 | + fn errors_if_hex_len_is_longer_than_oid_len_in_hex() { | |
| 57 | + let kind = Kind::Sha1; | |
| 58 | + assert!(matches!( | |
| 59 | + git_hash::Prefix::new(ObjectId::null(kind), kind.len_in_hex() + 1), | |
| 60 | + Err(git_hash::prefix::Error::TooLong { .. }) | |
| 61 | + )); | |
| 62 | + } | |
| 63 | + | |
| 64 | + #[test] | |
| 65 | + fn errors_if_hex_len_is_too_short() { | |
| 66 | + let kind = Kind::Sha1; | |
| 67 | + assert!(matches!( | |
| 68 | + git_hash::Prefix::new(ObjectId::null(kind), 3), | |
| 69 | + Err(git_hash::prefix::Error::TooShort { .. }) | |
| 70 | + )); | |
| 71 | + } | |
| 72 | +} | |
| 73 | + | |
| 74 | +mod try_from { | |
| 75 | + use std::{cmp::Ordering, convert::TryFrom}; | |
| 76 | + | |
| 77 | + use git_hash::prefix::from_hex::Error; | |
| 78 | + use git_hash::Prefix; | |
| 79 | + use git_testtools::hex_to_id; | |
| 80 | + | |
| 81 | + #[test] | |
| 82 | + fn id_8_chars() { | |
| 83 | + let oid_hex = "abcdefabcdefabcdefabcdefabcdefabcdefabcd"; | |
| 84 | + let input = "abcdef"; | |
| 85 | + | |
| 86 | + let expected = hex_to_id(oid_hex); | |
| 87 | + let actual = Prefix::try_from(input).expect("No errors"); | |
| 88 | + assert_eq!(actual.cmp_oid(&expected), Ordering::Equal); | |
| 89 | + } | |
| 90 | + | |
| 91 | + #[test] | |
| 92 | + fn id_9_chars() { | |
| 93 | + let oid_hex = "abcdefabcdefabcdefabcdefabcdefabcdefabcd"; | |
| 94 | + let input = "abcdefa"; | |
| 95 | + | |
| 96 | + let expected = hex_to_id(oid_hex); | |
| 97 | + let actual = Prefix::try_from(input).expect("No errors"); | |
| 98 | + assert_eq!(actual.cmp_oid(&expected), Ordering::Equal); | |
| 99 | + } | |
| 100 | + #[test] | |
| 101 | + fn id_to_short() { | |
| 102 | + let input = "ab"; | |
| 103 | + let expected = Error::TooShort { hex_len: 2 }; | |
| 104 | + let actual = Prefix::try_from(input).unwrap_err(); | |
| 105 | + assert_eq!(actual, expected); | |
| 106 | + } | |
| 107 | + | |
| 108 | + #[test] | |
| 109 | + fn id_to_long() { | |
| 110 | + let input = "abcdefabcdefabcdefabcdefabcdefabcdefabcd123123123123123123"; | |
| 111 | + let expected = Error::TooLong { hex_len: 58 }; | |
| 112 | + let actual = Prefix::try_from(input).unwrap_err(); | |
| 113 | + assert_eq!(actual, expected); | |
| 114 | + } | |
| 115 | + | |
| 116 | + #[test] | |
| 117 | + fn invalid_chars() { | |
| 118 | + let input = "abcdfOsd"; | |
| 119 | + let expected = Error::Invalid { c: 'O', index: 5 }; | |
| 120 | + let actual = Prefix::try_from(input).unwrap_err(); | |
| 121 | + assert_eq!(actual, expected); | |
| 122 | + } | |
| 123 | +} | |
| @@ -31,7 +31,7 @@ internal-testing-to-avoid-being-run-by-cargo-test-all = [] · 30 unmodified lines | ||
| 31 | 31 | |
| 32 | 32 | [dependencies] |
| 33 | 33 | git-features = { version = "^0.22.3", path = "../git-features", features = ["rustsha1", "progress"] } |
| 34 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 34 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 35 | 35 | git-bitmap = { version = "^0.1.2", path = "../git-bitmap" } |
| 36 | 36 | git-object = { version = "^0.20.2", path = "../git-object" } |
| 37 | 37 | |
| @@ -22,7 +22,7 @@ verbose-object-parsing-errors = ["nom/std"] · 21 unmodified lines | ||
| 22 | 22 | |
| 23 | 23 | [dependencies] |
| 24 | 24 | git-features = { version = "^0.22.2", path = "../git-features", features = ["rustsha1"] } |
| 25 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 25 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 26 | 26 | git-validate = { version = "^0.5.5", path = "../git-validate" } |
| 27 | 27 | git-actor = { version = "^0.11.3", path = "../git-actor" } |
| 28 | 28 | |
| @@ -28,8 +28,8 @@ required-features = [] · 27 unmodified lines | ||
| 28 | 28 | |
| 29 | 29 | [dependencies] |
| 30 | 30 | git-features = { version = "^0.22.3", path = "../git-features", features = ["rustsha1", "walkdir", "zlib", "crc32" ] } |
| 31 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 31 | 32 | git-path = { version = "^0.4.2", path = "../git-path" } |
| 32 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 33 | 33 | git-quote = { version = "^0.2.1", path = "../git-quote" } |
| 34 | 34 | git-object = { version = "^0.20.3", path = "../git-object" } |
| 35 | 35 | git-pack = { version = "^0.22.0", path = "../git-pack" } |
| @@ -38,8 +38,8 @@ required-features = ["internal-testing-to-avoid-being-run-by-cargo-test-all"] · 37 unmodified lines | ||
| 38 | 38 | |
| 39 | 39 | [dependencies] |
| 40 | 40 | git-features = { version = "^0.22.3", path = "../git-features", features = ["crc32", "rustsha1", "progress", "zlib"] } |
| 41 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 42 | 41 | git-path = { version = "^0.4.2", path = "../git-path" } |
| 42 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 43 | 43 | git-chunk = { version = "^0.3.1", path = "../git-chunk" } |
| 44 | 44 | git-object = { version = "^0.20.3", path = "../git-object" } |
| 45 | 45 | git-traverse = { version = "^0.16.3", path = "../git-traverse" } |
| @@ -41,7 +41,7 @@ required-features = ["async-client"] · 40 unmodified lines | ||
| 41 | 41 | [dependencies] |
| 42 | 42 | git-features = { version = "^0.22.3", path = "../git-features", features = ["progress"] } |
| 43 | 43 | git-transport = { version = "^0.19.3", path = "../git-transport" } |
| 44 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 44 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 45 | 45 | git-credentials = { version = "^0.4.0", path = "../git-credentials" } |
| 46 | 46 | |
| 47 | 47 | thiserror = "1.0.32" |
| @@ -25,7 +25,7 @@ required-features = ["internal-testing-git-features-parallel"] · 24 unmodified lines | ||
| 25 | 25 | [dependencies] |
| 26 | 26 | git-features = { version = "^0.22.3", path = "../git-features", features = ["walkdir"]} |
| 27 | 27 | git-path = { version = "^0.4.2", path = "../git-path" } |
| 28 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 28 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 29 | 29 | git-object = { version = "^0.20.3", path = "../git-object" } |
| 30 | 30 | git-validate = { version = "^0.5.5", path = "../git-validate" } |
| 31 | 31 | git-actor = { version = "^0.11.3", path = "../git-actor" } |
| @@ -14,7 +14,7 @@ doctest = false · 13 unmodified lines | ||
| 14 | 14 | [dependencies] |
| 15 | 15 | git-revision = { version = "^0.4.4", path = "../git-revision" } |
| 16 | 16 | git-validate = { version = "^0.5.5", path = "../git-validate" } |
| 17 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 17 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 18 | 18 | |
| 19 | 19 | bstr = { version = "0.2.13", default-features = false, features = ["std"]} |
| 20 | 20 | thiserror = "1.0.26" |
| @@ -87,7 +87,7 @@ git-refspec = { version = "^0.1.1", path = "../git-refspec" } · 86 unmodified lines | ||
| 87 | 87 | |
| 88 | 88 | git-config = { version = "^0.7.1", path = "../git-config" } |
| 89 | 89 | git-odb = { version = "^0.32.0", path = "../git-odb" } |
| 90 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 90 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 91 | 91 | git-object = { version = "^0.20.3", path = "../git-object" } |
| 92 | 92 | git-actor = { version = "^0.11.4", path = "../git-actor" } |
| 93 | 93 | git-pack = { version = "^0.22.0", path = "../git-pack", features = ["object-cache-dynamic"] } |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | use std::borrow::Cow; |
| 2 | +use std::time::SystemTime; | |
| 2 | 3 | |
| 3 | 4 | use crate::{bstr::BString, permission}; |
| 4 | 5 | |
| @@ -125,13 +126,13 @@ impl Personas { · 120 unmodified lines | ||
| 125 | 126 | committer_email = committer_email.or_else(|| env_var("GIT_COMMITTER_EMAIL")); |
| 126 | 127 | committer_date = std::env::var("GIT_COMMITTER_DATE") |
| 127 | 128 | .ok() |
| 128 | − .and_then(|date| git_date::parse(&date)); | |
| 129 | + .and_then(|date| git_date::parse(&date, Some(SystemTime::now())).ok()); | |
| 129 | 130 | |
| 130 | 131 | author_name = author_name.or_else(|| env_var("GIT_AUTHOR_NAME")); |
| 131 | 132 | author_email = author_email.or_else(|| env_var("GIT_AUTHOR_EMAIL")); |
| 132 | 133 | author_date = std::env::var("GIT_AUTHOR_DATE") |
| 133 | 134 | .ok() |
| 134 | − .and_then(|date| git_date::parse(&date)); | |
| 135 | + .and_then(|date| git_date::parse(&date, Some(SystemTime::now())).ok()); | |
| 135 | 136 | |
| 136 | 137 | user_email = user_email.or_else(|| env_var("EMAIL")); // NOTE: we don't have permission for this specific one… |
| 137 | 138 | } |
| @@ -16,7 +16,7 @@ doctest = false · 15 unmodified lines | ||
| 16 | 16 | serde1 = [ "serde", "git-hash/serde1", "git-object/serde1" ] |
| 17 | 17 | |
| 18 | 18 | [dependencies] |
| 19 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 19 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 20 | 20 | git-object = { version = "^0.20.2", path = "../git-object" } |
| 21 | 21 | git-date = { version = "^0.1.0", path = "../git-date" } |
| 22 | 22 | |
| @@ -1,3 +1,4 @@ | ||
| 1 | +use std::time::SystemTime; | |
| 1 | 2 | use std::{convert::TryInto, str::FromStr}; |
| 2 | 3 | |
| 3 | 4 | use bstr::{BStr, BString, ByteSlice, ByteVec}; |
| @@ -434,9 +435,16 @@ where · 430 unmodified lines | ||
| 434 | 435 | } else if has_ref_or_implied_name { |
| 435 | 436 | let time = nav |
| 436 | 437 | .to_str() |
| 437 | − .ok() | |
| 438 | − .and_then(git_date::parse) | |
| 439 | − .ok_or_else(|| Error::Time { input: nav.into() })?; | |
| 438 | + .map_err(|_| Error::Time { | |
| 439 | + input: nav.into(), | |
| 440 | + source: None, | |
| 441 | + }) | |
| 442 | + .and_then(|date| { | |
| 443 | + git_date::parse(date, Some(SystemTime::now())).map_err(|err| Error::Time { | |
| 444 | + input: nav.into(), | |
| 445 | + source: err.into(), | |
| 446 | + }) | |
| 447 | + })?; | |
| 440 | 448 | delegate |
| 441 | 449 | .reflog(delegate::ReflogLookup::Date(time)) |
| 442 | 450 | .ok_or(Error::Delegate)?; |
| @@ -17,7 +17,10 @@ pub enum Error { · 16 unmodified lines | ||
| 17 | 17 | #[error("Cannot peel to {:?} - unknown target.", .input)] |
| 18 | 18 | InvalidObject { input: BString }, |
| 19 | 19 | #[error("Could not parse time {:?} for revlog lookup.", .input)] |
| 20 | − Time { input: BString }, | |
| 20 | + Time { | |
| 21 | + input: BString, | |
| 22 | + source: Option<git_date::parse::Error>, | |
| 23 | + }, | |
| 21 | 24 | #[error("Sibling branches like 'upstream' or 'push' require a branch name with remote configuration, got {:?}", .name)] |
| 22 | 25 | SiblingBranchNeedsBranchName { name: BString }, |
| 23 | 26 | #[error("Reflog entries require a ref name, got {:?}", .name)] |
| @@ -43,7 +43,7 @@ fn reflog_by_date_for_current_branch() { · 42 unmodified lines | ||
| 43 | 43 | #[test] |
| 44 | 44 | fn reflog_by_date_with_date_parse_failure() { |
| 45 | 45 | let err = try_parse("@{foo}").unwrap_err(); |
| 46 | − assert!(matches!(err, spec::parse::Error::Time {input} if input == "foo")); | |
| 46 | + assert!(matches!(err, spec::parse::Error::Time {input, source} if input == "foo" && source.is_some())); | |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | 49 | #[test] |
| @@ -12,7 +12,7 @@ include = ["src/**/*"] · 11 unmodified lines | ||
| 12 | 12 | doctest = false |
| 13 | 13 | |
| 14 | 14 | [dependencies] |
| 15 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 15 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 16 | 16 | git-object = { version = "^0.20.3", path = "../git-object" } |
| 17 | 17 | thiserror = "1.0.32" |
| 18 | 18 | hash_hasher = "2.0.3" |
| @@ -30,7 +30,7 @@ internal-testing-to-avoid-being-run-by-cargo-test-all = [] · 29 unmodified lines | ||
| 30 | 30 | |
| 31 | 31 | [dependencies] |
| 32 | 32 | git-index = { version = "^0.4.3", path = "../git-index" } |
| 33 | −git-hash = { version = "^0.9.8", path = "../git-hash" } | |
| 33 | +git-hash = { version = "^0.9.9", path = "../git-hash" } | |
| 34 | 34 | git-object = { version = "^0.20.2", path = "../git-object" } |
| 35 | 35 | git-glob = { version = "^0.3.2", path = "../git-glob" } |
| 36 | 36 | git-path = { version = "^0.4.2", path = "../git-path" } |
| @@ -14,7 +14,7 @@ path = "src/main.rs" · 13 unmodified lines | ||
| 14 | 14 | doctest = false |
| 15 | 15 | |
| 16 | 16 | [dependencies] |
| 17 | −git-hash = { version = "^0.9.8", path = "../../git-hash" } | |
| 17 | +git-hash = { version = "^0.9.9", path = "../../git-hash" } | |
| 18 | 18 | git-lock = { version = "^2.0.0", path = "../../git-lock" } |
| 19 | 19 | git-discover = { version = "^0.4.2", path = "../../git-discover" } |
| 20 | 20 | git-attributes = { version = "^0.3.3", path = "../../git-attributes" } |