use std::str::FromStr;
use crate::error::{Error, Result};
use crate::refs::store::RefName;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReflogSelector {
pub ref_name: RefName,
pub index: usize,
}
impl FromStr for ReflogSelector {
type Err = Error;
fn from_str(spec: &str) -> Result<Self> {
let spec = spec.trim();
let invalid_selector = || Error::ReflogInvalidSelector {
spec: spec.to_owned(),
};
let (ref_part, rest) = spec.split_once("@{").ok_or_else(invalid_selector)?;
let ref_part = ref_part.trim();
let rest = rest.strip_suffix('}').ok_or_else(invalid_selector)?;
let index = rest.parse().map_err(|source| Error::ReflogInvalidIndex {
spec: spec.to_owned(),
source,
})?;
if ref_part.is_empty() {
return Err(invalid_selector());
}
Ok(Self {
ref_name: parse_reflog_ref(ref_part)?,
index,
})
}
}
pub fn parse_reflog_ref(spec: &str) -> Result<RefName> {
if let Ok(name) = RefName::try_from(spec) {
return Ok(name);
}
if spec != "HEAD" && !spec.starts_with("refs/") {
if let Ok(name) = RefName::try_from(&format!("refs/heads/{spec}")) {
return Ok(name);
}
}
Err(Error::ReflogUnknownRef {
refname: spec.to_owned(),
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn reference_validation_preserves_existing_shorthand_rules() {
for (input, expected) in [
("HEAD", "HEAD"),
("main", "refs/heads/main"),
("refs/heads/main", "refs/heads/main"),
("refs/remotes/origin/main", "refs/remotes/origin/main"),
("refs/stash", "refs/stash"),
("topic/nested", "topic/nested"),
("révision", "refs/heads/révision"),
] {
assert_eq!(parse_reflog_ref(input).unwrap().as_str(), expected);
}
for spec in ["", " main ", "refs/bad..name", "bad name", "a.lock", "@{"] {
assert!(
matches!(parse_reflog_ref(spec), Err(Error::ReflogUnknownRef { refname }) if refname == spec)
);
}
}
#[test]
fn selectors_parse_references_and_newest_first_indices() {
for (input, name, index) in [
("HEAD@{0}", "HEAD", 0),
("main@{12}", "refs/heads/main", 12),
(" main @{002} ", "refs/heads/main", 2),
("refs/stash@{+1}", "refs/stash", 1),
("topic/nested@{3}", "topic/nested", 3),
] {
let parsed: ReflogSelector = input.parse().unwrap();
assert_eq!(parsed.ref_name.as_str(), name);
assert_eq!(parsed.index, index);
}
let max: ReflogSelector = format!("HEAD@{{{}}}", usize::MAX).parse().unwrap();
assert_eq!(max.index, usize::MAX);
}
#[test]
fn selectors_return_distinct_typed_failures() {
for input in ["", "HEAD", "HEAD@0", "HEAD@{0", "HEAD@{0}tail", " @{0} "] {
assert!(
matches!(input.parse::<ReflogSelector>(), Err(Error::ReflogInvalidSelector { spec }) if spec == input.trim())
);
}
for input in [
"HEAD@{}",
"HEAD@{-1}",
"HEAD@{1.0}",
"HEAD@{ 1}",
"HEAD@{0}}",
"HEAD@{1}@{2}",
] {
assert!(
matches!(input.parse::<ReflogSelector>(), Err(Error::ReflogInvalidIndex { spec, .. }) if spec == input)
);
}
let overflow = format!("HEAD@{{{}0}}", usize::MAX);
let err = overflow.parse::<ReflogSelector>().unwrap_err();
assert!(
matches!(&err, Error::ReflogInvalidIndex { source, .. } if *source.kind() == std::num::IntErrorKind::PosOverflow)
);
assert!(std::error::Error::source(&err).is_some());
assert!(
matches!("bad name@{0}".parse::<ReflogSelector>(), Err(Error::ReflogUnknownRef { refname }) if refname == "bad name")
);
assert!(matches!(
"@{bad}".parse::<ReflogSelector>(),
Err(Error::ReflogInvalidIndex { .. })
));
}
}