From 03b8ed70e9b54f6389629d890705d3728c672daa Mon Sep 17 00:00:00 2001 From: David Hao Date: Thu, 21 Aug 2025 13:51:58 -0700 Subject: [PATCH] Assume that we trust codex for worktrees spawned from a repo that we already trust codex with --- codex-rs/core/src/config.rs | 10 ++++- codex-rs/core/src/util.rs | 86 +++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 4b6f8ac9ef..783d0ca1d0 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -261,7 +261,8 @@ pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Re // Mark the project as trusted. toml_edit is very good at handling // missing properties - let project_key = project_path.to_string_lossy().to_string(); + let normalized = crate::util::normalized_trust_project_root(project_path); + let project_key = normalized.to_string_lossy().to_string(); doc["projects"][project_key.as_str()]["trust_level"] = toml_edit::value("trusted"); // ensure codex_home exists @@ -454,8 +455,13 @@ impl ConfigToml { pub fn is_cwd_trusted(&self, resolved_cwd: &Path) -> bool { let projects = self.projects.clone().unwrap_or_default(); + // Prefer a normalized project key that points to the main git repo root when applicable. + let normalized_root = crate::util::normalized_trust_project_root(resolved_cwd); + let normalized_key = normalized_root.to_string_lossy().to_string(); + projects - .get(&resolved_cwd.to_string_lossy().to_string()) + .get(&normalized_key) + .or_else(|| projects.get(&resolved_cwd.to_string_lossy().to_string())) .map(|p| p.trust_level.clone().unwrap_or("".to_string()) == "trusted") .unwrap_or(false) } diff --git a/codex-rs/core/src/util.rs b/codex-rs/core/src/util.rs index e1248a4867..d570c999c5 100644 --- a/codex-rs/core/src/util.rs +++ b/codex-rs/core/src/util.rs @@ -1,4 +1,6 @@ +use std::fs; use std::path::Path; +use std::path::PathBuf; use std::time::Duration; use rand::Rng; @@ -42,3 +44,87 @@ pub fn is_inside_git_repo(base_dir: &Path) -> bool { false } + +/// Try to resolve the main git repository root for `base_dir`. +/// +/// - For a normal repo (where `.git` is a directory), returns the directory +/// that contains the `.git` directory. +/// - For a worktree (where `.git` is a file with a `gitdir:` pointer), reads +/// the referenced git directory and, if present, its `commondir` file to +/// locate the common `.git` directory of the main repository. Returns the +/// parent of that common directory. +/// - Returns `None` when no enclosing repo is found. +pub fn git_main_repo_root(base_dir: &Path) -> Option { + // Walk up from base_dir to find the first ancestor containing a `.git` entry. + let mut dir = base_dir.to_path_buf(); + loop { + let dot_git = dir.join(".git"); + if dot_git.is_dir() { + // Standard repository. The repo root is the directory containing `.git`. + return Some(dir); + } else if dot_git.is_file() { + // Worktree case: `.git` is a file like: `gitdir: /path/to/worktrees/` + if let Ok(contents) = fs::read_to_string(&dot_git) { + // Extract the path after `gitdir:` and trim whitespace. + let gitdir_prefix = "gitdir:"; + let line = contents + .lines() + .find(|l| l.trim_start().starts_with(gitdir_prefix)); + if let Some(line) = line { + let path_part = line.split_once(':').map(|(_, r)| r.trim()); + if let Some(gitdir_str) = path_part { + // Resolve relative paths against the directory containing `.git` (the worktree root). + let gitdir_path = Path::new(gitdir_str); + let gitdir_abs = if gitdir_path.is_absolute() { + gitdir_path.to_path_buf() + } else { + dir.join(gitdir_path) + }; + + // In worktrees, the per-worktree gitdir typically contains a `commondir` + // file that points (possibly relatively) to the common `.git` directory. + let commondir_path = gitdir_abs.join("commondir"); + if let Ok(common_dir_rel) = fs::read_to_string(&commondir_path) { + let common_dir_rel = common_dir_rel.trim(); + let common_dir_path = Path::new(common_dir_rel); + let common_dir_abs = if common_dir_path.is_absolute() { + common_dir_path.to_path_buf() + } else { + gitdir_abs.join(common_dir_path) + }; + // The main repo root is the parent of the common `.git` directory. + if let Some(parent) = common_dir_abs.parent() { + return Some(parent.to_path_buf()); + } + } else { + // Fallback: if no commondir file, use the parent of `gitdir_abs` if it looks like a `.git` dir. + if let Some(parent) = gitdir_abs.parent() { + return Some(parent.to_path_buf()); + } + } + } + } + } + // If parsing fails, continue the walk upwards in case of nested repos (rare). + } + + if !dir.pop() { + break; + } + } + + None +} + +/// Normalize a path for trust configuration lookups. +/// +/// If inside a git repo, returns the main repository root; otherwise returns the +// canonicalized `base_dir` (or `base_dir` if canonicalization fails). +pub fn normalized_trust_project_root(base_dir: &Path) -> PathBuf { + if let Some(repo_root) = git_main_repo_root(base_dir) { + return repo_root.canonicalize().unwrap_or(repo_root); + } + base_dir + .canonicalize() + .unwrap_or_else(|_| base_dir.to_path_buf()) +}