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::owner_lock::{DEFAULT_POLL_DELAY, OwnerLockError, SilentWait, acquire_owner_lock};
35use super::random_id::random_uuid;
37use super::transaction_phase::{Phase, PhaseMarkerError, reach_transaction_phase};
39use super::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git_checked};
40use super::transaction_journal::TransactionMode;
42use super::js_text::trim_javascript;
44use std::path::{Path, PathBuf};
46use std::collections::{BTreeMap, BTreeSet};
48use std::ffi::OsStr;
50
51pub const CAPTURED_FILENAME: &str = "captured.json";
53
54#[derive(Debug)]
58pub enum CaptureOrderError {
59 Store(CaptureError),
61 Lock(OwnerLockError),
63 Git(GitFailure),
65 Write(String),
67 Phase(PhaseMarkerError),
69}
70
71impl From<CaptureError> for CaptureOrderError {
72 fn from(error: CaptureError) -> Self {
74 return Self::Store(error);
75 }
76}
77
78impl From<GitFailure> for CaptureOrderError {
79 fn from(error: GitFailure) -> Self {
81 return Self::Git(error);
82 }
83}
84
85impl std::fmt::Display for CaptureOrderError {
86 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 return match self {
89 Self::Store(error) => formatter.write_str(error.0.as_str()),
90 Self::Lock(error) => write!(formatter, "{error}"),
91 Self::Git(error) => write!(formatter, "{error}"),
92 Self::Write(text) => formatter.write_str(text.as_str()),
93 Self::Phase(error) => write!(formatter, "{error}"),
94 };
95 }
96}
97
98#[derive(Clone, Debug, Eq, PartialEq)]
109pub struct CaptureStamp {
110 pub worktree_id: String,
112 pub sequence: i64,
114}
115
116fn ensure_worktree_id(store: &Path) -> Result<String, CaptureError> {
127 let path: PathBuf = store.join(WORKTREE_ID_FILENAME);
128 if let Some(text) = read_store_file(path.as_path())? {
129 let recorded: &str = trim_javascript(text.as_str());
130 if recorded.is_empty() {
131 return Err(CaptureError(format!(
132 "Capture store worktree identity is empty: {}",
133 path.display()
134 )));
135 }
136 return Ok(String::from(recorded));
137 }
138 let identity: String = random_uuid().map_err(|error| { return CaptureError(error.0); })?;
139 write_private_file(path.as_path(), format!("{identity}\n").as_bytes())
140 .map_err(|error| { return CaptureError(format!("writing {} failed: {error}", path.display())); })?;
141 sync_directory(store)
142 .map_err(|error| { return CaptureError(format!("syncing {} failed: {error}", store.display())); })?;
143 return Ok(identity);
144}
145
146pub fn write_sequence(store: &Path, sequence: i64) -> Result<(), CaptureError> {
157 let temporary: PathBuf = store.join(format!(
158 "{SEQUENCE_FILENAME}.{}.tmp",
159 random_uuid().map_err(|error| { return CaptureError(error.0); })?
160 ));
161 let written: Result<(), CaptureError> = write_private_file(
162 temporary.as_path(),
163 format!("{sequence}\n").as_bytes(),
164 )
165 .map_err(|error| { return CaptureError(format!("writing {} failed: {error}", temporary.display())); })
166 .and_then(|()| {
167 return std::fs::rename(&temporary, store.join(SEQUENCE_FILENAME)).map_err(|error| {
168 return CaptureError(format!("renaming {} failed: {error}", temporary.display()));
169 });
170 });
171 if let Err(error) = written {
172 let _ = std::fs::remove_file(&temporary);
174 return Err(error);
175 }
176 return sync_directory(store)
177 .map_err(|error| { return CaptureError(format!("syncing {} failed: {error}", store.display())); });
178}
179
180fn allocate_sequence(git_dir: &Path, store: &Path) -> Result<i64, CaptureError> {
191 let sequence: i64 = read_next_capture_sequence(git_dir)?;
192 write_sequence(store, sequence)?;
193 return Ok(sequence);
194}
195
196pub fn capture_in_order<Value, Failure: From<CaptureOrderError>>(
207 git_dir: &Path,
208 capture: impl FnOnce() -> Result<Value, Failure>,
209) -> Result<(CaptureStamp, Value), Failure> {
210 let store: PathBuf = ensure_capture_store(git_dir).map_err(CaptureOrderError::from)?;
211 let lock = acquire_owner_lock(
213 store.join(CAPTURE_LOCK_NAME).as_path(),
214 DEFAULT_POLL_DELAY,
215 &mut SilentWait,
216 )
217 .map_err(CaptureOrderError::Lock)?;
218 reach_transaction_phase(Phase::CaptureLocked, None).map_err(CaptureOrderError::Phase)?;
220 let worktree_id: String = ensure_worktree_id(store.as_path()).map_err(CaptureOrderError::from)?;
221 let sequence: i64 = allocate_sequence(git_dir, store.as_path()).map_err(CaptureOrderError::from)?;
222 let value: Value = capture()?;
223 drop(lock);
224 return Ok((
225 CaptureStamp {
226 worktree_id,
227 sequence,
228 },
229 value,
230 ));
231}
232
233pub fn write_captured_record(directory: &Path, record: &CapturedRecord) -> Result<(), CaptureOrderError> {
244 return write_exclusive_synced(
245 directory.join(CAPTURED_FILENAME).as_path(),
246 encode_captured_record(record).as_bytes(),
247 )
248 .map_err(|error| {
249 return CaptureOrderError::Write(format!(
250 "writing {} failed: {error}",
251 directory.join(CAPTURED_FILENAME).display()
252 ));
253 });
254}
255
256fn latin1(bytes: &[u8]) -> String {
268 return bytes.iter().map(|byte| { return char::from(*byte); }).collect();
269}
270
271fn nul_records(output: &[u8]) -> Vec<&[u8]> {
281 return output
282 .split(|byte| { return *byte == 0; })
283 .filter(|record| { return !record.is_empty(); })
284 .collect();
285}
286
287#[derive(Clone, Debug, Eq, PartialEq)]
298pub struct CaptureIndexes {
299 pub captured_index: PathBuf,
301 pub commit_index: PathBuf,
303 pub object_directory: PathBuf,
305}
306
307pub fn indexed_request<S: AsRef<OsStr>>(
318 cwd: &Path,
319 arguments: &[S],
320 index: Option<&Path>,
321 object_directory: Option<&Path>,
322) -> GitRequest {
323 let mut request: GitRequest = GitRequest::new(cwd, arguments);
324 request.without_prefix = true;
325 request.index = index.map(Path::to_path_buf);
326 request.object_directory = object_directory.map(Path::to_path_buf);
327 return request;
328}
329
330fn staged_entries(
341 context: &GitContext,
342 cwd: &Path,
343 index: &Path,
344) -> Result<BTreeMap<Vec<u8>, String>, GitFailure> {
345 let output: GitOutput = run_git_checked(
346 context,
347 &indexed_request(cwd, &["ls-files", "--stage", "-z"], Some(index), None),
348 )?;
349 let mut entries: BTreeMap<Vec<u8>, String> = BTreeMap::new();
350 for record in nul_records(output.stdout.as_slice()) {
351 let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
353 return Err(GitFailure(String::from("git ls-files --stage returned an unparsable record.")));
354 };
355 let staged: &mut String = entries.entry(record[tab + 1..].to_vec()).or_default();
356 staged.push_str(latin1(&record[..tab]).as_str());
357 staged.push(';');
358 }
359 return Ok(entries);
360}
361
362pub fn list_worktree_captured_paths(
375 context: &GitContext,
376 cwd: &Path,
377 indexes: &CaptureIndexes,
378 mode: TransactionMode,
379 stages_worktree: bool,
380 base_revision: &str,
381) -> Result<Vec<String>, GitFailure> {
382 if mode == TransactionMode::ExplicitPath {
383 let output: GitOutput = run_git_checked(
384 context,
385 &indexed_request(
386 cwd,
387 &[
388 "diff-index",
389 "--cached",
390 "--name-only",
391 "-z",
392 "--no-renames",
393 base_revision,
394 ],
395 Some(indexes.commit_index.as_path()),
396 Some(indexes.object_directory.as_path()),
397 ),
398 )?;
399 return Ok(nul_records(output.stdout.as_slice())
400 .into_iter()
401 .map(latin1)
402 .collect());
403 }
404 if !stages_worktree {
405 return Ok(Vec::new());
406 }
407 let before: BTreeMap<Vec<u8>, String> =
408 staged_entries(context, cwd, indexes.captured_index.as_path())?;
409 let after: BTreeMap<Vec<u8>, String> =
410 staged_entries(context, cwd, indexes.commit_index.as_path())?;
411 let mut changed: BTreeSet<Vec<u8>> = BTreeSet::new();
413 for path in before.keys().chain(after.keys()) {
414 if before.get(path) != after.get(path) {
415 changed.insert(path.clone());
416 }
417 }
418 return Ok(changed.iter().map(|path| { return latin1(path.as_slice()); }).collect());
419}
420
421pub fn capture_commit_content(
432 context: &GitContext,
433 cwd: &Path,
434 git_dir: &Path,
435 directory: &Path,
436 indexes: &CaptureIndexes,
437 mode: TransactionMode,
438 stages_worktree: bool,
439 base_revision: &str,
440 next_sequence_before_base: i64,
441 capture: impl FnOnce() -> Result<(), CaptureOrderError>,
442) -> Result<CapturedRecord, CaptureOrderError> {
443 let (stamp, ()) = capture_in_order(git_dir, capture)?;
444 let worktree_paths: Vec<String> = list_worktree_captured_paths(
445 context,
446 cwd,
447 indexes,
448 mode,
449 stages_worktree,
450 base_revision,
451 )?;
452 let record: CapturedRecord = CapturedRecord {
453 worktree_id: stamp.worktree_id,
454 sequence: stamp.sequence,
455 next_sequence_before_base,
456 worktree_paths,
457 };
458 write_captured_record(directory, &record)?;
459 return Ok(record);
460}
461
462#[cfg(test)]
464#[path = "capture_order_tests.rs"]
465mod tests;