pub mod error; use either::Either; use radicle::git::raw::ErrorExt as _; use radicle::git::{self, Namespaced, Oid, Qualified}; use radicle::storage::git::Repository; use super::refs::{Applied, Policy, RefUpdate, Update}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Ancestry { Equal, Ahead, Behind, Diverged, } pub enum Updated<'a> { Accepted(RefUpdate), Rejected(Update<'a>), } impl From for Updated<'_> { fn from(up: RefUpdate) -> Self { Updated::Accepted(up) } } impl<'a> From> for Updated<'a> { fn from(up: Update<'a>) -> Self { Updated::Rejected(up) } } pub fn contains(repo: &Repository, oid: Oid) -> Result { repo.backend .odb() .map(|odb| odb.exists(oid.into())) .map_err(error::Contains) } /// Find the object identified by `oid` and peel it to its associated /// commit `Oid`. /// /// # Errors /// /// - The object was not found /// - The object does not peel to a commit /// - Attempting to find the object fails fn find_and_peel(repo: &Repository, oid: Oid) -> Result { match repo.backend.find_object(*oid, None) { Ok(object) => Ok(object .peel(git::raw::ObjectType::Commit) .map_err(|err| error::Ancestry::Peel { oid, err })? .id() .into()), Err(e) if e.is_not_found() => Err(error::Ancestry::Missing { oid }), Err(err) => Err(error::Ancestry::Object { oid, err }), } } /// Peels the two objects to commits (see [`find_and_peel`]) and determines /// their ancestry relationship (see [`ahead_behind`]). pub fn ancestry(repo: &Repository, old: Oid, new: Oid) -> Result { let old = find_and_peel(repo, old)?; let new = find_and_peel(repo, new)?; ahead_behind(repo, old, new) } /// Determine the ancestry relationship between two commits. pub fn ahead_behind( repo: &Repository, old_commit: Oid, new_commit: Oid, ) -> Result { if old_commit == new_commit { return Ok(Ancestry::Equal); } let (ahead, behind) = repo .backend .graph_ahead_behind(*new_commit, *old_commit) .map_err(|err| error::Ancestry::Check { old: old_commit, new: new_commit, err, })?; if ahead > 0 && behind == 0 { Ok(Ancestry::Ahead) } else if ahead == 0 && behind > 0 { Ok(Ancestry::Behind) } else { Ok(Ancestry::Diverged) } } pub fn refname_to_id<'a, N>(repo: &Repository, refname: N) -> Result, error::Resolve> where N: Into>, { use radicle::git::raw::ErrorCode::NotFound; let refname = refname.into(); match repo.backend.refname_to_id(refname.as_ref()) { Ok(oid) => Ok(Some(oid.into())), Err(e) if matches!(e.code(), NotFound) => Ok(None), Err(err) => Err(error::Resolve { name: refname.to_owned(), err, }), } } pub fn update<'a, I>(repo: &Repository, updates: I) -> Result, error::Update> where I: IntoIterator>, { let mut applied = Applied::default(); for up in updates.into_iter() { match up { Update::Direct { name, target, no_ff, } => match direct(repo, name, target, no_ff)? { Updated::Rejected(r) => applied.rejected.push(r), Updated::Accepted(u) => applied.updated.push(u), }, Update::Prune { name, prev } => match prune(repo, name, prev)? { Updated::Rejected(r) => applied.rejected.push(r), Updated::Accepted(u) => applied.updated.push(u), }, } } Ok(applied) } fn direct<'a>( repo: &Repository, name: Namespaced<'a>, target: Oid, no_ff: Policy, ) -> Result, error::Update> { let Some(reference) = find(repo, &name)? else { repo.backend .reference(name.as_ref(), target.into(), false, "radicle: create") .map_err(|err| error::Update::Create { name: name.to_owned(), target, err, })?; return Ok(RefUpdate::Created { name: name.to_ref_string(), oid: target, } .into()); }; let Some(prev) = reference.target() else { // This should never happen, as there are no facilities to create // symbolic references in Radicle namespaces. If it does, e.g. because // some external program or the user themselves created it, we better // do not touch it. return Err(error::Update::Symbolic { name: name.to_owned(), }); }; if prev == *target { // If the two objects are identical, their ancestry does not matter, // we can always skip the update. return Ok(RefUpdate::Skipped { name: name.to_ref_string(), oid: target, } .into()); } let ancestry = { use git::raw::ObjectType::{self, *}; const ANY_KIND: Option = Some(Any); let prev = repo.backend.find_object(prev, ANY_KIND).map_err(|err| { error::Update::Ancestry(error::Ancestry::Object { oid: prev.into(), err, }) })?; let target = repo .backend .find_object(*target, ANY_KIND) .map_err(|err| error::Update::Ancestry(error::Ancestry::Object { oid: target, err }))?; match (prev.kind(), target.kind()) { (Some(Commit), Some(Commit)) => { // This is the common case, we have two commits to compare. let prev = prev.id().into(); let target = target.id().into(); Some(ahead_behind(repo, prev, target)?) } (Some(Tag), Some(Tag)) => { // Even though these tags might point to the same commit, // refuse to peel, because that tag itself has changed // (e.g. its name or signature). None } (Some(Commit | Tag), Some(Commit | Tag)) => { // The reference changes from a commit to a tag or vice versa. None } _ => { // One of the objects is not a commit or a tag, we're clueless. None } } }; match ancestry { Some(Ancestry::Equal) => Ok(RefUpdate::Skipped { name: name.to_ref_string(), oid: target, } .into()), Some(Ancestry::Ahead) => { // N.b. the update is a fast-forward so we can safely // pass `force: true`. repo.backend .reference(name.as_ref(), target.into(), true, "radicle: update") .map_err(|err| error::Update::Create { name: name.to_owned(), target, err, })?; Ok(RefUpdate::from(name.to_ref_string(), prev, target).into()) } Some(Ancestry::Behind | Ancestry::Diverged) | None if matches!(no_ff, Policy::Allow) => { // N.b. the update is a non-fast-forward but // we allow it, so we pass `force: true`. repo.backend .reference(name.as_ref(), target.into(), true, "radicle: update") .map_err(|err| error::Update::Create { name: name.to_owned(), target, err, })?; Ok(RefUpdate::from(name.to_ref_string(), prev, target).into()) } // N.b. if the target is behind, we simply reject the update Some(Ancestry::Behind) => Ok(Update::Direct { name, target, no_ff, } .into()), Some(Ancestry::Diverged) | None if matches!(no_ff, Policy::Reject) => Ok(Update::Direct { name, target, no_ff, } .into()), Some(Ancestry::Diverged) | None => Err(error::Update::NonFF { name: name.to_owned(), new: target, cur: prev.into(), }), } } fn prune<'a>( repo: &Repository, name: Namespaced<'a>, prev: Either>, ) -> Result, error::Update> { use radicle::git::raw::ObjectType; match find(repo, &name)? { Some(mut r) => { // N.b. peel this reference to whatever object it points to, // presumably a commit, and get its Oid let prev = r .peel(ObjectType::Any) .map_err(error::Update::Peel)? .id() .into(); r.delete().map_err(|err| error::Update::Delete { name: name.to_owned(), err, })?; Ok(RefUpdate::Deleted { name: name.to_ref_string(), oid: prev, } .into()) } None => Ok(Update::Prune { name, prev }.into()), } } fn find<'a>( repo: &'a Repository, name: &Namespaced<'_>, ) -> Result>, error::Update> { match repo.backend.find_reference(name.as_ref()) { Ok(r) => Ok(Some(r)), Err(e) if matches!(e.code(), radicle::git::raw::ErrorCode::NotFound) => Ok(None), Err(err) => Err(error::Update::Find { name: name.clone().into_owned(), err, }), } }