use std::{
collections::BTreeMap,
path::{Path, PathBuf},
str::FromStr,
};
use regex::Regex;
use serde::{Deserialize, Serialize};
use git_ref_format_core::{Namespaced, Qualified, RefString};
use radicle::{
Profile,
cob::patch::PatchId,
crypto::PublicKey,
git::{BranchName, Oid},
node::{Event, NodeId},
prelude::RepoId,
storage::{
ReadStorage, RefUpdate, RepositoryError,
refs::{Refs, RefsAt},
},
};
use crate::{
logger,
msg::RunId,
refs::{GenericRefName, TagName, ref_string},
};
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CiEvent {
V1(CiEventV1),
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CiEventV1 {
Shutdown,
Terminate(RunId),
BranchCreated {
from_node: NodeId,
repo: RepoId,
branch: BranchName,
tip: Oid,
},
BranchUpdated {
from_node: NodeId,
repo: RepoId,
branch: BranchName,
tip: Oid,
old_tip: Oid,
},
BranchDeleted {
from_node: NodeId,
repo: RepoId,
branch: BranchName,
tip: Oid,
},
TagCreated {
from_node: NodeId,
repo: RepoId,
tag: TagName,
tip: Oid,
},
TagUpdated {
from_node: NodeId,
repo: RepoId,
tag: TagName,
tip: Oid,
old_tip: Oid,
},
TagDeleted {
from_node: NodeId,
repo: RepoId,
tag: TagName,
tip: Oid,
},
PatchCreated {
from_node: NodeId,
repo: RepoId,
patch: PatchId,
new_tip: Oid,
},
PatchUpdated {
from_node: NodeId,
repo: RepoId,
patch: PatchId,
new_tip: Oid,
},
CanonicalRefUpdated {
from_node: NodeId,
repo: RepoId,
refname: GenericRefName,
target: Oid,
},
}
impl CiEvent {
pub fn from_node(&self) -> Option<&NodeId> {
match self {
Self::V1(CiEventV1::Shutdown) => None,
Self::V1(CiEventV1::Terminate(_)) => None,
Self::V1(CiEventV1::BranchCreated { from_node, .. }) => Some(from_node),
Self::V1(CiEventV1::BranchUpdated { from_node, .. }) => Some(from_node),
Self::V1(CiEventV1::BranchDeleted { from_node, .. }) => Some(from_node),
Self::V1(CiEventV1::TagCreated { from_node, .. }) => Some(from_node),
Self::V1(CiEventV1::TagUpdated { from_node, .. }) => Some(from_node),
Self::V1(CiEventV1::TagDeleted { from_node, .. }) => Some(from_node),
Self::V1(CiEventV1::PatchCreated { from_node, .. }) => Some(from_node),
Self::V1(CiEventV1::PatchUpdated { from_node, .. }) => Some(from_node),
Self::V1(CiEventV1::CanonicalRefUpdated { from_node, .. }) => Some(from_node),
}
}
pub fn repository(&self) -> Option<&RepoId> {
match self {
Self::V1(CiEventV1::Shutdown) => None,
Self::V1(CiEventV1::Terminate(_)) => None,
Self::V1(CiEventV1::BranchCreated { repo, .. }) => Some(repo),
Self::V1(CiEventV1::BranchUpdated { repo, .. }) => Some(repo),
Self::V1(CiEventV1::BranchDeleted { repo, .. }) => Some(repo),
Self::V1(CiEventV1::TagCreated { repo, .. }) => Some(repo),
Self::V1(CiEventV1::TagUpdated { repo, .. }) => Some(repo),
Self::V1(CiEventV1::TagDeleted { repo, .. }) => Some(repo),
Self::V1(CiEventV1::PatchCreated { repo, .. }) => Some(repo),
Self::V1(CiEventV1::PatchUpdated { repo, .. }) => Some(repo),
Self::V1(CiEventV1::CanonicalRefUpdated { repo, .. }) => Some(repo),
}
}
pub fn branch(&self) -> Option<&BranchName> {
match self {
Self::V1(CiEventV1::Shutdown) => None,
Self::V1(CiEventV1::BranchCreated { branch, .. }) => Some(branch),
Self::V1(CiEventV1::BranchUpdated { branch, .. }) => Some(branch),
Self::V1(CiEventV1::BranchDeleted { branch, .. }) => Some(branch),
_ => None,
}
}
pub fn tag(&self) -> Option<&TagName> {
match self {
Self::V1(CiEventV1::Shutdown) => None,
Self::V1(CiEventV1::TagCreated { tag, .. }) => Some(tag),
Self::V1(CiEventV1::TagUpdated { tag, .. }) => Some(tag),
Self::V1(CiEventV1::TagDeleted { tag, .. }) => Some(tag),
_ => None,
}
}
pub fn patch_id(&self) -> Option<&PatchId> {
match self {
Self::V1(CiEventV1::PatchCreated { patch, .. }) => Some(patch),
Self::V1(CiEventV1::PatchUpdated { patch, .. }) => Some(patch),
_ => None,
}
}
pub fn tip(&self) -> Option<&Oid> {
match self {
Self::V1(CiEventV1::Shutdown) => None,
Self::V1(CiEventV1::Terminate(_)) => None,
Self::V1(CiEventV1::BranchCreated { tip, .. }) => Some(tip),
Self::V1(CiEventV1::BranchUpdated { tip, .. }) => Some(tip),
Self::V1(CiEventV1::BranchDeleted { tip, .. }) => Some(tip),
Self::V1(CiEventV1::TagCreated { tip, .. }) => Some(tip),
Self::V1(CiEventV1::TagUpdated { tip, .. }) => Some(tip),
Self::V1(CiEventV1::TagDeleted { tip, .. }) => Some(tip),
Self::V1(CiEventV1::PatchCreated { new_tip, .. }) => Some(new_tip),
Self::V1(CiEventV1::PatchUpdated { new_tip, .. }) => Some(new_tip),
Self::V1(CiEventV1::CanonicalRefUpdated { target, .. }) => Some(target),
}
}
pub fn branch_created(
from_node: NodeId,
repo: RepoId,
branch: &BranchName,
tip: Oid,
) -> Result<Self, CiEventError> {
assert!(!branch.starts_with("refs/"));
Ok(Self::V1(CiEventV1::BranchCreated {
from_node,
repo,
branch: branch.clone(),
tip,
}))
}
pub fn branch_updated(
from_node: NodeId,
repo: RepoId,
branch: &BranchName,
tip: Oid,
old_tip: Oid,
) -> Result<Self, CiEventError> {
assert!(!branch.starts_with("refs/"));
Ok(Self::V1(CiEventV1::BranchUpdated {
from_node,
repo,
branch: branch.clone(),
tip,
old_tip,
}))
}
pub fn branch_deleted(
from_node: NodeId,
repo: RepoId,
branch: &BranchName,
tip: Oid,
) -> Result<Self, CiEventError> {
assert!(!branch.starts_with("refs/"));
Ok(Self::V1(CiEventV1::BranchDeleted {
from_node,
repo,
branch: branch.clone(),
tip,
}))
}
pub fn tag_created(
from_node: NodeId,
repo: RepoId,
tag: &TagName,
tip: Oid,
) -> Result<Self, CiEventError> {
assert!(!tag.starts_with("refs/"));
Ok(Self::V1(CiEventV1::TagCreated {
from_node,
repo,
tag: tag.clone(),
tip,
}))
}
pub fn tag_updated(
from_node: NodeId,
repo: RepoId,
tag: &TagName,
tip: Oid,
old_tip: Oid,
) -> Result<Self, CiEventError> {
assert!(!tag.starts_with("refs/"));
Ok(Self::V1(CiEventV1::TagUpdated {
from_node,
repo,
tag: tag.clone(),
tip,
old_tip,
}))
}
pub fn tag_deleted(
from_node: NodeId,
repo: RepoId,
tag: &TagName,
tip: Oid,
) -> Result<Self, CiEventError> {
assert!(!tag.starts_with("refs/"));
Ok(Self::V1(CiEventV1::TagDeleted {
from_node,
repo,
tag: tag.clone(),
tip,
}))
}
pub fn patch_created(from_node: NodeId, repo: RepoId, patch: PatchId, tip: Oid) -> Self {
Self::V1(CiEventV1::PatchCreated {
from_node,
repo,
patch,
new_tip: tip,
})
}
pub fn patch_updated(from_node: NodeId, repo: RepoId, patch: PatchId, new_tip: Oid) -> Self {
Self::V1(CiEventV1::PatchUpdated {
from_node,
repo,
patch,
new_tip,
})
}
#[allow(clippy::unwrap_used)]
pub fn from_node_event(event: &Event, profile: &Profile) -> Result<Vec<Self>, CiEventError> {
let (rid, updates) = match event {
Event::RefsFetched {
remote: _,
rid,
updated,
} => (*rid, updated),
Event::LocalRefsAnnounced {
rid,
refs,
timestamp: _,
} => {
let repo = profile
.storage
.repository(*rid)
.map_err(CiEventError::Repository)?;
let parent = repo
.backend
.find_commit(refs.at.into())
.map_err(CiEventError::Git)?
.parent(0)
.map_err(CiEventError::Git)?
.id();
let parent_refs = RefsAt {
remote: refs.remote,
at: parent.into(),
};
let signed_refs_new = refs
.load(&repo)
.map_err(|err| CiEventError::StorageRefs(err.into()))?
.sigrefs;
let signed_refs_old = parent_refs
.load(&repo)
.map_err(|err| CiEventError::StorageRefs(err.into()))?
.sigrefs;
let updates = diff_refs(signed_refs_old.refs(), signed_refs_new.refs())
.into_iter()
.filter_map(|mut ref_update| {
let name = ref_update_name_mut(&mut ref_update);
*name = Qualified::from_refstr(&*name)?
.with_namespace(profile.public_key.to_component())
.into_qualified()
.into_refstring();
Some(ref_update)
});
(*rid, &updates.collect())
}
_ => {
logger::node_event_not_handled(event);
return Ok(vec![]);
}
};
let mut events = vec![];
for update in updates {
let e = match update {
RefUpdate::Created { name, oid } => {
let origin = originator(name.to_namespaced().unwrap())?;
match ParsedRef::parse_ref(name) {
Some(ParsedRef::Branch(branch)) => {
Self::branch_created(origin, rid, &branch, *oid)?
}
Some(ParsedRef::Patch(patch_id)) => {
Self::patch_created(origin, rid, patch_id, *oid)
}
Some(ParsedRef::Tag(tag_name)) => {
Self::tag_created(origin, rid, &tag_name, *oid)?
}
None => continue,
}
}
RefUpdate::Updated { name, old, new } => {
let origin = originator(name.to_namespaced().unwrap())?;
match ParsedRef::parse_ref(name) {
Some(ParsedRef::Branch(branch)) => {
Self::branch_updated(origin, rid, &branch, *new, *old)?
}
Some(ParsedRef::Patch(patch_id)) => {
Self::patch_updated(origin, rid, patch_id, *new)
}
Some(ParsedRef::Tag(tag_name)) => {
Self::tag_updated(origin, rid, &tag_name, *new, *old)?
}
None => continue,
}
}
RefUpdate::Deleted { name, oid } => {
let origin = originator(name.to_namespaced().unwrap())?;
match ParsedRef::parse_ref(name) {
Some(ParsedRef::Branch(branch)) => {
Self::branch_deleted(origin, rid, &branch, *oid)?
}
Some(ParsedRef::Patch(_patch_id)) => continue,
Some(ParsedRef::Tag(tag_name)) => {
Self::tag_deleted(origin, rid, &tag_name, *oid)?
}
None => continue,
}
}
RefUpdate::Skipped { .. } => continue,
};
events.push(e);
}
Ok(events)
}
pub fn to_pretty_json(&self) -> Result<String, CiEventError> {
serde_json::to_string_pretty(self).map_err(CiEventError::to_json)
}
}
fn originator(name: Namespaced) -> Result<PublicKey, CiEventError> {
PublicKey::from_namespaced(&name).map_err(|err| CiEventError::key_from_namespaced(&name, err))
}
fn diff_refs(old_refs: &Refs, new_refs: &Refs) -> Vec<RefUpdate> {
let mut updates = Vec::new();
let mut old_refs = old_refs
.iter()
.map(|(old_ref, &old_oid)| (old_ref, old_oid))
.collect::<BTreeMap<_, _>>();
let mut new_refs = new_refs
.iter()
.map(|(new_ref, &new_oid)| (new_ref, new_oid))
.collect::<BTreeMap<_, _>>();
old_refs.retain(|old_ref, old_oid| match new_refs.remove_entry(old_ref) {
Some((new_ref, new_oid)) => {
if new_oid != *old_oid {
updates.push(RefUpdate::Updated {
name: new_ref.clone(),
old: *old_oid,
new: new_oid,
})
}
false
}
None => true,
});
updates.extend(
old_refs
.into_iter()
.map(|(old_ref, oid)| RefUpdate::Deleted {
name: old_ref.clone(),
oid,
}),
);
updates.extend(
new_refs
.into_iter()
.map(|(new_ref, oid)| RefUpdate::Created {
name: new_ref.clone(),
oid,
}),
);
updates
}
fn ref_update_name_mut(ref_update: &mut RefUpdate) -> &mut RefString {
match ref_update {
RefUpdate::Updated { name, .. } => name,
RefUpdate::Created { name, .. } => name,
RefUpdate::Deleted { name, .. } => name,
RefUpdate::Skipped { name, .. } => name,
}
}
pub struct CiEvents {
events: Vec<CiEvent>,
}
impl CiEvents {
pub fn from_file(filename: &Path) -> Result<Self, CiEventError> {
let events = std::fs::read(filename).map_err(|e| CiEventError::read_file(filename, e))?;
let events = String::from_utf8(events).map_err(|e| CiEventError::not_utf8(filename, e))?;
let events: Result<Vec<CiEvent>, _> = events.lines().map(serde_json::from_str).collect();
let events = events.map_err(|e| CiEventError::not_json(filename, e))?;
Ok(Self { events })
}
pub fn iter(&self) -> impl Iterator<Item = &CiEvent> {
self.events.iter()
}
}
#[derive(Debug, thiserror::Error)]
pub enum CiEventError {
#[error("updated ref name has no name space: {0:?})")]
WithoutNamespace2(String),
#[error("failed to create a branch name from {0:?}")]
BranchName(String, crate::refs::RefError),
#[error("failed to read broker events file {0}")]
ReadFile(PathBuf, #[source] std::io::Error),
#[error("broker events file is not UTF8: {0}")]
NotUtf8(PathBuf, #[source] std::string::FromUtf8Error),
#[error("broker events file is not valid JSON: {0}")]
NotJson(
PathBuf,
#[source] Box<dyn std::error::Error + Send + 'static>,
),
#[error("failed to convert name spaced Git ref into node public key: {0}")]
KeyFromNamespaced(
RefString,
#[source] Box<dyn std::error::Error + Send + 'static>,
),
#[error("failed to encode CI event as JSON")]
ToJson(#[source] Box<dyn std::error::Error + Send + 'static>),
#[error(transparent)]
Repository(RepositoryError),
#[error(transparent)]
StorageRefs(radicle::storage::refs::Error),
#[error(transparent)]
Git(radicle::git::raw::Error),
}
impl CiEventError {
fn read_file(filename: &Path, err: std::io::Error) -> Self {
Self::ReadFile(filename.into(), err)
}
fn not_utf8(filename: &Path, err: std::string::FromUtf8Error) -> Self {
Self::NotUtf8(filename.into(), err)
}
fn not_json(filename: &Path, err: serde_json::Error) -> Self {
Self::NotJson(filename.into(), Box::new(err))
}
fn to_json(err: serde_json::Error) -> Self {
Self::ToJson(Box::new(err))
}
fn key_from_namespaced(name: &Namespaced, err: radicle_crypto::PublicKeyError) -> Self {
Self::KeyFromNamespaced(name.to_ref_string(), Box::new(err))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
use radicle::{prelude::NodeId, storage::RefUpdate};
use std::str::FromStr;
use crate::{
refs::{branch_from_namespaced, ref_string},
test::{MockNode, TestResult},
};
const MAIN_BRANCH_REF_NAME: &str =
"refs/namespaces/z6MkiB8T5cBEQHnrs2MgjMVqvpSVj42X81HjKfFi2XBoMbtr/refs/heads/main";
const PATCH_REF_NAME: &str = "refs/namespaces/z6MkiB8T5cBEQHnrs2MgjMVqvpSVj42X81HjKfFi2XBoMbtr/refs/heads/patches/f9fa90725474de9002be503ae3cda4670c9a1740";
const PATCH_ID: &str = "f9fa90725474de9002be503ae3cda4670c9a1740";
fn nid() -> NodeId {
const NID: &str = "z6MkgEMYod7Hxfy9qCvDv5hYHkZ4ciWmLFgfvm3Wn1b2w2FV";
NodeId::from_str(NID).unwrap()
}
fn rid() -> RepoId {
const RID: &str = "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5";
RepoId::from_urn(RID).unwrap()
}
fn oid_from(oid: &str) -> Oid {
Oid::from_str(oid).unwrap()
}
fn oid() -> Oid {
const OID: &str = "ff3099ba5de28d954c41d0b5a84316f943794ea4";
oid_from(OID)
}
fn namespaced_main<'a>() -> Namespaced<'a> {
ref_string(MAIN_BRANCH_REF_NAME)
.unwrap()
.to_namespaced()
.unwrap()
.to_owned()
}
fn plain_main() -> BranchName {
branch_from_namespaced(&namespaced_main()).unwrap()
}
#[test]
fn nothing_updated() -> TestResult<()> {
let mock_node = MockNode::new()?;
let profile = mock_node.profile()?;
let event = Event::RefsFetched {
remote: nid(),
rid: rid(),
updated: vec![],
};
let result = CiEvent::from_node_event(&event, &profile);
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec![]);
Ok(())
}
#[test]
fn skipped() -> TestResult<()> {
let mock_node = MockNode::new()?;
let profile = mock_node.profile()?;
let event = Event::RefsFetched {
remote: nid(),
rid: rid(),
updated: vec![RefUpdate::Skipped {
name: ref_string(MAIN_BRANCH_REF_NAME).unwrap(),
oid: oid(),
}],
};
let result = CiEvent::from_node_event(&event, &profile);
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec![]);
Ok(())
}
#[test]
fn branch_created() -> TestResult<()> {
let mock_node = MockNode::new()?;
let profile = mock_node.profile()?;
let rid = rid();
let oid = oid();
let event = Event::RefsFetched {
remote: nid(),
rid,
updated: vec![RefUpdate::Created {
name: namespaced_main().to_ref_string(),
oid,
}],
};
let x = CiEvent::from_node_event(&event, &profile);
eprintln!("result: {x:#?}");
match x {
Err(_) => panic!("should succeed"),
Ok(events) if !events.is_empty() => {
for e in events {
match e {
CiEvent::V1(CiEventV1::BranchCreated {
from_node: _,
repo,
branch,
tip,
}) if repo == rid && branch == plain_main() && tip == oid => {}
_ => panic!("should not succeed that way"),
}
}
}
Ok(_) => panic!("empty list of events should not happen"),
}
Ok(())
}
#[test]
fn branch_updated() -> TestResult<()> {
let mock_node = MockNode::new()?;
let profile = mock_node.profile()?;
let rid = rid();
let oid = oid();
let event = Event::RefsFetched {
remote: nid(),
rid,
updated: vec![RefUpdate::Updated {
name: namespaced_main().to_ref_string(),
old: oid,
new: oid,
}],
};
let x = CiEvent::from_node_event(&event, &profile);
eprintln!("result: {x:#?}");
match x {
Err(_) => panic!("should succeed"),
Ok(events) if !events.is_empty() => {
for e in events {
match e {
CiEvent::V1(CiEventV1::BranchUpdated {
from_node: _,
repo,
branch,
tip,
old_tip,
}) if repo == rid
&& branch == plain_main()
&& tip == oid
&& old_tip == oid => {}
_ => panic!("should not succeed that way"),
}
}
}
Ok(_) => panic!("empty list of events should not happen"),
}
Ok(())
}
#[test]
fn branch_deleted() -> TestResult<()> {
let mock_node = MockNode::new()?;
let profile = mock_node.profile()?;
let rid = rid();
let oid = oid();
let event = Event::RefsFetched {
remote: nid(),
rid,
updated: vec![RefUpdate::Deleted {
name: namespaced_main().to_ref_string(),
oid,
}],
};
let x = CiEvent::from_node_event(&event, &profile);
eprintln!("result: {x:#?}");
match x {
Err(_) => panic!("should succeed"),
Ok(events) if !events.is_empty() => {
for e in events {
match e {
CiEvent::V1(CiEventV1::BranchDeleted {
repo, branch, tip, ..
}) if repo == rid && branch == plain_main() && tip == oid => {}
_ => panic!("should not succeed that way"),
}
}
}
Ok(_) => panic!("empty list of events should not happen"),
}
Ok(())
}
#[test]
fn patch_created() -> TestResult<()> {
let mock_node = MockNode::new()?;
let profile = mock_node.profile()?;
let rid = rid();
let patch_id = oid_from(PATCH_ID).into();
let oid = oid();
let event = Event::RefsFetched {
remote: nid(),
rid,
updated: vec![RefUpdate::Created {
name: ref_string(PATCH_REF_NAME).unwrap(),
oid,
}],
};
let x = CiEvent::from_node_event(&event, &profile);
eprintln!("result: {x:#?}");
match x {
Err(_) => panic!("should succeed"),
Ok(events) if !events.is_empty() => {
for e in events {
match e {
CiEvent::V1(CiEventV1::PatchCreated {
from_node: _,
repo,
patch,
new_tip,
}) if repo == rid && patch == patch_id && new_tip == oid => {}
_ => panic!("should not succeed that way"),
}
}
}
Ok(_) => panic!("empty list of events should not happen"),
}
Ok(())
}
#[test]
fn patch_updated() -> TestResult<()> {
let mock_node = MockNode::new()?;
let profile = mock_node.profile()?;
let rid = rid();
let patch_id = oid_from(PATCH_ID).into();
let oid = oid();
let event = Event::RefsFetched {
remote: nid(),
rid,
updated: vec![RefUpdate::Updated {
name: ref_string(PATCH_REF_NAME).unwrap(),
old: oid,
new: oid,
}],
};
let x = CiEvent::from_node_event(&event, &profile);
eprintln!("result: {x:#?}");
match x {
Err(_) => panic!("should succeed"),
Ok(events) if !events.is_empty() => {
for e in events {
match e {
CiEvent::V1(CiEventV1::PatchUpdated {
from_node: _,
repo,
patch,
new_tip,
}) if repo == rid && patch == patch_id && new_tip == oid => {}
_ => panic!("should not succeed that way"),
}
}
}
Ok(_) => panic!("empty list of events should not happen"),
}
Ok(())
}
#[test]
fn diff_refs() {
let oid1 = oid_from("c474d78c37a5c664aba2042db78ec235c0a5d569");
let oid2 = oid_from("5b6886f63ff90d61afaeec2f3569a9a1a0984bdb");
let foo = RefString::try_from("refs/heads/foo").unwrap();
let bar = RefString::try_from("refs/heads/bar").unwrap();
let baz = RefString::try_from("refs/heads/baz").unwrap();
let xyz = RefString::try_from("refs/heads/xyz").unwrap();
let old = Refs::from(
[
(foo.clone(), oid1),
(bar.clone(), oid1),
(xyz.clone(), oid1),
]
.into_iter(),
);
let new = Refs::from(
[
(foo.clone(), oid2),
(baz.clone(), oid2),
(xyz.clone(), oid1),
]
.into_iter(),
);
let updates = super::diff_refs(&old, &new);
assert_eq!(
updates,
[
RefUpdate::Updated {
name: foo,
old: oid1,
new: oid2
},
RefUpdate::Deleted {
name: bar,
oid: oid1,
},
RefUpdate::Created {
name: baz,
oid: oid2,
}
]
);
}
}
#[derive(Debug, Eq, PartialEq)]
#[allow(dead_code)]
enum ParsedRef {
Branch(BranchName),
Patch(PatchId),
Tag(TagName),
}
impl ParsedRef {
#[allow(clippy::unwrap_used)]
fn parse_ref(refname: &RefString) -> Option<Self> {
use crate::refs::branch_from_str;
fn parse_patch_id(refname: &RefString) -> Option<ParsedRef> {
const PATTERN: &str = r"^refs/namespaces/[^/]+/refs/heads/patches/([^/]+)$";
let re = Regex::new(PATTERN).unwrap();
if let Some(captures) = re.captures(refname)
&& let Some(patch_id) = captures.get(1)
&& let Ok(oid) = Oid::from_str(patch_id.as_str())
{
let patch_id = PatchId::from(oid);
return Some(ParsedRef::Patch(patch_id));
}
None
}
fn parse_branch_name(refname: &RefString) -> Option<ParsedRef> {
const PATTERN: &str = r"^refs/namespaces/[^/]+/refs/heads/(.+)$";
let re = Regex::new(PATTERN).unwrap();
if let Some(captures) = re.captures(refname)
&& let Some(branch_name) = captures.get(1)
&& let Ok(branch_name) = branch_from_str(branch_name.as_str())
{
return Some(ParsedRef::Branch(branch_name));
}
None
}
fn parse_tag_name(refname: &RefString) -> Option<ParsedRef> {
const PATTERN: &str = r"^refs/namespaces/[^/]+/refs/tags/(.+)$";
let re = Regex::new(PATTERN).unwrap();
if let Some(captures) = re.captures(refname)
&& let Some(tag_name) = captures.get(1)
&& let Ok(tag_name) = ref_string(tag_name.as_str())
{
return Some(ParsedRef::Tag(tag_name.into()));
}
None
}
parse_patch_id(refname)
.or_else(|| parse_branch_name(refname))
.or_else(|| parse_tag_name(refname))
.or(None)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test_parsed_ref {
use std::str::FromStr;
use crate::refs::{branch_ref, ref_string};
use super::*;
#[test]
fn branch() {
let actual = ref_string("refs/namespaces/NID/refs/heads/main").unwrap();
let wanted = branch_ref(&ref_string("main").unwrap()).unwrap();
assert_eq!(
ParsedRef::parse_ref(&actual),
Some(ParsedRef::Branch(wanted))
);
}
#[test]
fn patch() {
let actual = ref_string(
"refs/namespaces/NID/refs/heads/patches/9d1a97571e86caafa86df7bc1692d305710a596e",
)
.unwrap();
let wanted = PatchId::from_str("9d1a97571e86caafa86df7bc1692d305710a596e").unwrap();
assert_eq!(
ParsedRef::parse_ref(&actual),
Some(ParsedRef::Patch(wanted))
);
}
#[test]
fn tag() {
let actual = ref_string("refs/namespaces/NID/refs/tags/v0.0.0").unwrap();
let wanted = ref_string("v0.0.0").unwrap();
assert_eq!(
ParsedRef::parse_ref(&actual),
Some(ParsedRef::Tag(wanted.into()))
);
}
}