use prikk_error::{PrikkError, Result};
use prikk_object::ObjectEnvelope;
use crate::fsutil::{read_file_if_exists, remove_file_if_present_required, write_file_atomically};
use crate::layout::RepositoryLayout;
use crate::lock::ActiveLock;
use crate::refs::{ensure_no_incomplete_publication, validate_local_branch_ref};
use crate::wal::Wal;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveCommitResult {
pub wal_sequence: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActiveRefMetadata {
Missing,
Valid(String),
Invalid(String),
}
#[derive(Debug)]
pub struct LegacyActiveCleanupAuthorization {
layout: RepositoryLayout,
}
#[derive(Debug, Clone)]
pub struct ActiveSession {
layout: RepositoryLayout,
}
impl ActiveSession {
#[must_use]
pub fn new(layout: RepositoryLayout) -> Self {
Self { layout }
}
pub fn append_patch(
&self,
envelope: &ObjectEnvelope,
active_patch_limit: usize,
) -> Result<ActiveCommitResult> {
self.layout.require_current_format()?;
let _lock = ActiveLock::acquire(&self.layout)?;
ensure_no_incomplete_publication(&self.layout)?;
let wal = Wal::for_layout(&self.layout);
let replay = wal.replay()?;
if replay.trailing_partial_bytes != 0 {
return Err(PrikkError::Integrity(format!(
"active WAL has {} trailing partial bytes; run doctor before appending",
replay.trailing_partial_bytes
)));
}
if crate::worktree_patch::active_patch_limit_exceeded(
replay.records.len(),
active_patch_limit,
) {
return Err(PrikkError::LockConflict(format!(
"active WAL has {} queued patches, at or above the configured limit \
({active_patch_limit}); run doctor or seal before appending again",
replay.records.len()
)));
}
if replay.records.is_empty() {
prepare_empty_active_ref_for_append(&self.layout, "heads/main")?;
} else {
require_active_ref_for_non_empty_wal(&self.layout, "heads/main")?;
}
let wal_sequence = wal.append_patch(envelope)?;
Ok(ActiveCommitResult { wal_sequence })
}
}
pub fn read_active_ref_metadata(layout: &RepositoryLayout) -> Result<ActiveRefMetadata> {
let relative = layout.repository_relative(&layout.default_active_ref_name_path())?;
let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
return Ok(ActiveRefMetadata::Missing);
};
let text = match std::str::from_utf8(&bytes) {
Ok(text) => text,
Err(err) => {
return Ok(ActiveRefMetadata::Invalid(format!(
"active ref metadata is not UTF-8: {err}"
)));
}
};
match validate_local_branch_ref(text) {
Ok(canonical) => Ok(ActiveRefMetadata::Valid(canonical)),
Err(err) => Ok(ActiveRefMetadata::Invalid(err.to_string())),
}
}
pub fn write_active_ref_metadata(layout: &RepositoryLayout, ref_name: &str) -> Result<String> {
layout.require_current_format()?;
let canonical = validate_local_branch_ref(ref_name)?;
let relative = layout.repository_relative(&layout.default_active_ref_name_path())?;
write_file_atomically(
layout.repository_mutation_root(),
&relative,
canonical.as_bytes(),
)?;
Ok(canonical)
}
pub fn remove_active_ref_metadata(layout: &RepositoryLayout) -> Result<bool> {
layout.require_current_format()?;
remove_active_ref_metadata_authorized(layout)
}
fn remove_active_ref_metadata_authorized(layout: &RepositoryLayout) -> Result<bool> {
let relative = layout.repository_relative(&layout.default_active_ref_name_path())?;
remove_file_if_present_required(layout.repository_mutation_root(), &relative)
}
pub fn finish_active_publication_cleanup(
layout: &RepositoryLayout,
active_lock: &ActiveLock,
) -> Result<()> {
layout.require_current_format()?;
active_lock.require_layout(layout)?;
Wal::for_layout(layout).truncate_empty()?;
remove_active_ref_metadata_authorized(layout)?;
Ok(())
}
pub fn finish_legacy_active_publication_cleanup(
layout: &RepositoryLayout,
active_lock: &ActiveLock,
authorization: LegacyActiveCleanupAuthorization,
) -> Result<()> {
active_lock.require_layout(layout)?;
if authorization.layout != *layout {
return Err(PrikkError::Integrity(
"legacy active cleanup authorization belongs to a different repository".to_string(),
));
}
layout.validate_format()?;
Wal::for_layout(layout).truncate_empty_for_legacy_recovery()?;
remove_active_ref_metadata_authorized(layout)?;
Ok(())
}
pub(crate) fn authorize_legacy_active_cleanup(
layout: &RepositoryLayout,
) -> LegacyActiveCleanupAuthorization {
LegacyActiveCleanupAuthorization {
layout: layout.clone(),
}
}
pub(crate) fn prepare_empty_active_ref_for_append(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<String> {
match read_active_ref_metadata(layout)? {
ActiveRefMetadata::Missing => {}
ActiveRefMetadata::Valid(_) | ActiveRefMetadata::Invalid(_) => {
remove_active_ref_metadata(layout)?;
}
}
write_active_ref_metadata(layout, ref_name)
}
pub fn require_active_ref_for_non_empty_wal(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<String> {
let expected = validate_local_branch_ref(ref_name)?;
match read_active_ref_metadata(layout)? {
ActiveRefMetadata::Valid(actual) if actual == expected => Ok(actual),
ActiveRefMetadata::Valid(actual) => Err(PrikkError::LockConflict(format!(
"active WAL is owned by {actual}; requested ref {expected}"
))),
ActiveRefMetadata::Missing => Err(PrikkError::Integrity(
"active WAL has records but active ref metadata is missing".to_string(),
)),
ActiveRefMetadata::Invalid(reason) => Err(PrikkError::Integrity(format!(
"active WAL has records but active ref metadata is malformed: {reason}"
))),
}
}
#[cfg(test)]
mod tests;