Skip to main content

agent_docs/
env.rs

1use std::env;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use anyhow::{Context, Result};
6use directories::BaseDirs;
7
8use crate::paths::normalize_root_path;
9
10#[derive(Debug, Clone, Default)]
11pub struct PathOverrides {
12    pub codex_home: Option<PathBuf>,
13    pub project_path: Option<PathBuf>,
14}
15
16#[derive(Debug, Clone)]
17pub struct ResolvedRoots {
18    pub codex_home: PathBuf,
19    pub project_path: PathBuf,
20    pub is_linked_worktree: bool,
21    pub git_common_dir: Option<PathBuf>,
22    pub primary_worktree_path: Option<PathBuf>,
23}
24
25pub fn resolve_roots(overrides: &PathOverrides) -> Result<ResolvedRoots> {
26    let cwd = env::current_dir().context("failed to read current directory")?;
27    let codex_home = resolve_codex_home(overrides.codex_home.as_deref(), &cwd);
28    let project_path = resolve_project_path(overrides.project_path.as_deref(), &cwd);
29    let metadata = resolve_linked_worktree_metadata(&project_path);
30
31    Ok(ResolvedRoots {
32        codex_home,
33        project_path,
34        is_linked_worktree: metadata.is_linked_worktree,
35        git_common_dir: metadata.git_common_dir,
36        primary_worktree_path: metadata.primary_worktree_path,
37    })
38}
39
40fn resolve_codex_home(cli_value: Option<&Path>, cwd: &Path) -> PathBuf {
41    if let Some(path) = cli_value {
42        return normalize_root_path(path, cwd);
43    }
44
45    if let Some(path) = read_env_path("CODEX_HOME") {
46        return normalize_root_path(&path, cwd);
47    }
48
49    if let Some(base_dirs) = BaseDirs::new() {
50        let default = base_dirs.home_dir().join(".codex");
51        return normalize_root_path(&default, cwd);
52    }
53
54    normalize_root_path(&cwd.join(".codex"), cwd)
55}
56
57fn resolve_project_path(cli_value: Option<&Path>, cwd: &Path) -> PathBuf {
58    if let Some(path) = cli_value {
59        return normalize_root_path(path, cwd);
60    }
61
62    if let Some(path) = read_env_path("PROJECT_PATH") {
63        return normalize_root_path(&path, cwd);
64    }
65
66    if let Some(path) = git_top_level(cwd) {
67        return normalize_root_path(&path, cwd);
68    }
69
70    normalize_root_path(cwd, cwd)
71}
72
73fn read_env_path(name: &str) -> Option<PathBuf> {
74    let raw = env::var_os(name)?;
75    if raw.is_empty() {
76        None
77    } else {
78        Some(PathBuf::from(raw))
79    }
80}
81
82fn git_top_level(cwd: &Path) -> Option<PathBuf> {
83    git_rev_parse_path(cwd, "--show-toplevel")
84}
85
86#[derive(Debug, Default)]
87struct LinkedWorktreeMetadata {
88    is_linked_worktree: bool,
89    git_common_dir: Option<PathBuf>,
90    primary_worktree_path: Option<PathBuf>,
91}
92
93fn resolve_linked_worktree_metadata(cwd: &Path) -> LinkedWorktreeMetadata {
94    let absolute_git_dir = git_rev_parse_path(cwd, "--absolute-git-dir");
95    let git_common_dir = git_rev_parse_path(cwd, "--git-common-dir");
96
97    let Some(git_common_dir) = git_common_dir else {
98        return LinkedWorktreeMetadata::default();
99    };
100
101    let is_linked_worktree = absolute_git_dir
102        .as_ref()
103        .is_some_and(|git_dir| git_dir != &git_common_dir);
104    let primary_worktree_path = if is_linked_worktree {
105        git_common_dir.parent().map(Path::to_path_buf)
106    } else {
107        None
108    };
109
110    LinkedWorktreeMetadata {
111        is_linked_worktree,
112        git_common_dir: Some(git_common_dir),
113        primary_worktree_path,
114    }
115}
116
117fn git_rev_parse_path(cwd: &Path, arg: &str) -> Option<PathBuf> {
118    let output = Command::new("git")
119        .args(["rev-parse", arg])
120        .current_dir(cwd)
121        .output()
122        .ok()?;
123
124    if !output.status.success() {
125        return None;
126    }
127
128    let stdout = String::from_utf8_lossy(&output.stdout);
129    let trimmed = stdout.trim();
130    if trimmed.is_empty() {
131        None
132    } else {
133        Some(normalize_root_path(Path::new(trimmed), cwd))
134    }
135}