radicle: Have migration repair downgrades

Instead of repairing downgrades, automatic migration could fail.
This occurs when attempting to load signed references, and a downgrade
is detected while coalescing `RepositoryInfo`.

Similarly to how fetch recovers, automatic migration must also recover
by detecting for the downgrade error, and allowing the migration to
happen in this case.
This commit is contained in:
Lorenz Leutgeb 2026-03-24 19:13:59 +01:00 committed by Fintan Halpenny
parent 33db6637b4
commit 86cacfb846
5 changed files with 57 additions and 42 deletions

View File

@ -2,7 +2,7 @@ mod args;
pub use args::Args;
use radicle::storage::{ReadStorage, RepositoryInfo};
use radicle::storage::{ReadStorage, RepositoryInfo, SignedRefsInfo};
use crate::terminal as term;
@ -34,7 +34,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
if !doc.is_public() && args.public {
continue;
}
if refs.is_none() && !args.all && !args.seeded {
if matches!(refs, SignedRefsInfo::None) && !args.all && !args.seeded {
continue;
}
let seeded = policy.is_seeding(&rid)?;

View File

@ -38,7 +38,7 @@ use radicle::node::seed;
use radicle::node::seed::Store as _;
use radicle::node::{Penalty, Severity};
use radicle::storage::refs::{FeatureLevel, SIGREFS_BRANCH};
use radicle::storage::RepositoryError;
use radicle::storage::{RepositoryError, RepositoryInfo, SignedRefsInfo};
use radicle_fetch::policy::SeedingPolicy;
use crate::fetcher;
@ -522,33 +522,19 @@ where
Ok(())
}
fn upgrade_sigrefs(&mut self, info: &radicle::storage::RepositoryInfo) -> Result<(), Error> {
let Some(ref refs) = info.refs else {
// No refs, nothing to upgrade.
return Ok(());
};
if refs.feature_level() >= FeatureLevel::LATEST && refs.parent().is_some() {
// Refs are at target level or above, nothing to upgrade.
fn upgrade_sigrefs(&mut self, info: &RepositoryInfo) -> Result<(), Error> {
if !matches!(info.refs, SignedRefsInfo::NeedsMigration) {
return Ok(());
}
let rid = info.rid;
if refs.parent().is_none() {
log::info!(
"Migrating `rad/sigrefs` of {rid} to force feature level {}, as the history currently contains only a root commit.",
FeatureLevel::LATEST
);
} else {
log::info!(
"Migrating `rad/sigrefs` of {rid} from level {} which is lower than target level {}.",
refs.feature_level(),
FeatureLevel::LATEST
);
}
log::info!(
"Migrating `rad/sigrefs` of {rid} to force feature level {}.",
FeatureLevel::LATEST
);
let repo = self.storage.repository_mut(info.rid)?;
let repo = self.storage.repository_mut(rid)?;
// NOTE: We assume to reach `FeatureLevel::LATEST` by signing refs.
repo.force_sign_refs(&self.signer)?;
Ok(())

View File

@ -26,11 +26,40 @@ use crate::identity::{Identity, RepoId};
use crate::node::device::Device;
use crate::node::SyncedAt;
use crate::storage::git::NAMESPACES_GLOB;
use crate::storage::refs::Refs;
use crate::storage::refs::{FeatureLevel, Refs, SignedRefsAt};
use self::refs::{RefsAt, SignedRefs};
use crate::git::UserInfo;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SignedRefsInfo {
/// Repositories with this set to `None` are ones that are seeded but not forked.
None,
/// Local signed refs, if any.
Some(refs::SignedRefsAt),
NeedsMigration,
}
impl SignedRefsInfo {
pub(crate) fn new(
result: Result<Option<SignedRefsAt>, refs::sigrefs::read::error::Read>,
) -> Result<Self, refs::sigrefs::read::error::Read> {
Ok(match result {
Ok(Some(refs))
if refs.feature_level() >= FeatureLevel::LATEST && refs.parent().is_some() =>
{
SignedRefsInfo::Some(refs)
}
Ok(Some(_)) => SignedRefsInfo::NeedsMigration,
Ok(None) => SignedRefsInfo::None,
Err(refs::sigrefs::read::error::Read::Downgrade { .. }) => {
SignedRefsInfo::NeedsMigration
}
Err(err) => return Err(err),
})
}
}
/// Basic repository information.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepositoryInfo {
@ -40,9 +69,8 @@ pub struct RepositoryInfo {
pub head: Oid,
/// Identity document.
pub doc: Doc,
/// Local signed refs, if any.
/// Repositories with this set to `None` are ones that are seeded but not forked.
pub refs: Option<refs::SignedRefsAt>,
/// Information about local signed references.
pub refs: SignedRefsInfo,
/// Sync time of the repository.
pub synced_at: Option<SyncedAt>,
}

View File

@ -18,9 +18,8 @@ use crate::identity::doc::DocError;
use crate::identity::{CanonicalRefs, Doc, DocAt, RepoId};
use crate::identity::{Identity, Project};
use crate::node::device::Device;
use crate::node::SyncedAt;
use crate::storage::refs;
use crate::storage::refs::{FeatureLevel, Refs, SignedRefs, SignedRefsAt};
use crate::storage::{refs, SignedRefsInfo};
use crate::storage::{
ReadRepository, ReadStorage, Remote, Remotes, RepositoryInfo, SetHead, SignRepository,
WriteRepository, WriteStorage,
@ -167,12 +166,13 @@ impl ReadStorage for Storage {
}
};
// Nb. This will be `None` if they were not found.
let refs = refs::SignedRefsAt::load(self.info.key, &repo)
let refs = SignedRefsInfo::new(refs::SignedRefsAt::load(self.info.key, &repo))
.map_err(|err| Error::Refs(refs::Error::Read(err)))?;
let synced_at = refs
.as_ref()
.map(|r| node::SyncedAt::new(r.at, &repo))
.transpose()?;
let synced_at = match &refs {
SignedRefsInfo::Some(refs) => Some(node::SyncedAt::new(refs.at, &repo)?),
_ => None,
};
repos.push(RepositoryInfo {
rid,
@ -258,12 +258,13 @@ impl Storage {
let repo = self.repository(*rid)?;
let (_, head) = repo.head()?;
let refs = refs::SignedRefsAt::load(self.info.key, &repo)
.map_err(|err| RepositoryError::from(refs::Error::Read(err)))?;
let synced_at = refs
.as_ref()
.map(|r| SyncedAt::new(r.at, &repo))
.transpose()?;
let refs = SignedRefsInfo::new(refs::SignedRefsAt::load(self.info.key, &repo))
.map_err(|err| Error::Refs(refs::Error::Read(err)))?;
let synced_at = match &refs {
SignedRefsInfo::Some(refs) => Some(node::SyncedAt::new(refs.at, &repo)?),
_ => None,
};
Ok(RepositoryInfo {
rid: *rid,

View File

@ -102,7 +102,7 @@ impl ReadStorage for MockStorage {
rid: *rid,
head: r.head().unwrap().1,
doc: r.doc.clone().into(),
refs: None,
refs: SignedRefsInfo::None,
synced_at: None,
})
.collect())