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}