workdir: report current selector and ref

This commit is contained in:
2026-07-31 23:48:46 +09:00
parent 32d3f8f75e
commit 06a98fe30c
15 changed files with 395 additions and 86 deletions
+11 -5
View File
@@ -143,14 +143,20 @@ pub struct WorkingDirectoryOccupancy {
pub struct WorkingDirectorySummary {
pub working_directory_id: String,
pub repository_id: String,
/// Selector used to create this Workdir, retained as immutable materialization evidence.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested_selector: Option<String>,
pub creation_selector: Option<String>,
/// Provider-specific immutable ref resolved when this Workdir was created.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creation_ref: Option<String>,
/// Selector currently observed from the materialized Workdir, when one exists.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_selector: Option<String>,
/// Provider-specific immutable ref currently observed from the materialized Workdir.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_ref: Option<String>,
pub materializer_kind: MaterializerKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_tree: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
pub status: WorkingDirectoryStatusKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
+56 -8
View File
@@ -40,10 +40,11 @@ impl WorkingDirectory {
WorkingDirectorySummary {
working_directory_id: self.id.clone(),
repository_id: self.repository_id.clone(),
requested_selector: self.evidence.requested_selector.clone(),
creation_selector: self.evidence.requested_selector.clone(),
creation_ref: Some(self.evidence.resolved_commit.clone()),
current_selector: None,
current_ref: None,
materializer_kind: self.materializer_kind.clone(),
resolved_commit: Some(self.evidence.resolved_commit.clone()),
resolved_tree: self.evidence.resolved_tree.clone(),
cleanup_target: Some(self.cleanup_target.clone()),
status: self.status.clone(),
cleanliness: None,
@@ -88,6 +89,9 @@ impl WorkingDirectoryBinding {
}
let mut summary = working_directory.status_summary();
summary.cleanliness = if summary.status == WorkingDirectoryStatusKind::Active {
let (current_selector, current_ref) = binding_current_revision(self);
summary.current_selector = current_selector;
summary.current_ref = current_ref;
Some(binding_cleanliness(self))
} else {
Some("unknown".to_string())
@@ -171,6 +175,22 @@ fn binding_paths_are_available(binding: &WorkingDirectoryBinding) -> bool {
source_repository_path.is_dir()
}
fn binding_current_revision(binding: &WorkingDirectoryBinding) -> (Option<String>, Option<String>) {
let current_ref = git_stdout(binding.root(), ["rev-parse", "HEAD"])
.ok()
.filter(|value| !value.is_empty());
if current_ref.is_none() {
return (None, None);
}
let current_selector = git_stdout(
binding.root(),
["symbolic-ref", "--short", "--quiet", "HEAD"],
)
.ok()
.filter(|value| !value.is_empty());
(current_selector, current_ref)
}
fn binding_cleanliness(binding: &WorkingDirectoryBinding) -> String {
match git_stdout(binding.root(), ["status", "--porcelain"]) {
Ok(output) if output.is_empty() => "clean".to_string(),
@@ -208,10 +228,11 @@ impl LocalGitWorktreeMaterializer {
summary: WorkingDirectorySummary {
working_directory_id: working_directory_id.to_string(),
repository_id: "unknown".to_string(),
requested_selector: None,
creation_selector: None,
creation_ref: None,
current_selector: None,
current_ref: None,
materializer_kind: MaterializerKind::LocalGitWorktree,
resolved_commit: None,
resolved_tree: None,
cleanup_target: Some(WorkingDirectoryCleanupTarget {
kind: "local_git_worktree".to_string(),
working_directory_id: working_directory_id.to_string(),
@@ -930,12 +951,39 @@ mod tests {
listed[0].summary.working_directory_id,
working_directory.working_directory.id
);
assert_eq!(listed[0].summary.creation_selector.as_deref(), Some("HEAD"));
assert_eq!(listed[0].summary.current_selector, None);
assert_eq!(
listed[0].summary.requested_selector.as_deref(),
Some("HEAD")
listed[0].summary.current_ref,
listed[0].summary.creation_ref
);
}
#[test]
fn working_directory_observes_current_selector_and_ref_without_changing_creation_evidence() {
let repo = create_clean_repo();
let runtime_root = tempfile::tempdir().unwrap();
let materializer = LocalGitWorktreeMaterializer::new(runtime_root.path());
let working_directory = materializer.create(&request(repo.path())).unwrap();
let bound = materializer
.bind_working_directory(&working_directory.working_directory.id, None)
.unwrap();
let initial_ref = bound.status().summary.creation_ref.expect("creation ref");
git(&bound.root, &["switch", "-c", "observed-branch"]);
fs::write(bound.root.join("observed.txt"), "observed\n").unwrap();
git(&bound.root, &["add", "observed.txt"]);
git(&bound.root, &["commit", "-m", "advance workdir"]);
let summary = materializer.list_working_directories().unwrap()[0]
.summary
.clone();
assert_eq!(summary.creation_selector.as_deref(), Some("HEAD"));
assert_eq!(summary.creation_ref.as_deref(), Some(initial_ref.as_str()));
assert_eq!(summary.current_selector.as_deref(), Some("observed-branch"));
assert_ne!(summary.current_ref.as_deref(), Some(initial_ref.as_str()));
}
#[test]
fn relative_cwd_rejects_absolute_parent_nonexistent_file_and_symlink_escape() {
let repo = create_clean_repo();