1use prikk_error::{PrikkError, Result};
15use prikk_object::ObjectEnvelope;
16
17use crate::fsutil::{append_file_required, read_file_if_exists, truncate_file_empty_required};
18use crate::layout::RepositoryLayout;
19use crate::lock::ActiveLock;
20use crate::refs::{ensure_no_incomplete_publication, validate_local_branch_ref};
21use crate::wal::Wal;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ActiveCommitResult {
26 pub wal_sequence: u64,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum ActiveRefMetadata {
33 Missing,
35 Valid(String),
37 Invalid(String),
39}
40
41#[derive(Debug, Clone)]
43pub struct ActiveSession {
44 layout: RepositoryLayout,
45}
46
47impl ActiveSession {
48 #[must_use]
50 pub fn new(layout: RepositoryLayout) -> Self {
51 Self { layout }
52 }
53
54 pub fn append_patch(
59 &self,
60 envelope: &ObjectEnvelope,
61 active_patch_limit: usize,
62 ) -> Result<ActiveCommitResult> {
63 self.layout.require_current_format()?;
64 let _lock = ActiveLock::acquire(&self.layout)?;
65 ensure_no_incomplete_publication(&self.layout)?;
66 let wal = Wal::for_layout(&self.layout);
67 let replay = wal.replay()?;
68 if replay.trailing_partial_bytes != 0 {
69 return Err(PrikkError::Integrity(format!(
70 "active WAL has {} trailing partial bytes; run doctor before appending",
71 replay.trailing_partial_bytes
72 )));
73 }
74 if replay.has_item_failure() {
77 return Err(PrikkError::Integrity(
78 "active WAL has a damaged record; run doctor before appending".to_string(),
79 ));
80 }
81 if crate::worktree_patch::active_patch_limit_exceeded(
82 replay.records.len(),
83 active_patch_limit,
84 ) {
85 return Err(PrikkError::LockConflict(format!(
86 "active WAL has {} queued patches, at or above the configured limit \
87 ({active_patch_limit}); run doctor or seal before appending again",
88 replay.records.len()
89 )));
90 }
91 if replay.records.is_empty() {
92 prepare_empty_active_ref_for_append(&self.layout, "heads/main")?;
93 } else {
94 require_active_ref_for_non_empty_wal(&self.layout, "heads/main")?;
97 }
98 let wal_sequence = wal.append_patch(envelope)?;
99 Ok(ActiveCommitResult { wal_sequence })
100 }
101}
102
103pub fn read_active_ref_metadata(layout: &RepositoryLayout) -> Result<ActiveRefMetadata> {
105 let relative = layout.repository_relative(&layout.default_active_ref_name_path())?;
106 let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
107 return Ok(ActiveRefMetadata::Missing);
108 };
109 if bytes.is_empty() {
114 return Ok(ActiveRefMetadata::Missing);
115 }
116 let text = match std::str::from_utf8(&bytes) {
117 Ok(text) => text,
118 Err(err) => {
119 return Ok(ActiveRefMetadata::Invalid(format!(
120 "active ref metadata is not UTF-8: {err}"
121 )));
122 }
123 };
124 match validate_local_branch_ref(text) {
125 Ok(canonical) => Ok(ActiveRefMetadata::Valid(canonical)),
126 Err(err) => Ok(ActiveRefMetadata::Invalid(err.to_string())),
127 }
128}
129
130pub fn write_active_ref_metadata(layout: &RepositoryLayout, ref_name: &str) -> Result<String> {
135 layout.require_current_format()?;
136 let canonical = validate_local_branch_ref(ref_name)?;
137 let relative = layout.repository_relative(&layout.default_active_ref_name_path())?;
138 truncate_file_empty_required(layout.repository_mutation_root(), &relative)?;
139 append_file_required(
140 layout.repository_mutation_root(),
141 &relative,
142 canonical.as_bytes(),
143 )?;
144 Ok(canonical)
145}
146
147pub fn remove_active_ref_metadata(layout: &RepositoryLayout) -> Result<bool> {
151 layout.require_current_format()?;
152 remove_active_ref_metadata_authorized(layout)
153}
154
155fn remove_active_ref_metadata_authorized(layout: &RepositoryLayout) -> Result<bool> {
156 let relative = layout.repository_relative(&layout.default_active_ref_name_path())?;
157 let had_content = !read_file_if_exists(layout.repository_mutation_root(), &relative)?
158 .unwrap_or_default()
159 .is_empty();
160 truncate_file_empty_required(layout.repository_mutation_root(), &relative)?;
161 Ok(had_content)
162}
163
164pub fn finish_active_publication_cleanup(
166 layout: &RepositoryLayout,
167 active_lock: &ActiveLock,
168) -> Result<()> {
169 layout.require_current_format()?;
170 active_lock.require_layout(layout)?;
171 Wal::for_layout(layout).truncate_empty()?;
172 remove_active_ref_metadata_authorized(layout)?;
173 Ok(())
174}
175
176pub(crate) fn prepare_empty_active_ref_for_append(
187 layout: &RepositoryLayout,
188 ref_name: &str,
189) -> Result<String> {
190 write_active_ref_metadata(layout, ref_name)
191}
192
193pub fn require_active_ref_for_non_empty_wal(
195 layout: &RepositoryLayout,
196 ref_name: &str,
197) -> Result<String> {
198 let expected = validate_local_branch_ref(ref_name)?;
199 match read_active_ref_metadata(layout)? {
200 ActiveRefMetadata::Valid(actual) if actual == expected => Ok(actual),
201 ActiveRefMetadata::Valid(actual) => Err(PrikkError::LockConflict(format!(
202 "active WAL is owned by {actual}; requested ref {expected}"
203 ))),
204 ActiveRefMetadata::Missing => Err(PrikkError::Integrity(
205 "active WAL has records but active ref metadata is missing".to_string(),
206 )),
207 ActiveRefMetadata::Invalid(reason) => Err(PrikkError::Integrity(format!(
208 "active WAL has records but active ref metadata is malformed: {reason}"
209 ))),
210 }
211}
212
213#[cfg(all(test, target_os = "linux"))]
216mod tests;