1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4pub fn resolve_queue_path(cli_flag: Option<&PathBuf>) -> PathBuf {
10 if let Some(path) = cli_flag {
11 return path.clone();
12 }
13 if let Ok(env_path) = std::env::var("SQ_QUEUE_PATH") {
14 return PathBuf::from(env_path);
15 }
16
17 let cwd = match std::env::current_dir() {
18 Ok(path) => path,
19 Err(_) => return PathBuf::from(".sift/issues.jsonl"),
20 };
21
22 let git = git_context(&cwd);
23 resolve_implicit_queue_path(&cwd, git.as_ref())
24}
25
26fn resolve_implicit_queue_path(cwd: &Path, git: Option<&GitContext>) -> PathBuf {
27 if let Some(git) = git {
28 if let Some(found) = find_existing_queue(&git.cwd, &git.worktree_root) {
29 return found;
30 }
31
32 if let Some(main_worktree_root) = linked_main_worktree_root(git) {
33 if let Ok(rel_cwd) = git.cwd.strip_prefix(&git.worktree_root) {
34 if let Some(found) =
35 find_existing_queue(&main_worktree_root.join(rel_cwd), &main_worktree_root)
36 {
37 return found;
38 }
39 }
40 }
41
42 return git.cwd.join(".sift").join("issues.jsonl");
43 }
44
45 cwd.join(".sift").join("issues.jsonl")
46}
47
48#[derive(Debug)]
49struct GitContext {
50 cwd: PathBuf,
51 worktree_root: PathBuf,
52 git_dir: PathBuf,
53 git_common_dir: PathBuf,
54}
55
56fn git_context(cwd: &Path) -> Option<GitContext> {
57 let cwd = std::fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
58 let worktree_root = resolve_git_path(&cwd, &git_rev_parse(&cwd, "--show-toplevel")?);
59 let git_dir = resolve_git_path(&cwd, &git_rev_parse(&cwd, "--git-dir")?);
60 let git_common_dir = resolve_git_path(&cwd, &git_rev_parse(&cwd, "--git-common-dir")?);
61
62 Some(GitContext {
63 cwd,
64 worktree_root,
65 git_dir,
66 git_common_dir,
67 })
68}
69
70fn linked_main_worktree_root(git: &GitContext) -> Option<PathBuf> {
71 if git.git_dir == git.git_common_dir {
72 return None;
73 }
74
75 git.git_common_dir.parent().map(Path::to_path_buf)
76}
77
78fn git_rev_parse(cwd: &Path, arg: &str) -> Option<String> {
79 let output = Command::new("git")
80 .current_dir(cwd)
81 .args(["rev-parse", arg])
82 .output()
83 .ok()?;
84
85 if !output.status.success() {
86 return None;
87 }
88
89 let stdout = String::from_utf8(output.stdout).ok()?;
90 let line = stdout.lines().next()?.trim();
91 if line.is_empty() {
92 None
93 } else {
94 Some(line.to_string())
95 }
96}
97
98fn resolve_git_path(cwd: &Path, raw: &str) -> PathBuf {
99 let path = PathBuf::from(raw);
100 let absolute = if path.is_absolute() {
101 path
102 } else {
103 cwd.join(path)
104 };
105 std::fs::canonicalize(&absolute).unwrap_or(absolute)
106}
107
108fn find_existing_queue(start: &Path, stop: &Path) -> Option<PathBuf> {
109 let mut current = start.to_path_buf();
110
111 loop {
112 let candidate = current.join(".sift").join("issues.jsonl");
113 if candidate.is_file() {
114 return Some(candidate);
115 }
116
117 if current == stop {
118 return None;
119 }
120
121 if !current.starts_with(stop) {
122 return None;
123 }
124
125 current = current.parent()?.to_path_buf();
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::{resolve_implicit_queue_path, GitContext};
132 use std::fs;
133 use std::path::{Path, PathBuf};
134 use tempfile::TempDir;
135
136 fn write_queue(path: &Path) {
137 fs::create_dir_all(path.parent().unwrap()).unwrap();
138 fs::write(path, "").unwrap();
139 }
140
141 #[test]
142 fn prefers_nearest_existing_ancestor_queue_in_worktree() {
143 let dir = TempDir::new().unwrap();
144 let root = dir.path().to_path_buf();
145 let cwd = root.join("packages/alpha/client");
146 fs::create_dir_all(&cwd).unwrap();
147
148 let ancestor_queue = root.join("packages/.sift/issues.jsonl");
149 write_queue(&ancestor_queue);
150
151 let git = GitContext {
152 cwd: cwd.clone(),
153 worktree_root: root.clone(),
154 git_dir: root.join(".git"),
155 git_common_dir: root.join(".git"),
156 };
157
158 assert_eq!(
159 resolve_implicit_queue_path(&cwd, Some(&git)),
160 ancestor_queue
161 );
162 }
163
164 #[test]
165 fn falls_back_to_cwd_when_no_existing_queue_is_found() {
166 let dir = TempDir::new().unwrap();
167 let root = dir.path().to_path_buf();
168 let cwd = root.join("packages/alpha/client");
169 fs::create_dir_all(&cwd).unwrap();
170
171 let git = GitContext {
172 cwd: cwd.clone(),
173 worktree_root: root.clone(),
174 git_dir: root.join(".git"),
175 git_common_dir: root.join(".git"),
176 };
177
178 assert_eq!(
179 resolve_implicit_queue_path(&cwd, Some(&git)),
180 cwd.join(".sift/issues.jsonl")
181 );
182 }
183
184 #[test]
185 fn linked_worktree_checks_main_worktree_when_current_worktree_has_no_queue() {
186 let dir = TempDir::new().unwrap();
187 let main_root = dir.path().join("main");
188 let linked_root = dir.path().join("linked");
189 fs::create_dir_all(&main_root).unwrap();
190 fs::create_dir_all(&linked_root).unwrap();
191
192 let cwd = linked_root.join("src/packages/alpha/client");
193 fs::create_dir_all(&cwd).unwrap();
194
195 let main_queue = main_root.join("src/packages/.sift/issues.jsonl");
196 write_queue(&main_queue);
197
198 let git = GitContext {
199 cwd: cwd.clone(),
200 worktree_root: linked_root.clone(),
201 git_dir: main_root.join(".git/worktrees/linked"),
202 git_common_dir: main_root.join(".git"),
203 };
204
205 assert_eq!(resolve_implicit_queue_path(&cwd, Some(&git)), main_queue);
206 }
207
208 #[test]
209 fn outside_git_falls_back_to_cwd_relative_queue() {
210 let dir = TempDir::new().unwrap();
211 let cwd = dir.path().join("scratch/nested");
212 fs::create_dir_all(&cwd).unwrap();
213
214 assert_eq!(
215 resolve_implicit_queue_path(&cwd, None),
216 PathBuf::from(&cwd).join(".sift/issues.jsonl")
217 );
218 }
219}