use crate::LixError;
use crate::changelog::CommitId;
use crate::commit_graph::{CommitGraphNode, CommitGraphReader};
use crate::common::validate_non_empty_identity_value;
use super::{BranchHead, BranchRefReader};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BranchOperation {
CreateBranch,
SwitchBranch,
MergeBranch,
MergeBranchPreview,
Restore,
LoadDefaultBranch,
}
impl BranchOperation {
pub(crate) fn label(self) -> &'static str {
match self {
Self::CreateBranch => "create_branch",
Self::SwitchBranch => "switch_branch",
Self::MergeBranch => "merge_branch",
Self::MergeBranchPreview => "merge_branch_preview",
Self::Restore => "restore",
Self::LoadDefaultBranch => "load_default_branch_id",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BranchReferenceRole {
Source,
Target,
DefaultBranch,
CommitSource,
}
impl BranchReferenceRole {
pub(crate) fn label(self) -> &'static str {
match self {
Self::Source => "source",
Self::Target => "target",
Self::DefaultBranch => "default_branch",
Self::CommitSource => "commit_source",
}
}
}
pub(crate) struct BranchLifecycle<'a> {
refs: &'a dyn BranchRefReader,
}
impl<'a> BranchLifecycle<'a> {
pub(crate) fn new(refs: &'a dyn BranchRefReader) -> Self {
Self { refs }
}
pub(crate) fn require_non_empty_id(
branch_id: &str,
operation: BranchOperation,
role: BranchReferenceRole,
) -> Result<(), LixError> {
require_canonical_uuid("branch_id", branch_id, operation, role)
}
pub(crate) async fn require_existing_commit(
commit_graph: &mut dyn CommitGraphReader,
commit_id: CommitId,
operation: BranchOperation,
role: BranchReferenceRole,
) -> Result<CommitGraphNode, LixError> {
commit_graph
.load_node(&commit_id)
.await?
.ok_or_else(|| LixError::commit_not_found(commit_id, operation.label(), role.label()))
}
pub(crate) fn parse_commit_id(
commit_id: &str,
operation: BranchOperation,
role: BranchReferenceRole,
) -> Result<CommitId, LixError> {
require_non_empty_public_id("commit_id", commit_id, operation, role)?;
CommitId::parse_lix(commit_id, "branch lifecycle commit_id")
}
pub(crate) async fn require_existing_ref(
&self,
branch_id: &str,
operation: BranchOperation,
role: BranchReferenceRole,
) -> Result<BranchHead, LixError> {
Self::require_non_empty_id(branch_id, operation, role)?;
self.require_existing_stored_ref(branch_id, operation, role)
.await
}
pub(crate) async fn require_existing_commit_id(
&self,
branch_id: &str,
operation: BranchOperation,
role: BranchReferenceRole,
) -> Result<CommitId, LixError> {
Ok(self
.require_existing_ref(branch_id, operation, role)
.await?
.commit_id)
}
async fn require_existing_stored_ref(
&self,
branch_id: &str,
operation: BranchOperation,
role: BranchReferenceRole,
) -> Result<BranchHead, LixError> {
self.refs
.load_head(branch_id)
.await?
.ok_or_else(|| LixError::branch_not_found(branch_id, operation.label(), role.label()))
}
}
fn require_non_empty_public_id(
label: &str,
value: &str,
operation: BranchOperation,
role: BranchReferenceRole,
) -> Result<(), LixError> {
validate_non_empty_identity_value(label, value)
.map(|_| ())
.map_err(|_| {
LixError::new(
LixError::CODE_INVALID_PARAM,
format!(
"{} {} {label} must be non-empty",
operation.label(),
role.label()
),
)
})
}
fn require_canonical_uuid(
label: &str,
value: &str,
operation: BranchOperation,
role: BranchReferenceRole,
) -> Result<(), LixError> {
require_non_empty_public_id(label, value, operation, role)?;
if crate::storage_codec::id_string::uuid_bytes_from_canonical(value).is_none() {
return Err(LixError::new(
LixError::CODE_INVALID_PARAM,
format!(
"{} {} {label} must be a canonical UUID",
operation.label(),
role.label()
),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use super::*;
use crate::changelog::CommitId;
#[tokio::test]
async fn require_existing_ref_returns_head() {
let reader = RowsBranchRefReader::new(vec![BranchHead {
working_base_commit_id: None,
branch_id: "01920000-0000-7000-8000-0000000000a1".to_string(),
commit_id: CommitId::for_test_label("commit-a"),
}]);
let lifecycle = BranchLifecycle::new(&reader);
let head = lifecycle
.require_existing_ref(
"01920000-0000-7000-8000-0000000000a1",
BranchOperation::SwitchBranch,
BranchReferenceRole::Target,
)
.await
.expect("branch should resolve");
assert_eq!(head.commit_id, "commit-a");
}
#[tokio::test]
async fn require_existing_ref_rejects_empty_id_as_invalid_param() {
let reader = RowsBranchRefReader::new(Vec::new());
let lifecycle = BranchLifecycle::new(&reader);
let error = lifecycle
.require_existing_ref(
"",
BranchOperation::SwitchBranch,
BranchReferenceRole::Target,
)
.await
.expect_err("empty branch id should be rejected before lookup");
assert_eq!(error.code, LixError::CODE_INVALID_PARAM);
}
#[tokio::test]
async fn require_existing_ref_reports_missing_branch() {
let reader = RowsBranchRefReader::new(Vec::new());
let lifecycle = BranchLifecycle::new(&reader);
let error = lifecycle
.require_existing_ref(
"01920000-0000-7000-8000-0000000000f1",
BranchOperation::SwitchBranch,
BranchReferenceRole::Target,
)
.await
.expect_err("missing branch should be rejected");
assert_eq!(error.code, LixError::CODE_BRANCH_NOT_FOUND);
}
struct RowsBranchRefReader {
heads: Vec<BranchHead>,
}
impl RowsBranchRefReader {
fn new(heads: Vec<BranchHead>) -> Self {
Self { heads }
}
}
#[async_trait]
impl BranchRefReader for RowsBranchRefReader {
async fn load_head(&self, branch_id: &str) -> Result<Option<BranchHead>, LixError> {
Ok(self
.heads
.iter()
.find(|head| head.branch_id == branch_id)
.cloned())
}
async fn scan_heads(&self) -> Result<Vec<BranchHead>, LixError> {
Ok(self.heads.clone())
}
}
}