Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions codex-rs/core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
86 changes: 86 additions & 0 deletions codex-rs/core/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;

use rand::Rng;
Expand Down Expand Up @@ -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<PathBuf> {
// 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/<name>`
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())
}