1use std::env;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result, bail};
5use nils_common::git as shared_git;
6
7use crate::paths::normalize_root_path;
8
9#[derive(Debug, Clone, Default)]
10pub struct PathOverrides {
11 pub agent_home: Option<PathBuf>,
12 pub project_path: Option<PathBuf>,
13}
14
15#[derive(Debug, Clone)]
16pub struct ResolvedRoots {
17 pub agent_home: PathBuf,
18 pub project_path: PathBuf,
19 pub is_linked_worktree: bool,
20 pub git_common_dir: Option<PathBuf>,
21 pub primary_worktree_path: Option<PathBuf>,
22}
23
24pub fn resolve_roots(overrides: &PathOverrides) -> Result<ResolvedRoots> {
25 let cwd = env::current_dir().context("failed to read current directory")?;
26 let agent_home = resolve_agent_home(overrides.agent_home.as_deref(), &cwd)?;
27 let project_path = resolve_project_path(overrides.project_path.as_deref(), &cwd);
28 let metadata = resolve_linked_worktree_metadata(&project_path);
29
30 Ok(ResolvedRoots {
31 agent_home,
32 project_path,
33 is_linked_worktree: metadata.is_linked_worktree,
34 git_common_dir: metadata.git_common_dir,
35 primary_worktree_path: metadata.primary_worktree_path,
36 })
37}
38
39fn resolve_agent_home(cli_value: Option<&Path>, cwd: &Path) -> Result<PathBuf> {
40 if let Some(path) = cli_value {
41 return Ok(normalize_root_path(path, cwd));
42 }
43
44 if let Some(path) = read_env_path("AGENT_HOME") {
45 return Ok(normalize_root_path(&path, cwd));
46 }
47
48 bail!("AGENT_HOME is required; set AGENT_HOME or pass --agent-home")
49}
50
51fn resolve_project_path(cli_value: Option<&Path>, cwd: &Path) -> PathBuf {
52 if let Some(path) = cli_value {
53 return normalize_root_path(path, cwd);
54 }
55
56 if let Some(path) = read_env_path("PROJECT_PATH") {
57 return normalize_root_path(&path, cwd);
58 }
59
60 if let Some(path) = git_top_level(cwd) {
61 return normalize_root_path(&path, cwd);
62 }
63
64 normalize_root_path(cwd, cwd)
65}
66
67fn read_env_path(name: &str) -> Option<PathBuf> {
68 let raw = env::var_os(name)?;
69 if raw.is_empty() {
70 None
71 } else {
72 Some(PathBuf::from(raw))
73 }
74}
75
76fn git_top_level(cwd: &Path) -> Option<PathBuf> {
77 git_rev_parse_path(cwd, "--show-toplevel")
78}
79
80#[derive(Debug, Default)]
81struct LinkedWorktreeMetadata {
82 is_linked_worktree: bool,
83 git_common_dir: Option<PathBuf>,
84 primary_worktree_path: Option<PathBuf>,
85}
86
87fn resolve_linked_worktree_metadata(cwd: &Path) -> LinkedWorktreeMetadata {
88 let absolute_git_dir = git_rev_parse_path(cwd, "--absolute-git-dir");
89 let git_common_dir = git_rev_parse_path(cwd, "--git-common-dir");
90
91 let Some(git_common_dir) = git_common_dir else {
92 return LinkedWorktreeMetadata::default();
93 };
94
95 let is_linked_worktree = absolute_git_dir
96 .as_ref()
97 .is_some_and(|git_dir| git_dir != &git_common_dir);
98 let primary_worktree_path = if is_linked_worktree {
99 git_common_dir.parent().map(Path::to_path_buf)
100 } else {
101 None
102 };
103
104 LinkedWorktreeMetadata {
105 is_linked_worktree,
106 git_common_dir: Some(git_common_dir),
107 primary_worktree_path,
108 }
109}
110
111fn git_rev_parse_path(cwd: &Path, arg: &str) -> Option<PathBuf> {
112 let raw = shared_git::rev_parse_in(cwd, &[arg]).ok().flatten()?;
113 Some(normalize_root_path(Path::new(&raw), cwd))
114}