Skip to main content

sift_queue/
queue_path.rs

1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4/// Resolve the queue file path from (in priority order):
5/// 1. CLI --queue flag
6/// 2. SQ_QUEUE_PATH environment variable
7/// 3. Implicit discovery via git worktree semantics
8/// 4. Fallback default path
9pub 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}