317 lines
9.5 KiB
Rust
317 lines
9.5 KiB
Rust
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<RefUpdate> for Updated<'_> {
|
|
fn from(up: RefUpdate) -> Self {
|
|
Updated::Accepted(up)
|
|
}
|
|
}
|
|
|
|
impl<'a> From<Update<'a>> for Updated<'a> {
|
|
fn from(up: Update<'a>) -> Self {
|
|
Updated::Rejected(up)
|
|
}
|
|
}
|
|
|
|
pub fn contains(repo: &Repository, oid: Oid) -> Result<bool, error::Contains> {
|
|
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<Oid, error::Ancestry> {
|
|
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<Ancestry, error::Ancestry> {
|
|
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<Ancestry, error::Ancestry> {
|
|
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<Option<Oid>, error::Resolve>
|
|
where
|
|
N: Into<Qualified<'a>>,
|
|
{
|
|
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<Applied<'a>, error::Update>
|
|
where
|
|
I: IntoIterator<Item = Update<'a>>,
|
|
{
|
|
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<Updated<'a>, 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<ObjectType> = 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<Oid, Qualified<'a>>,
|
|
) -> Result<Updated<'a>, 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<Option<radicle::git::raw::Reference<'a>>, 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,
|
|
}),
|
|
}
|
|
}
|