Skip to content
272 changes: 266 additions & 6 deletions cli/src/services/checkout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
//!
//! Checkout identity is repository metadata used by Agent Trace diagnostics.

use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;

Expand All @@ -19,6 +21,9 @@ const SCE_CHECKOUT_DIR: &str = "sce";
/// File name for the checkout ID inside `<git-dir>/sce/`.
const CHECKOUT_ID_FILE: &str = "checkout-id";

/// File name for the identity-creation lock inside `<git-dir>/sce/`.
const CHECKOUT_ID_LOCK_FILE: &str = "checkout-id.lock";

/// Resolves the Git directory (`.git` for normal clones, or the worktree-specific
/// path for linked worktrees) by running `git rev-parse --git-dir` from the
/// given repository root.
Expand Down Expand Up @@ -105,14 +110,13 @@ pub fn read_checkout_id(git_dir: &Path) -> Result<Option<String>> {
/// Gets the existing checkout ID or creates a new one.
///
/// If `<git_dir>/sce/checkout-id` already exists, returns the stored ID (idempotent).
/// If it does not exist, generates a new `UUIDv7`, writes it to the file, and returns it.
/// If it does not exist, acquires `<git_dir>/sce/checkout-id.lock`, generates a new
/// `UUIDv7`, writes it through a temporary file and an atomic rename, and returns it.
pub fn get_or_create_checkout_id(git_dir: &Path) -> Result<String> {
if let Some(existing_id) = read_checkout_id(git_dir)? {
return Ok(existing_id);
}

let checkout_id = Uuid::now_v7().to_string();

let checkout_dir = git_dir.join(SCE_CHECKOUT_DIR);
std::fs::create_dir_all(&checkout_dir).with_context(|| {
format!(
Expand All @@ -121,13 +125,269 @@ pub fn get_or_create_checkout_id(git_dir: &Path) -> Result<String> {
)
})?;

let lock_path = checkout_dir.join(CHECKOUT_ID_LOCK_FILE);
let lock_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)
.with_context(|| {
format!(
"Failed to open checkout ID lock file '{}'",
lock_path.display()
)
})?;
lock_file.lock().with_context(|| {
format!(
"Failed to acquire checkout ID lock '{}'",
lock_path.display()
)
})?;

if let Some(existing_id) = read_checkout_id(git_dir)? {
return Ok(existing_id);
}

let checkout_id = Uuid::now_v7().to_string();
persist_checkout_id(&checkout_dir, &checkout_id)?;

Ok(checkout_id)
}

fn persist_checkout_id(checkout_dir: &Path, checkout_id: &str) -> Result<()> {
persist_checkout_id_inner(checkout_dir, checkout_id, |_, _| Ok(()))
}

fn persist_checkout_id_inner<F>(
checkout_dir: &Path,
checkout_id: &str,
before_rename: F,
) -> Result<()>
where
F: FnOnce(&Path, &Path) -> Result<()>,
{
let checkout_id_path = checkout_dir.join(CHECKOUT_ID_FILE);
std::fs::write(&checkout_id_path, &checkout_id).with_context(|| {
let tmp_path = checkout_dir.join(format!("checkout-id.tmp-{checkout_id}"));

let mut tmp_file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp_path)
.with_context(|| {
format!(
"Failed to create temporary checkout ID file '{}'",
tmp_path.display()
)
})?;
tmp_file
.write_all(checkout_id.as_bytes())
.with_context(|| {
format!(
"Failed to write checkout ID to temporary file '{}'",
tmp_path.display()
)
})?;
tmp_file.sync_data().with_context(|| {
format!(
"Failed to sync temporary checkout ID file '{}'",
tmp_path.display()
)
})?;
drop(tmp_file);

before_rename(&tmp_path, &checkout_id_path)?;

std::fs::rename(&tmp_path, &checkout_id_path).with_context(|| {
format!(
"Failed to write checkout ID to '{}'",
"Failed to rename '{}' to '{}'",
tmp_path.display(),
checkout_id_path.display()
)
})?;

Ok(checkout_id)
#[cfg(unix)]
{
if let Ok(dir) = std::fs::File::open(checkout_dir) {
let _ = dir.sync_all();
}
}

Ok(())
}

#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

use super::*;

static NEXT_TEST_GIT_DIR_ID: AtomicU64 = AtomicU64::new(0);

fn unique_test_git_dir(label: &str) -> PathBuf {
let id = NEXT_TEST_GIT_DIR_ID.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"sce-checkout-identity-{label}-{}-{id}",
std::process::id()
))
}

fn remove_test_git_dir(git_dir: &Path) {
let _ = std::fs::remove_dir_all(git_dir);
}

#[test]
fn concurrent_first_time_callers_converge_on_one_checkout_id() {
let git_dir = unique_test_git_dir("concurrent-first-time");
std::fs::create_dir_all(&git_dir).expect("git dir should be created");

let handles: Vec<_> = (0..8)
.map(|_| {
let git_dir = git_dir.clone();
thread::spawn(move || {
get_or_create_checkout_id(&git_dir).expect("checkout id should be created")
})
})
.collect();

let ids: Vec<String> = handles
.into_iter()
.map(|handle| handle.join().expect("thread should not panic"))
.collect();

let first = ids[0].clone();
assert!(
ids.iter().all(|id| *id == first),
"all concurrent callers should converge on one checkout id, got {ids:?}"
);

let persisted =
read_checkout_id(&git_dir).expect("checkout id should be readable after creation");
assert_eq!(persisted, Some(first));

remove_test_git_dir(&git_dir);
}

#[test]
fn already_created_checkout_id_is_read_without_acquiring_lock() {
let git_dir = unique_test_git_dir("fast-path-no-lock");
let checkout_dir = git_dir.join(SCE_CHECKOUT_DIR);
std::fs::create_dir_all(&checkout_dir).expect("checkout dir should be created");

let existing_id = Uuid::now_v7().to_string();
std::fs::write(checkout_dir.join(CHECKOUT_ID_FILE), &existing_id)
.expect("seeded checkout id should be written");

let lock_path = checkout_dir.join(CHECKOUT_ID_LOCK_FILE);
let lock_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)
.expect("lock file should be created");
lock_file.lock().expect("lock should be acquired");

let (tx, rx) = mpsc::channel();
let git_dir_clone = git_dir.clone();
thread::spawn(move || {
let result = get_or_create_checkout_id(&git_dir_clone);
let _ = tx.send(result);
});

let result = rx
.recv_timeout(Duration::from_millis(500))
.expect("fast path should not block on the identity-creation lock");
assert_eq!(result.expect("existing id should be read"), existing_id);

drop(lock_file);
remove_test_git_dir(&git_dir);
}

#[test]
fn completed_rename_leaves_the_canonical_path_with_a_complete_id() {
let git_dir = unique_test_git_dir("completed-rename");
let checkout_dir = git_dir.join(SCE_CHECKOUT_DIR);
std::fs::create_dir_all(&checkout_dir).expect("checkout dir should be created");

let checkout_id = Uuid::now_v7().to_string();
persist_checkout_id(&checkout_dir, &checkout_id)
.expect("persistence should succeed without an injected interruption");

let persisted = read_checkout_id(&git_dir)
.expect("checkout id should be readable")
.expect("checkout id should exist");
assert_eq!(persisted, checkout_id);
Uuid::parse_str(&persisted).expect("persisted id should be a complete, valid UUID");

remove_test_git_dir(&git_dir);
}

#[test]
fn interruption_before_rename_leaves_the_canonical_path_absent() {
let git_dir = unique_test_git_dir("interrupted-before-rename");
let checkout_dir = git_dir.join(SCE_CHECKOUT_DIR);
std::fs::create_dir_all(&checkout_dir).expect("checkout dir should be created");

let checkout_id = Uuid::now_v7().to_string();
let result =
persist_checkout_id_inner(&checkout_dir, &checkout_id, |tmp_path, canonical_path| {
assert!(
tmp_path.exists(),
"temp file should exist by the time the pre-rename hook runs"
);
assert_eq!(
std::fs::read_to_string(tmp_path).expect("temp file should be readable"),
checkout_id,
"temp file should already contain the complete id before the injected interruption"
);
assert!(
!canonical_path.exists(),
"canonical path should still be absent at the pre-rename hook"
);
Err(anyhow!("injected interruption before rename"))
});

assert!(
result.is_err(),
"persistence should surface the injected pre-rename interruption"
);

let canonical_path = checkout_dir.join(CHECKOUT_ID_FILE);
assert!(
!canonical_path.exists(),
"canonical checkout-id path must stay absent when interrupted before rename"
);
assert_eq!(
read_checkout_id(&git_dir).expect("read should not error on an absent canonical id"),
None
);

remove_test_git_dir(&git_dir);
}

#[test]
fn an_orphaned_temp_file_does_not_block_convergence_on_the_canonical_id() {
let git_dir = unique_test_git_dir("orphaned-temp-file");
let checkout_dir = git_dir.join(SCE_CHECKOUT_DIR);
std::fs::create_dir_all(&checkout_dir).expect("checkout dir should be created");

let orphaned_tmp_path = checkout_dir.join(format!("checkout-id.tmp-{}", Uuid::now_v7()));
std::fs::write(&orphaned_tmp_path, b"orphaned-partial-content")
.expect("orphaned temp file should be written");

let checkout_id = get_or_create_checkout_id(&git_dir)
.expect("orphaned temp file should not block id creation");
Uuid::parse_str(&checkout_id).expect("returned id should be a complete, valid UUID");

let persisted = read_checkout_id(&git_dir)
.expect("checkout id should be readable")
.expect("checkout id should exist");
assert_eq!(persisted, checkout_id);

assert!(orphaned_tmp_path.exists());

remove_test_git_dir(&git_dir);
}
}
1 change: 1 addition & 0 deletions cli/src/services/mutation_trace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
//! unchanged).

pub mod protocol;
pub(crate) mod runtime;
pub mod store;
pub mod types;

Expand Down
Loading
Loading