Add worktree settings parser (#40624)

## What changed

- Add the `codex-worktree` crate for resolving managed worktree settings from
  the existing `[desktop]` configuration.
- Default the worktree root to `$CODEX_HOME/worktrees`, automatic cleanup to
  enabled, and retained worktrees to 15.
- Validate configured roots, cleanup flags, and retention counts before
  exposing the effective settings.

## Testing

- Cover defaults, configured values, and invalid root and retention settings.

GitOrigin-RevId: 1b279c62ef41e3c8eb540ce9beaf0f6cd96d903f
This commit is contained in:
Benjamin Carlsson
2026-08-25 14:58:20 +00:00
committed by copyberry
parent 4b81410a80
commit f832b2fe7b
7 changed files with 202 additions and 0 deletions

11
codex-rs/Cargo.lock generated
View File

@@ -4844,6 +4844,17 @@ dependencies = [
"wiremock",
]
[[package]]
name = "codex-worktree"
version = "0.0.0"
dependencies = [
"anyhow",
"dunce",
"pretty_assertions",
"serde_json",
"tempfile",
]
[[package]]
name = "color-eyre"
version = "0.6.5"

View File

@@ -98,6 +98,7 @@ members = [
"tools",
"v8-poc",
"websocket-client",
"worktree",
"workload-identity",
"utils/absolute-path",
"utils/audio",

View File

@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "worktree",
crate_name = "codex_worktree",
)

View File

@@ -0,0 +1,21 @@
[package]
name = "codex-worktree"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[lib]
doctest = false
test = false
[dependencies]
anyhow = { workspace = true }
dunce = { workspace = true }
serde_json = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }

View File

@@ -0,0 +1,4 @@
mod settings;
pub use settings::DEFAULT_WORKTREE_KEEP_COUNT;
pub use settings::WorktreeSettings;

View File

@@ -0,0 +1,83 @@
//! Parses and validates worktree settings from the existing `[desktop]` config.
//!
//! Resolves the managed worktree root and the automatic-cleanup and retention
//! settings without introducing a separate configuration format.
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
const WORKTREE_ROOT: &str = "git-worktree-root";
const AUTO_CLEANUP: &str = "worktree-auto-cleanup-enabled";
const KEEP_COUNT: &str = "worktree-keep-count";
pub const DEFAULT_WORKTREE_KEEP_COUNT: usize = 15;
/// Effective host-local settings already understood by Codex Desktop.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WorktreeSettings {
pub root: PathBuf,
pub auto_cleanup_enabled: bool,
pub keep_count: usize,
}
impl WorktreeSettings {
/// Resolves existing `[desktop]` values without introducing another config format.
pub fn from_desktop_config(
codex_home: &Path,
desktop: Option<&HashMap<String, Value>>,
) -> Result<Self> {
let root = match desktop.and_then(|settings| settings.get(WORKTREE_ROOT)) {
None | Some(Value::Null) => codex_home.join("worktrees"),
Some(value) => {
let configured = value
.as_str()
.context("desktop.git-worktree-root must be a string")?
.trim();
if configured.is_empty() {
codex_home.join("worktrees")
} else {
let path = PathBuf::from(configured);
if !path.is_absolute() {
bail!("desktop.git-worktree-root must be an absolute path");
}
path
}
}
};
let auto_cleanup_enabled = desktop
.and_then(|settings| settings.get(AUTO_CLEANUP))
.map(|value| {
value
.as_bool()
.context("desktop.worktree-auto-cleanup-enabled must be a boolean")
})
.transpose()?
.unwrap_or(true);
let keep_count = desktop
.and_then(|settings| settings.get(KEEP_COUNT))
.map(|value| {
let count = value
.as_u64()
.context("desktop.worktree-keep-count must be a positive integer")?;
if count == 0 {
bail!("desktop.worktree-keep-count must be a positive integer");
}
usize::try_from(count).context("desktop.worktree-keep-count is too large")
})
.transpose()?
.unwrap_or(DEFAULT_WORKTREE_KEEP_COUNT);
Ok(Self {
root: dunce::simplified(&root).to_path_buf(),
auto_cleanup_enabled,
keep_count,
})
}
}

View File

@@ -0,0 +1,76 @@
#![allow(clippy::expect_used)]
use codex_worktree::WorktreeSettings;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use std::collections::HashMap;
fn desktop_config(entries: [(&str, Value); 3]) -> HashMap<String, Value> {
entries
.into_iter()
.map(|(key, value)| (key.to_owned(), value))
.collect()
}
#[test]
fn default_settings_match_desktop_worktree_defaults() {
let codex_home = tempfile::tempdir().expect("create Codex home");
let settings = WorktreeSettings::from_desktop_config(codex_home.path(), /*desktop*/ None)
.expect("load default worktree settings");
assert_eq!(
settings,
WorktreeSettings {
root: codex_home.path().join("worktrees"),
auto_cleanup_enabled: true,
keep_count: 15,
}
);
}
#[test]
fn desktop_settings_use_existing_root_and_retention_keys() {
let codex_home = tempfile::tempdir().expect("create Codex home");
let custom_root = codex_home.path().join("custom-managed-worktrees");
let desktop = desktop_config([
(
"git-worktree-root",
json!(custom_root.to_string_lossy().into_owned()),
),
("worktree-auto-cleanup-enabled", json!(false)),
("worktree-keep-count", json!(4)),
]);
let settings = WorktreeSettings::from_desktop_config(codex_home.path(), Some(&desktop))
.expect("load existing Desktop worktree settings");
assert_eq!(
settings,
WorktreeSettings {
root: custom_root,
auto_cleanup_enabled: false,
keep_count: 4,
}
);
}
#[test]
fn desktop_settings_reject_invalid_root_and_retention_values() {
let codex_home = tempfile::tempdir().expect("create Codex home");
let invalid_configs = [
HashMap::from([("git-worktree-root".to_owned(), json!("relative/worktrees"))]),
HashMap::from([("worktree-auto-cleanup-enabled".to_owned(), json!("yes"))]),
HashMap::from([("worktree-keep-count".to_owned(), json!(0))]),
HashMap::from([("worktree-keep-count".to_owned(), json!(-1))]),
HashMap::from([("worktree-keep-count".to_owned(), json!(1.5))]),
];
for desktop in invalid_configs {
assert!(
WorktreeSettings::from_desktop_config(codex_home.path(), Some(&desktop)).is_err(),
"invalid Desktop worktree settings were accepted: {desktop:?}",
);
}
}