Compare commits

...

1 Commits

Author SHA1 Message Date
starr-openai
d70c05423b Add startup worktree MVP 2026-04-01 20:45:26 -07:00
6 changed files with 649 additions and 17 deletions

View File

@@ -63,6 +63,10 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
/// Create a new local git worktree and start the session from that checkout.
#[arg(long = "worktree", default_value_t = false, global = true)]
pub worktree: bool,
/// Allow running Codex outside a Git repository.
#[arg(long = "skip-git-repo-check", global = true, default_value_t = false)]
pub skip_git_repo_check: bool,
@@ -312,4 +316,13 @@ mod tests {
assert_eq!(args.session_id.as_deref(), Some("session-123"));
assert_eq!(args.prompt.as_deref(), Some(PROMPT));
}
#[test]
fn parse_worktree_flag() {
let cli = Cli::parse_from(["codex-exec", "--worktree", "--cd", "/tmp/repo", "hello"]);
assert!(cli.worktree);
assert_eq!(cli.cwd, Some(PathBuf::from("/tmp/repo")));
assert_eq!(cli.prompt.as_deref(), Some("hello"));
}
}

View File

@@ -66,6 +66,9 @@ use codex_core::config_loader::format_config_error_with_source;
use codex_core::format_exec_policy_error_with_source;
use codex_core::path_utils;
use codex_feedback::CodexFeedback;
use codex_git_utils::CodexManagedWorktree;
use codex_git_utils::GitToolingError;
use codex_git_utils::create_codex_managed_worktree;
use codex_git_utils::get_git_repo_root;
use codex_otel::set_parent_from_context;
use codex_otel::traceparent_context_from_env;
@@ -165,6 +168,14 @@ struct ExecRunArgs {
stderr_with_ansi: bool,
}
#[derive(Debug, PartialEq, Eq)]
struct StartupCwd {
resolved_cwd: Option<PathBuf>,
config_cwd: AbsolutePathBuf,
}
type WorktreeCreator = fn(&Path, &Path) -> Result<CodexManagedWorktree, GitToolingError>;
fn exec_root_span() -> tracing::Span {
info_span!(
"codex.exec",
@@ -174,6 +185,33 @@ fn exec_root_span() -> tracing::Span {
)
}
fn resolve_startup_cwd(
requested_cwd: Option<PathBuf>,
codex_home: &Path,
worktree_creator: Option<WorktreeCreator>,
) -> anyhow::Result<StartupCwd> {
let config_cwd = match requested_cwd.as_deref() {
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
None => AbsolutePathBuf::current_dir()?,
};
let Some(worktree_creator) = worktree_creator else {
return Ok(StartupCwd {
resolved_cwd: requested_cwd,
config_cwd,
});
};
let worktree = worktree_creator(config_cwd.as_path(), codex_home)
.map_err(|err| anyhow::anyhow!("failed to create worktree: {err}"))?;
let config_cwd = AbsolutePathBuf::from_absolute_path(&worktree.worktree_workspace_root)?;
Ok(StartupCwd {
resolved_cwd: Some(worktree.worktree_workspace_root),
config_cwd,
})
}
pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
if let Err(err) = set_default_originator("codex_exec".to_string()) {
tracing::warn!(?err, "Failed to set codex exec originator override {err:?}");
@@ -189,6 +227,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
full_auto,
dangerously_bypass_approvals_and_sandbox,
cwd,
worktree,
skip_git_repo_check,
add_dir,
ephemeral,
@@ -201,6 +240,14 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
config_overrides,
} = cli;
// Fail no-prompt stdin reads before creating a detached worktree so
// `codex-exec --worktree </dev/null` does not leak a checkout.
let prompt = if worktree && command.is_none() && prompt.is_none() {
Some(resolve_root_prompt(None))
} else {
prompt
};
let (_stdout_with_ansi, stderr_with_ansi) = match color {
cli::Color::Always => (true, true),
cli::Color::Never => (false, false),
@@ -240,12 +287,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
}
};
let resolved_cwd = cwd.clone();
let config_cwd = match resolved_cwd.as_deref() {
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
None => AbsolutePathBuf::current_dir()?,
};
// we load config.toml here to determine project state.
#[allow(clippy::print_stderr)]
let codex_home = match find_codex_home() {
@@ -256,6 +297,15 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
}
};
let StartupCwd {
resolved_cwd,
config_cwd,
} = resolve_startup_cwd(
cwd,
codex_home.as_path(),
worktree.then_some(create_codex_managed_worktree),
)?;
#[allow(clippy::print_stderr)]
let config_toml = match load_config_as_toml_with_cli_overrides(
&codex_home,
@@ -1699,6 +1749,67 @@ mod tests {
);
}
#[test]
fn resolve_startup_cwd_uses_requested_cwd_without_worktree() {
let codex_home = tempdir().expect("create temp codex home");
let cwd = tempdir().expect("create temp cwd");
let startup_cwd = resolve_startup_cwd(
Some(cwd.path().to_path_buf()),
codex_home.path(),
/*worktree_creator*/ None,
)
.expect("resolve startup cwd");
assert_eq!(
startup_cwd,
StartupCwd {
resolved_cwd: Some(cwd.path().to_path_buf()),
config_cwd: AbsolutePathBuf::from_absolute_path(
cwd.path().canonicalize().expect("canonicalize cwd")
)
.expect("absolute cwd"),
}
);
}
#[test]
fn resolve_startup_cwd_uses_worktree_workspace_root_when_enabled() {
let codex_home = tempdir().expect("create temp codex home");
let cwd = tempdir().expect("create temp cwd");
let startup_cwd = resolve_startup_cwd(
Some(cwd.path().to_path_buf()),
codex_home.path(),
Some(|source_cwd, codex_home| {
let worktree_git_root = codex_home.join("worktrees/fake/project");
let worktree_git_dir = worktree_git_root.join(".git");
let marker_path = worktree_git_dir.join("codex-managed");
Ok(CodexManagedWorktree {
source_cwd: source_cwd.to_path_buf(),
source_repo_root: source_cwd.to_path_buf(),
worktree_git_root: worktree_git_root.clone(),
worktree_git_dir,
worktree_workspace_root: worktree_git_root.join("nested/path"),
starting_ref: "main".to_string(),
marker_path,
})
}),
)
.expect("resolve startup cwd");
let expected_worktree_workspace_root =
codex_home.path().join("worktrees/fake/project/nested/path");
assert_eq!(
startup_cwd,
StartupCwd {
resolved_cwd: Some(expected_worktree_workspace_root.clone()),
config_cwd: AbsolutePathBuf::from_absolute_path(&expected_worktree_workspace_root)
.expect("absolute worktree cwd"),
}
);
}
#[test]
fn builds_uncommitted_review_request() {
let args = ReviewArgs {

View File

@@ -8,6 +8,7 @@ mod ghost_commits;
mod info;
mod operations;
mod platform;
mod worktree;
pub use apply::ApplyGitRequest;
pub use apply::ApplyGitResult;
@@ -49,6 +50,8 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
pub use worktree::CodexManagedWorktree;
pub use worktree::create_codex_managed_worktree;
type CommitID = String;

View File

@@ -0,0 +1,384 @@
use std::collections::hash_map::DefaultHasher;
use std::ffi::OsString;
use std::fs;
use std::hash::Hash;
use std::hash::Hasher;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::SystemTime;
use crate::GitToolingError;
use crate::operations::ensure_git_repository;
use crate::operations::repo_subdir;
use crate::operations::resolve_head;
use crate::operations::resolve_repository_root;
use crate::operations::run_git_for_status;
use crate::operations::run_git_for_stdout;
static WORKTREE_BUCKET_COUNTER: AtomicU64 = AtomicU64::new(0);
pub const CODEX_MANAGED_WORKTREE_MARKER_FILE: &str = "codex-managed";
/// Metadata for a detached worktree created under `$CODEX_HOME/worktrees`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodexManagedWorktree {
pub source_cwd: PathBuf,
pub source_repo_root: PathBuf,
pub worktree_git_root: PathBuf,
pub worktree_git_dir: PathBuf,
pub worktree_workspace_root: PathBuf,
pub starting_ref: String,
pub marker_path: PathBuf,
}
/// Creates a detached worktree for `source_cwd` and returns the mapped cwd
/// inside the new checkout.
pub fn create_codex_managed_worktree(
source_cwd: &Path,
codex_home: &Path,
) -> Result<CodexManagedWorktree, GitToolingError> {
ensure_git_repository(source_cwd)?;
let source_repo_root = resolve_repository_root(source_cwd)?;
let source_cwd = source_cwd.to_path_buf();
let relative_cwd = repo_subdir(&source_repo_root, &source_cwd);
let starting_ref = starting_ref_for_repo(source_repo_root.as_path())?;
let worktree_git_root = allocate_worktree_root(codex_home, source_repo_root.as_path())?;
create_worktree_checkout(
source_repo_root.as_path(),
&worktree_git_root,
&starting_ref,
)?;
let setup_result = setup_worktree_checkout(&worktree_git_root);
let (worktree_git_dir, marker_path) = match setup_result {
Ok(setup) => setup,
Err(err) => {
cleanup_worktree_checkout(source_repo_root.as_path(), &worktree_git_root);
return Err(err);
}
};
let worktree_workspace_root = match relative_cwd {
Some(relative) => worktree_git_root.join(relative),
None => worktree_git_root.clone(),
};
Ok(CodexManagedWorktree {
source_cwd,
source_repo_root,
worktree_git_root,
worktree_git_dir,
worktree_workspace_root,
starting_ref,
marker_path,
})
}
fn starting_ref_for_repo(repo_root: &Path) -> Result<String, GitToolingError> {
let branch = run_git_for_stdout(
repo_root,
vec![OsString::from("branch"), OsString::from("--show-current")],
None,
)?;
if !branch.is_empty() {
return Ok(branch);
}
match resolve_head(repo_root)? {
Some(head) => Ok(head),
None => Ok(String::from("HEAD")),
}
}
fn allocate_worktree_root(
codex_home: &Path,
source_repo_root: &Path,
) -> Result<PathBuf, GitToolingError> {
let repo_name = source_repo_root
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.unwrap_or("repo");
let worktrees_root = codex_home.join("worktrees");
fs::create_dir_all(&worktrees_root)?;
for _ in 0..64 {
let bucket = next_worktree_bucket(source_repo_root);
let candidate = worktrees_root.join(bucket).join(repo_name);
if candidate.exists() {
continue;
}
if let Some(parent) = candidate.parent() {
fs::create_dir_all(parent)?;
}
return Ok(candidate);
}
Err(GitToolingError::Io(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"unable to allocate a unique codex worktree path",
)))
}
fn next_worktree_bucket(source_repo_root: &Path) -> String {
let mut hasher = DefaultHasher::new();
source_repo_root.hash(&mut hasher);
std::process::id().hash(&mut hasher);
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.hash(&mut hasher);
WORKTREE_BUCKET_COUNTER
.fetch_add(1, Ordering::Relaxed)
.hash(&mut hasher);
format!("{:04x}", (hasher.finish() & 0xffff) as u16)
}
fn create_worktree_checkout(
source_repo_root: &Path,
worktree_git_root: &Path,
starting_ref: &str,
) -> Result<(), GitToolingError> {
let result = run_git_for_status(
source_repo_root,
vec![
OsString::from("worktree"),
OsString::from("add"),
OsString::from("--detach"),
OsString::from(worktree_git_root.as_os_str()),
OsString::from(starting_ref),
],
None,
);
if let Err(err) = result {
let _ = fs::remove_dir_all(worktree_git_root);
return Err(err);
}
Ok(())
}
fn setup_worktree_checkout(
worktree_git_root: &Path,
) -> Result<(PathBuf, PathBuf), GitToolingError> {
let worktree_git_dir = worktree_git_dir(worktree_git_root)?;
let marker_path = write_codex_managed_marker(&worktree_git_dir)?;
Ok((worktree_git_dir, marker_path))
}
fn cleanup_worktree_checkout(source_repo_root: &Path, worktree_git_root: &Path) {
let _ = run_git_for_status(
source_repo_root,
vec![
OsString::from("worktree"),
OsString::from("remove"),
OsString::from("--force"),
OsString::from(worktree_git_root.as_os_str()),
],
/*env*/ None,
);
let _ = fs::remove_dir_all(worktree_git_root);
}
fn write_codex_managed_marker(worktree_git_dir: &Path) -> Result<PathBuf, GitToolingError> {
let marker_path = worktree_git_dir.join(CODEX_MANAGED_WORKTREE_MARKER_FILE);
let mut marker = fs::File::create(&marker_path)?;
marker.write_all(b"codex-managed\n")?;
Ok(marker_path)
}
fn worktree_git_dir(worktree_git_root: &Path) -> Result<PathBuf, GitToolingError> {
let git_dir = run_git_for_stdout(
worktree_git_root,
vec![OsString::from("rev-parse"), OsString::from("--git-dir")],
None,
)?;
let git_dir = PathBuf::from(git_dir);
if git_dir.is_absolute() {
Ok(git_dir)
} else {
Ok(worktree_git_root.join(git_dir))
}
}
#[cfg(test)]
mod tests {
use super::CODEX_MANAGED_WORKTREE_MARKER_FILE;
use super::CodexManagedWorktree;
use super::allocate_worktree_root;
use super::cleanup_worktree_checkout;
use super::create_codex_managed_worktree;
use super::create_worktree_checkout;
use super::starting_ref_for_repo;
use crate::GitToolingError;
#[cfg(unix)]
use crate::platform::create_symlink;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use tempfile::tempdir;
fn run_git_in(repo_path: &Path, args: &[&str]) {
let status = Command::new("git")
.current_dir(repo_path)
.args(args)
.status()
.expect("git command");
assert!(status.success(), "git command failed: {args:?}");
}
fn git_stdout_in(repo_path: &Path, args: &[&str]) -> String {
let output = Command::new("git")
.current_dir(repo_path)
.args(args)
.output()
.expect("git command");
assert!(output.status.success(), "git command failed: {args:?}");
String::from_utf8(output.stdout).expect("git stdout utf8")
}
fn init_test_repo(repo_path: &Path) {
run_git_in(repo_path, &["init", "--initial-branch=main"]);
run_git_in(repo_path, &["config", "core.autocrlf", "false"]);
run_git_in(repo_path, &["config", "user.name", "Tester"]);
run_git_in(repo_path, &["config", "user.email", "test@example.com"]);
}
fn commit(repo_path: &Path, message: &str) {
run_git_in(repo_path, &["add", "."]);
run_git_in(
repo_path,
&[
"-c",
"user.name=Tester",
"-c",
"user.email=test@example.com",
"commit",
"-m",
message,
],
);
}
fn create_repo_with_nested_cwd() -> (tempfile::TempDir, PathBuf, PathBuf) {
let temp = tempdir().expect("tempdir");
let repo = temp.path().join("repo");
let nested = repo.join("nested").join("path");
fs::create_dir_all(&nested).expect("nested dir");
init_test_repo(&repo);
fs::write(repo.join("README.md"), "hello\n").expect("write file");
fs::write(nested.join("marker.txt"), "nested\n").expect("write nested file");
commit(&repo, "initial");
(temp, repo, nested)
}
fn assert_worktree_result(
result: &CodexManagedWorktree,
codex_home: &Path,
repo: &Path,
nested: &Path,
) {
let expected_repo_root = repo.canonicalize().expect("repo canonicalized");
assert_eq!(result.source_repo_root, expected_repo_root);
assert_eq!(
result.worktree_workspace_root,
result.worktree_git_root.join("nested/path")
);
assert_eq!(result.source_cwd, nested);
assert!(
result
.worktree_git_root
.starts_with(codex_home.join("worktrees"))
);
assert!(result.worktree_git_dir.exists());
assert_eq!(
result.marker_path,
result
.worktree_git_dir
.join(CODEX_MANAGED_WORKTREE_MARKER_FILE)
);
}
#[test]
fn create_codex_managed_worktree_preserves_nested_cwd_mapping() -> Result<(), GitToolingError> {
let (_temp, repo, nested) = create_repo_with_nested_cwd();
let codex_home = tempdir().expect("codex home");
let result = create_codex_managed_worktree(&nested, codex_home.path())?;
assert_worktree_result(&result, codex_home.path(), &repo, &nested);
assert!(result.worktree_workspace_root.exists());
Ok(())
}
#[test]
#[cfg(unix)]
fn create_codex_managed_worktree_preserves_nested_cwd_mapping_from_symlink()
-> Result<(), GitToolingError> {
let (temp, repo, _nested) = create_repo_with_nested_cwd();
let repo_symlink = temp.path().join("repo-symlink");
create_symlink(&repo, &repo, &repo_symlink)?;
let symlinked_nested = repo_symlink.join("nested/path");
let codex_home = tempdir().expect("codex home");
let result = create_codex_managed_worktree(&symlinked_nested, codex_home.path())?;
assert_eq!(
result.worktree_workspace_root,
result.worktree_git_root.join("nested/path")
);
assert_eq!(result.source_cwd, symlinked_nested);
Ok(())
}
#[test]
fn create_codex_managed_worktree_writes_marker_file() -> Result<(), GitToolingError> {
let (_temp, repo, nested) = create_repo_with_nested_cwd();
let codex_home = tempdir().expect("codex home");
let result = create_codex_managed_worktree(&nested, codex_home.path())?;
let marker = fs::read_to_string(&result.marker_path)?;
assert_eq!(marker, "codex-managed\n");
assert_eq!(
result.marker_path,
result
.worktree_git_dir
.join(CODEX_MANAGED_WORKTREE_MARKER_FILE)
);
assert!(repo.exists());
Ok(())
}
#[test]
fn cleanup_worktree_checkout_removes_worktree_registration() -> Result<(), GitToolingError> {
let (_temp, repo, _nested) = create_repo_with_nested_cwd();
let codex_home = tempdir().expect("codex home");
let starting_ref = starting_ref_for_repo(&repo)?;
let worktree_git_root = allocate_worktree_root(codex_home.path(), &repo)?;
create_worktree_checkout(&repo, &worktree_git_root, &starting_ref)?;
assert!(worktree_git_root.exists());
assert!(
git_stdout_in(&repo, &["worktree", "list", "--porcelain"])
.contains(&worktree_git_root.to_string_lossy().to_string())
);
cleanup_worktree_checkout(&repo, &worktree_git_root);
assert!(!worktree_git_root.exists());
assert!(
!git_stdout_in(&repo, &["worktree", "list", "--porcelain"])
.contains(&worktree_git_root.to_string_lossy().to_string())
);
Ok(())
}
}

View File

@@ -98,6 +98,10 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
/// Create a new local git worktree and start the session from that checkout.
#[arg(long = "worktree", default_value_t = false)]
pub worktree: bool,
/// Enable live web search. When enabled, the native Responses `web_search` tool is available to the model (no percall approval).
#[arg(long = "search", default_value_t = false)]
pub web_search: bool,
@@ -117,3 +121,18 @@ pub struct Cli {
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn parse_worktree_flag() {
let cli = Cli::parse_from(["codex-tui", "--worktree", "--cd", "/tmp/repo", "hello"]);
assert!(cli.worktree);
assert_eq!(cli.cwd, Some(PathBuf::from("/tmp/repo")));
assert_eq!(cli.prompt.as_deref(), Some("hello"));
}
}

View File

@@ -40,6 +40,9 @@ use codex_core::path_utils;
use codex_core::read_session_meta_line;
use codex_core::state_db::get_state_db;
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_git_utils::CodexManagedWorktree;
use codex_git_utils::GitToolingError;
use codex_git_utils::create_codex_managed_worktree;
use codex_protocol::ThreadId;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::SandboxMode;
@@ -79,10 +82,9 @@ mod app_event;
mod app_event_sender;
mod app_server_session;
mod ascii_animation;
#[cfg(not(target_os = "linux"))]
#[cfg(all(not(target_os = "linux"), feature = "voice-input"))]
mod audio_device;
#[cfg(target_os = "linux")]
#[allow(dead_code)]
#[cfg(all(not(target_os = "linux"), not(feature = "voice-input")))]
mod audio_device {
use crate::app_event::RealtimeAudioDeviceKind;
@@ -152,10 +154,9 @@ pub mod update_action;
mod update_prompt;
mod updates;
mod version;
#[cfg(not(target_os = "linux"))]
#[cfg(all(not(target_os = "linux"), feature = "voice-input"))]
mod voice;
#[cfg(target_os = "linux")]
#[allow(dead_code)]
#[cfg(all(not(target_os = "linux"), not(feature = "voice-input")))]
mod voice {
use crate::app_event_sender::AppEventSender;
use codex_core::config::Config;
@@ -221,6 +222,14 @@ use crate::onboarding::onboarding_screen::run_onboarding_app;
use crate::tui::Tui;
pub use cli::Cli;
use codex_arg0::Arg0DispatchPaths;
#[derive(Debug, PartialEq, Eq)]
struct StartupCwd {
cwd: Option<PathBuf>,
config_cwd: AbsolutePathBuf,
}
type WorktreeCreator = fn(&Path, &Path) -> Result<CodexManagedWorktree, GitToolingError>;
pub use markdown_render::render_markdown_text;
pub use public_widgets::composer_input::ComposerAction;
pub use public_widgets::composer_input::ComposerInput;
@@ -586,6 +595,33 @@ fn latest_session_lookup_params(
}
}
fn resolve_startup_cwd(
requested_cwd: Option<PathBuf>,
codex_home: &Path,
worktree_creator: Option<WorktreeCreator>,
) -> std::io::Result<StartupCwd> {
let config_cwd = match requested_cwd.as_deref() {
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
None => AbsolutePathBuf::current_dir()?,
};
let Some(worktree_creator) = worktree_creator else {
return Ok(StartupCwd {
cwd: requested_cwd,
config_cwd,
});
};
let worktree = worktree_creator(config_cwd.as_path(), codex_home)
.map_err(|err| std::io::Error::other(format!("Error creating worktree: {err}")))?;
let config_cwd = AbsolutePathBuf::from_absolute_path(&worktree.worktree_workspace_root)?;
Ok(StartupCwd {
cwd: Some(worktree.worktree_workspace_root),
config_cwd,
})
}
pub async fn run_main(
mut cli: Cli,
arg0_paths: Arg0DispatchPaths,
@@ -604,6 +640,11 @@ pub async fn run_main(
auth_token: remote_auth_token.clone(),
})
.unwrap_or(AppServerTarget::Embedded);
if cli.worktree && matches!(app_server_target, AppServerTarget::Remote { .. }) {
return Err(std::io::Error::other(
"--worktree is only supported for local Codex sessions",
));
}
let (sandbox_mode, approval_policy) = if cli.full_auto {
(
Some(SandboxMode::WorkspaceWrite),
@@ -653,11 +694,11 @@ pub async fn run_main(
}
};
let cwd = cli.cwd.clone();
let config_cwd = match cwd.as_deref() {
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
None => AbsolutePathBuf::current_dir()?,
};
let StartupCwd { cwd, config_cwd } = resolve_startup_cwd(
cli.cwd.clone(),
codex_home.as_path(),
cli.worktree.then_some(create_codex_managed_worktree),
)?;
#[allow(clippy::print_stderr)]
let config_toml = match load_config_as_toml_with_cli_overrides(
@@ -1701,6 +1742,67 @@ mod tests {
);
}
#[test]
fn resolve_startup_cwd_uses_requested_cwd_without_worktree() {
let codex_home = TempDir::new().expect("create temp codex home");
let cwd = TempDir::new().expect("create temp cwd");
let startup_cwd = resolve_startup_cwd(
Some(cwd.path().to_path_buf()),
codex_home.path(),
/*worktree_creator*/ None,
)
.expect("resolve startup cwd");
assert_eq!(
startup_cwd,
StartupCwd {
cwd: Some(cwd.path().to_path_buf()),
config_cwd: AbsolutePathBuf::from_absolute_path(
&cwd.path().canonicalize().expect("canonicalize cwd")
)
.expect("absolute cwd"),
}
);
}
#[test]
fn resolve_startup_cwd_uses_worktree_workspace_root_when_enabled() {
let codex_home = TempDir::new().expect("create temp codex home");
let cwd = TempDir::new().expect("create temp cwd");
let startup_cwd = resolve_startup_cwd(
Some(cwd.path().to_path_buf()),
codex_home.path(),
Some(|source_cwd, codex_home| {
let worktree_git_root = codex_home.join("worktrees/fake/project");
let worktree_git_dir = worktree_git_root.join(".git");
let marker_path = worktree_git_dir.join("codex-managed");
Ok(CodexManagedWorktree {
source_cwd: source_cwd.to_path_buf(),
source_repo_root: source_cwd.to_path_buf(),
worktree_git_root: worktree_git_root.clone(),
worktree_git_dir,
worktree_workspace_root: worktree_git_root.join("nested/path"),
starting_ref: "main".to_string(),
marker_path,
})
}),
)
.expect("resolve startup cwd");
let expected_worktree_workspace_root =
codex_home.path().join("worktrees/fake/project/nested/path");
assert_eq!(
startup_cwd,
StartupCwd {
cwd: Some(expected_worktree_workspace_root.clone()),
config_cwd: AbsolutePathBuf::from_absolute_path(&expected_worktree_workspace_root)
.expect("absolute worktree cwd"),
}
);
}
#[test]
fn normalize_remote_addr_accepts_secure_websocket_url() {
assert_eq!(