use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::git::fmt::Qualified;
use crate::git::fmt::RefStr;
use crate::git::fmt::RefString;
use super::protect::Unprotected;
pub type RawName = RefString;
pub type RawTarget = RefString;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Target {
Direct(Direct),
Symbolic(Symbolic),
}
impl AsRef<RefStr> for Target {
fn as_ref(&self) -> &RefStr {
match self {
Target::Direct(direct) => direct.0.as_ref(),
Target::Symbolic(symbolic) => symbolic.0.as_ref(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Direct(Unprotected<Qualified<'static>>);
impl Direct {
fn to_ref_string(&self) -> Unprotected<RefString> {
self.0.to_ref_string()
}
}
impl PartialEq<RefString> for Direct {
fn eq(&self, other: &RefString) -> bool {
**self.0.as_ref() == **other
}
}
impl AsRef<Qualified<'static>> for Direct {
fn as_ref(&self) -> &Qualified<'static> {
self.0.as_ref()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Symbolic(Unprotected<RefString>);
impl AsRef<RefString> for Symbolic {
fn as_ref(&self) -> &RefString {
self.0.as_ref()
}
}
impl Target {
pub fn as_refstr(&self) -> &RefStr {
match self {
Target::Direct(q) => q.as_ref().as_ref(),
Target::Symbolic(s) => s.as_ref().as_ref(),
}
}
fn direct(d: Unprotected<Qualified<'static>>) -> Self {
Self::Direct(Direct(d))
}
fn symbolic(s: Unprotected<RefString>) -> Self {
Self::Symbolic(Symbolic(s))
}
fn classify(s: Unprotected<RefString>) -> Self {
match Qualified::from_refstr(s.as_ref()) {
Some(q) => Target::direct(
Unprotected::new(q.to_owned())
.expect("qualified derived from unprotected is unprotected"),
),
None => Target::symbolic(s),
}
}
}
impl Serialize for Target {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_refstr().as_str())
}
}
impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_refstr().as_str())
}
}
pub(super) type Name = Unprotected<RefString>;
impl std::cmp::Ord for Name {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_ref().cmp(other.as_ref())
}
}
impl std::cmp::PartialOrd for Name {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(try_from = "IndexMap<Name, Unprotected<RefString>>")]
pub struct SymbolicRefs(IndexMap<Name, Target>);
impl SymbolicRefs {
pub fn iter(&self) -> impl Iterator<Item = (&RawName, &Target)> {
self.0.iter().map(|(name, target)| (name.as_ref(), target))
}
pub fn iter_resolved(&self) -> impl Iterator<Item = (&RawName, &Qualified<'static>)> {
self.0.keys().filter_map(|name| {
self.resolve(name.as_ref())
.map(|target| (name.as_ref(), target))
})
}
fn resolve(&self, name: &RefString) -> Option<&Qualified<'static>> {
let mut current = self.0.get(name)?;
loop {
match current {
Target::Direct(q) => return Some(q.as_ref()),
Target::Symbolic(s) => match self.0.get(s.as_ref()) {
Some(next) => current = next,
None => return None,
},
}
}
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl SymbolicRefs {
pub fn head(branch_name: &RefString) -> Self {
let mut result = Self::default();
result
.try_insert_unprotected(
Unprotected::head().to_owned(),
Unprotected::branch(branch_name).to_ref_string(),
)
.expect("not creating cycle");
result
}
pub fn resolve_head(&self) -> Option<&Qualified<'static>> {
self.resolve(Unprotected::head().as_ref())
}
}
#[derive(Debug, Error)]
pub enum InsertionError {
#[error("inserting symbolic reference '{name} → {target}' would create a cycle")]
Cyclic { name: RawName, target: RawName },
#[error(
"inserting symbolic reference '{name} → {target}' would result in a symbolic reference targeting an unqualified reference"
)]
TargetNotQualified { name: RawName, target: RawName },
}
impl SymbolicRefs {
pub(super) fn try_insert_unprotected(
&mut self,
name: Name,
target: Unprotected<RefString>,
) -> Result<(), InsertionError> {
self.insert(name, Target::classify(target))
}
fn is_reachable_from(&self, start: &RefString, name: &Name) -> bool {
let name = name.as_ref();
if start == name {
return true;
}
let mut current: &RefStr = start;
loop {
match self.0.get(current) {
None => return false,
Some(target) => {
let next = target.as_refstr();
if *next == **name {
return true;
}
current = next;
}
}
}
}
#[cfg(test)]
fn try_insert(&mut self, name: RawName, target: RawTarget) -> Result<(), InsertionError> {
self.try_insert_unprotected(
Unprotected::new(name).expect("name is unprotected"),
Unprotected::new(target).expect("target is unprotected"),
)
}
pub fn combine(&mut self, other: SymbolicRefs) -> Result<(), InsertionError> {
for (name, target) in other.0 {
self.insert(name, target)?;
}
Ok(())
}
fn insert(&mut self, name: Name, mut target: Target) -> Result<(), InsertionError> {
let target_str = match &target {
Target::Direct(q) => q.as_ref().to_ref_string(),
Target::Symbolic(s) => s.as_ref().clone(),
};
if self.is_reachable_from(&target_str, &name) {
return Err(InsertionError::Cyclic {
name: name.into_inner(),
target: target_str,
});
}
if let Target::Direct(q) = &target {
if self.0.contains_key(&q.as_ref().to_ref_string()) {
target = Target::symbolic(q.to_ref_string());
}
}
if let Target::Symbolic(s) = &target {
if self.resolve(s.as_ref()).is_none() {
return Err(InsertionError::TargetNotQualified {
name: name.into_inner(),
target: target_str,
});
}
}
self.reclassify_targets(&name);
self.0.insert(name, target);
Ok(())
}
fn reclassify_targets(&mut self, new_key: &Name) {
let key = new_key.as_ref();
for existing in self.0.values_mut() {
if let Target::Direct(q) = existing {
if q == key {
*existing = Target::symbolic(q.to_ref_string());
}
}
}
}
}
impl TryFrom<IndexMap<Name, Unprotected<RefString>>> for SymbolicRefs {
type Error = InsertionError;
fn try_from(map: IndexMap<Name, Unprotected<RefString>>) -> Result<Self, Self::Error> {
map.into_iter()
.try_fold(Self::default(), |mut result, (name, target)| {
result.try_insert_unprotected(name, target)?;
Ok(result)
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use crate::assert_matches;
use crate::git::fmt::refname;
use super::*;
#[test]
fn infinite_single() {
let mut symbolic = SymbolicRefs::default();
assert_matches!(
symbolic.try_insert(refname!("a"), refname!("a")),
Err(InsertionError::Cyclic { .. })
);
assert!(symbolic.is_empty());
}
#[test]
fn infinite_multi() {
let mut symbolic = SymbolicRefs::default();
assert_matches!(
symbolic.try_insert(refname!("a"), refname!("refs/heads/b")),
Ok(())
);
assert_matches!(
symbolic.try_insert(refname!("refs/heads/b"), refname!("refs/heads/c")),
Ok(())
);
assert_matches!(
symbolic.try_insert(refname!("refs/heads/c"), refname!("a")),
Err(InsertionError::Cyclic { .. })
);
}
#[test]
fn deserialize_valid() {
assert_matches!(
serde_json::from_value::<SymbolicRefs>(serde_json::json!({
"refs/heads/a": "refs/heads/b",
})),
Ok(_)
);
}
#[test]
fn deserialize_order() {
assert_matches!(
serde_json::from_value::<SymbolicRefs>(serde_json::json!({
"MAIN": "refs/heads/master",
"HEAD": "MAIN",
})),
Ok(_)
);
assert_matches!(
serde_json::from_value::<SymbolicRefs>(serde_json::json!({
"HEAD": "MAIN",
"MAIN": "refs/heads/master",
})),
Err(_)
);
}
#[test]
fn deserialize_infinite() {
assert_matches!(
serde_json::from_value::<SymbolicRefs>(serde_json::json!({
"refs/heads/a": "refs/heads/a",
})),
Err(_)
);
assert_matches!(
serde_json::from_value::<SymbolicRefs>(serde_json::json!({
"refs/heads/a": "refs/heads/b",
"refs/heads/b": "refs/heads/c",
"refs/heads/c": "refs/heads/a",
})),
Err(_)
);
assert_matches!(
serde_json::from_value::<SymbolicRefs>(serde_json::json!({
"HEAD": "b",
})),
Err(_)
);
}
#[test]
fn resolve_two_hop_chain() {
let symrefs = serde_json::from_value::<SymbolicRefs>(serde_json::json!({
"MAIN": "refs/heads/master",
"HEAD": "MAIN",
}))
.unwrap();
assert_eq!(
symrefs.resolve_head().map(|r| r.as_str()),
Some("refs/heads/master"),
);
}
#[test]
fn infinite_extend() {
let mut a = SymbolicRefs::default();
assert_matches!(
a.try_insert(refname!("refs/heads/a"), refname!("refs/heads/b")),
Ok(())
);
let mut b = SymbolicRefs::default();
assert_matches!(
b.try_insert(refname!("refs/heads/b"), refname!("refs/heads/a")),
Ok(())
);
assert_matches!(a.combine(b), Err(InsertionError::Cyclic { .. }));
}
#[test]
fn target_classification() {
let symrefs = serde_json::from_value::<SymbolicRefs>(serde_json::json!({
"HEAD": "refs/heads/main",
}))
.unwrap();
let (_, target) = symrefs.iter().next().unwrap();
assert_matches!(target, Target::Direct(_));
}
#[test]
fn target_classification_symbolic() {
let symrefs = serde_json::from_value::<SymbolicRefs>(serde_json::json!({
"MAIN": "refs/heads/master",
"HEAD": "MAIN",
}))
.unwrap();
let head_entry = symrefs
.iter()
.find_map(|(name, target)| (name.as_str() == "HEAD").then_some(target))
.unwrap();
assert_matches!(head_entry, Target::Symbolic(_));
let main_entry = symrefs
.iter()
.find_map(|(name, target)| (name.as_str() == "MAIN").then_some(target))
.unwrap();
assert_matches!(main_entry, Target::Direct(_));
}
#[test]
fn target_reclassification() {
let mut symrefs = SymbolicRefs::default();
symrefs
.try_insert(refname!("HEAD"), refname!("refs/heads/main"))
.unwrap();
symrefs
.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
.unwrap();
let main = symrefs
.iter()
.find_map(|(_, target)| {
(target.as_refstr().as_str() == "refs/heads/main").then_some(target)
})
.unwrap();
assert_matches!(main, Target::Symbolic(_));
}
#[test]
fn target_reclassification_commutative() {
let mut symrefs = SymbolicRefs::default();
symrefs
.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
.unwrap();
symrefs
.try_insert(refname!("HEAD"), refname!("refs/heads/main"))
.unwrap();
let main = symrefs
.iter()
.find_map(|(_, target)| {
(target.as_refstr().as_str() == "refs/heads/main").then_some(target)
})
.unwrap();
assert_matches!(main, Target::Symbolic(_));
}
#[test]
fn reclassification_reverse_chain() {
let mut symrefs = SymbolicRefs::default();
for (name, target) in [
(refname!("refs/heads/c"), refname!("refs/heads/d")),
(refname!("refs/heads/b"), refname!("refs/heads/c")),
(refname!("refs/heads/a"), refname!("refs/heads/b")),
] {
symrefs.try_insert(name, target).unwrap();
}
for (_, target) in symrefs.iter() {
match target.as_refstr().as_str() {
"refs/heads/d" => assert_matches!(target, Target::Direct(_)),
other => {
assert_matches!(target, Target::Symbolic(_), "expected Symbolic for {other}")
}
}
}
assert_eq!(
symrefs
.resolve(&refname!("refs/heads/a"))
.map(|q| q.as_str()),
Some("refs/heads/d"),
);
}
#[test]
fn reclassification_diamond() {
let mut symrefs = SymbolicRefs::default();
symrefs
.try_insert(refname!("HEAD"), refname!("refs/heads/main"))
.unwrap();
symrefs
.try_insert(refname!("DEFAULT"), refname!("refs/heads/main"))
.unwrap();
symrefs
.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
.unwrap();
let targets_for_main: Vec<_> = symrefs
.iter()
.filter(|(_, t)| t.as_refstr().as_str() == "refs/heads/main")
.collect();
assert_eq!(targets_for_main.len(), 2);
for (name, target) in targets_for_main {
assert_matches!(
target,
Target::Symbolic(_),
"expected Symbolic for {name}'s target"
);
}
}
#[test]
fn reclassification_order_invariant() {
let a = {
let mut s = SymbolicRefs::default();
s.try_insert(refname!("HEAD"), refname!("refs/heads/main"))
.unwrap();
s.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
.unwrap();
s
};
let b = {
let mut s = SymbolicRefs::default();
s.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
.unwrap();
s.try_insert(refname!("HEAD"), refname!("refs/heads/main"))
.unwrap();
s
};
assert_eq!(a.resolve_head(), b.resolve_head());
let classify_a = a
.iter()
.find(|(_, t)| t.as_refstr().as_str() == "refs/heads/main")
.unwrap()
.1;
let classify_b = b
.iter()
.find(|(_, t)| t.as_refstr().as_str() == "refs/heads/main")
.unwrap()
.1;
assert_matches!(classify_a, Target::Symbolic(_));
assert_matches!(classify_b, Target::Symbolic(_));
}
#[test]
fn reclassification_combine() {
let mut a = SymbolicRefs::head(&refname!("main"));
let mut b = SymbolicRefs::default();
b.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
.unwrap();
a.combine(b).unwrap();
let main_target = a
.iter()
.find(|(_, t)| t.as_refstr().as_str() == "refs/heads/main")
.unwrap()
.1;
assert_matches!(main_target, Target::Symbolic(_));
assert_eq!(
a.resolve_head().map(|q| q.as_str()),
Some("refs/heads/master")
);
}
#[test]
fn reclassification_combine_reverse() {
let mut b = SymbolicRefs::default();
b.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
.unwrap();
let a = SymbolicRefs::head(&refname!("main"));
b.combine(a).unwrap();
let main_target = b
.iter()
.find_map(|(_, t)| (t.as_refstr().as_str() == "refs/heads/main").then_some(t))
.unwrap();
assert_matches!(main_target, Target::Symbolic(_));
assert_eq!(
b.resolve_head().map(|q| q.as_str()),
Some("refs/heads/master")
);
}
}