1use super::capture_store::{
24 CAPTURE_LOCK_NAME, CaptureError, SEQUENCE_FILENAME, WORKTREE_ID_FILENAME,
25 ensure_capture_store, read_next_capture_sequence, read_store_file,
26};
27use super::capture_records::{CapturedRecord, encode_captured_record};
29use super::private_storage::{
31 sync_directory, write_exclusive_synced, write_private_file,
32};
33use super::live_lock::acquire_live_owner_lock;
35use super::owner_lock::OwnerLockError;
36use super::random_id::random_uuid;
38use super::transaction_phase::{Phase, PhaseMarkerError, reach_transaction_phase};
40use super::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git_checked};
41use super::transaction_journal::TransactionMode;
43use super::js_text::trim_javascript;
45use std::path::{Path, PathBuf};
47use std::collections::{BTreeMap, BTreeSet};
49use std::ffi::OsStr;
51
52pub const CAPTURED_FILENAME: &str = "captured.json";
54
55#[derive(Debug)]
59pub enum CaptureOrderError {
60 Store(CaptureError),
62 Lock(OwnerLockError),
64 Git(GitFailure),
66 Write(String),
68 Phase(PhaseMarkerError),
70}
71
72impl From<CaptureError> for CaptureOrderError {
73 fn from(error: CaptureError) -> Self {
75 return Self::Store(error);
76 }
77}
78
79impl From<GitFailure> for CaptureOrderError {
80 fn from(error: GitFailure) -> Self {
82 return Self::Git(error);
83 }
84}
85
86impl std::fmt::Display for CaptureOrderError {
87 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 return match self {
90 Self::Store(error) => formatter.write_str(error.0.as_str()),
91 Self::Lock(error) => write!(formatter, "{error}"),
92 Self::Git(error) => write!(formatter, "{error}"),
93 Self::Write(text) => formatter.write_str(text.as_str()),
94 Self::Phase(error) => write!(formatter, "{error}"),
95 };
96 }
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct CaptureStamp {
111 pub worktree_id: String,
113 pub sequence: i64,
115}
116
117fn ensure_worktree_id(store: &Path) -> Result<String, CaptureError> {
128 let path: PathBuf = store.join(WORKTREE_ID_FILENAME);
129 if let Some(text) = read_store_file(path.as_path())? {
130 let recorded: &str = trim_javascript(text.as_str());
131 if recorded.is_empty() {
132 return Err(CaptureError(format!(
133 "Capture store worktree identity is empty: {}",
134 path.display()
135 )));
136 }
137 return Ok(String::from(recorded));
138 }
139 let identity: String = random_uuid().map_err(|error| { return CaptureError(error.0); })?;
140 write_private_file(path.as_path(), format!("{identity}\n").as_bytes())
141 .map_err(|error| { return CaptureError(format!("writing {} failed: {error}", path.display())); })?;
142 sync_directory(store)
143 .map_err(|error| { return CaptureError(format!("syncing {} failed: {error}", store.display())); })?;
144 return Ok(identity);
145}
146
147pub fn write_sequence(store: &Path, sequence: i64) -> Result<(), CaptureError> {
158 let temporary: PathBuf = store.join(format!(
159 "{SEQUENCE_FILENAME}.{}.tmp",
160 random_uuid().map_err(|error| { return CaptureError(error.0); })?
161 ));
162 let written: Result<(), CaptureError> = write_private_file(
163 temporary.as_path(),
164 format!("{sequence}\n").as_bytes(),
165 )
166 .map_err(|error| { return CaptureError(format!("writing {} failed: {error}", temporary.display())); })
167 .and_then(|()| {
168 return std::fs::rename(&temporary, store.join(SEQUENCE_FILENAME)).map_err(|error| {
169 return CaptureError(format!("renaming {} failed: {error}", temporary.display()));
170 });
171 });
172 if let Err(error) = written {
173 let _ = std::fs::remove_file(&temporary);
175 return Err(error);
176 }
177 return sync_directory(store)
178 .map_err(|error| { return CaptureError(format!("syncing {} failed: {error}", store.display())); });
179}
180
181fn allocate_sequence(git_dir: &Path, store: &Path) -> Result<i64, CaptureError> {
192 let sequence: i64 = read_next_capture_sequence(git_dir)?;
193 write_sequence(store, sequence)?;
194 return Ok(sequence);
195}
196
197pub fn capture_in_order<Value, Failure: From<CaptureOrderError>>(
208 git_dir: &Path,
209 capture: impl FnOnce() -> Result<Value, Failure>,
210) -> Result<(CaptureStamp, Value), Failure> {
211 let store: PathBuf = ensure_capture_store(git_dir).map_err(CaptureOrderError::from)?;
212 let lock = acquire_live_owner_lock(store.join(CAPTURE_LOCK_NAME).as_path(), "the capture lock")
214 .map_err(CaptureOrderError::Lock)?;
215 reach_transaction_phase(Phase::CaptureLocked, None).map_err(CaptureOrderError::Phase)?;
217 let worktree_id: String = ensure_worktree_id(store.as_path()).map_err(CaptureOrderError::from)?;
218 let sequence: i64 = allocate_sequence(git_dir, store.as_path()).map_err(CaptureOrderError::from)?;
219 let value: Value = capture()?;
220 drop(lock);
221 return Ok((
222 CaptureStamp {
223 worktree_id,
224 sequence,
225 },
226 value,
227 ));
228}
229
230pub fn write_captured_record(directory: &Path, record: &CapturedRecord) -> Result<(), CaptureOrderError> {
241 return write_exclusive_synced(
242 directory.join(CAPTURED_FILENAME).as_path(),
243 encode_captured_record(record).as_bytes(),
244 )
245 .map_err(|error| {
246 return CaptureOrderError::Write(format!(
247 "writing {} failed: {error}",
248 directory.join(CAPTURED_FILENAME).display()
249 ));
250 });
251}
252
253fn latin1(bytes: &[u8]) -> String {
265 return bytes.iter().map(|byte| { return char::from(*byte); }).collect();
266}
267
268fn nul_records(output: &[u8]) -> Vec<&[u8]> {
278 return output
279 .split(|byte| { return *byte == 0; })
280 .filter(|record| { return !record.is_empty(); })
281 .collect();
282}
283
284#[derive(Clone, Debug, Eq, PartialEq)]
295pub struct CaptureIndexes {
296 pub captured_index: PathBuf,
298 pub commit_index: PathBuf,
300 pub object_directory: PathBuf,
302}
303
304pub fn indexed_request<S: AsRef<OsStr>>(
315 cwd: &Path,
316 arguments: &[S],
317 index: Option<&Path>,
318 object_directory: Option<&Path>,
319) -> GitRequest {
320 let mut request: GitRequest = GitRequest::new(cwd, arguments);
321 request.without_prefix = true;
322 request.index = index.map(Path::to_path_buf);
323 request.object_directory = object_directory.map(Path::to_path_buf);
324 return request;
325}
326
327fn staged_entries(
338 context: &GitContext,
339 cwd: &Path,
340 index: &Path,
341) -> Result<BTreeMap<Vec<u8>, String>, GitFailure> {
342 let output: GitOutput = run_git_checked(
343 context,
344 &indexed_request(cwd, &["ls-files", "--stage", "-z"], Some(index), None),
345 )?;
346 let mut entries: BTreeMap<Vec<u8>, String> = BTreeMap::new();
347 for record in nul_records(output.stdout.as_slice()) {
348 let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
350 return Err(GitFailure(String::from("git ls-files --stage returned an unparsable record.")));
351 };
352 let staged: &mut String = entries.entry(record[tab + 1..].to_vec()).or_default();
353 staged.push_str(latin1(&record[..tab]).as_str());
354 staged.push(';');
355 }
356 return Ok(entries);
357}
358
359pub fn list_worktree_captured_paths(
372 context: &GitContext,
373 cwd: &Path,
374 indexes: &CaptureIndexes,
375 mode: TransactionMode,
376 stages_worktree: bool,
377 base_revision: &str,
378) -> Result<Vec<String>, GitFailure> {
379 if mode == TransactionMode::ExplicitPath {
380 let output: GitOutput = run_git_checked(
381 context,
382 &indexed_request(
383 cwd,
384 &[
385 "diff-index",
386 "--cached",
387 "--name-only",
388 "-z",
389 "--no-renames",
390 base_revision,
391 ],
392 Some(indexes.commit_index.as_path()),
393 Some(indexes.object_directory.as_path()),
394 ),
395 )?;
396 return Ok(nul_records(output.stdout.as_slice())
397 .into_iter()
398 .map(latin1)
399 .collect());
400 }
401 if !stages_worktree {
402 return Ok(Vec::new());
403 }
404 let before: BTreeMap<Vec<u8>, String> =
405 staged_entries(context, cwd, indexes.captured_index.as_path())?;
406 let after: BTreeMap<Vec<u8>, String> =
407 staged_entries(context, cwd, indexes.commit_index.as_path())?;
408 let mut changed: BTreeSet<Vec<u8>> = BTreeSet::new();
410 for path in before.keys().chain(after.keys()) {
411 if before.get(path) != after.get(path) {
412 changed.insert(path.clone());
413 }
414 }
415 return Ok(changed.iter().map(|path| { return latin1(path.as_slice()); }).collect());
416}
417
418pub fn capture_commit_content(
429 context: &GitContext,
430 cwd: &Path,
431 git_dir: &Path,
432 directory: &Path,
433 indexes: &CaptureIndexes,
434 mode: TransactionMode,
435 stages_worktree: bool,
436 base_revision: &str,
437 next_sequence_before_base: i64,
438 capture: impl FnOnce() -> Result<(), CaptureOrderError>,
439) -> Result<CapturedRecord, CaptureOrderError> {
440 let (stamp, ()) = capture_in_order(git_dir, capture)?;
441 let worktree_paths: Vec<String> = list_worktree_captured_paths(
442 context,
443 cwd,
444 indexes,
445 mode,
446 stages_worktree,
447 base_revision,
448 )?;
449 let record: CapturedRecord = CapturedRecord {
450 worktree_id: stamp.worktree_id,
451 sequence: stamp.sequence,
452 next_sequence_before_base,
453 worktree_paths,
454 };
455 write_captured_record(directory, &record)?;
456 return Ok(record);
457}
458
459#[cfg(test)]
461#[path = "capture_order_tests.rs"]
462mod tests;