Skip to main content

codetether_agent/tui/utils/
workspace_helpers.rs

1//! Git/FS helpers backing [`super::workspace::WorkspaceSnapshot`].
2
3use std::path::Path;
4use std::process::Command;
5
6pub fn should_skip_entry(name: &str) -> bool {
7    matches!(
8        name,
9        ".git" | "node_modules" | "target" | ".next" | "__pycache__" | ".venv"
10    )
11}
12
13/// Fetch branch and dirty-file count in a single `git` subprocess.
14///
15/// `git status --porcelain=v1 --branch` prints one header line
16/// (`## branch-name...tracking-info` or `## HEAD (no branch)`) followed
17/// by one line per dirty path. Parsing both from one spawn halves the
18/// subprocess cost of [`super::workspace::WorkspaceSnapshot::capture`].
19pub fn detect_git_status(root: &Path) -> (Option<String>, usize) {
20    let Ok(output) = Command::new("git")
21        .arg("-C")
22        .arg(root)
23        .args(["status", "--porcelain=v1", "--branch"])
24        .output()
25    else {
26        return (None, 0);
27    };
28    if !output.status.success() {
29        return (None, 0);
30    }
31    let text = String::from_utf8_lossy(&output.stdout);
32    let mut branch = None;
33    let mut dirty = 0usize;
34    for line in text.lines() {
35        if let Some(rest) = line.strip_prefix("## ") {
36            let name = rest
37                .split("...")
38                .next()
39                .unwrap_or(rest)
40                .trim()
41                .to_string();
42            if !name.is_empty() && name != "HEAD (no branch)" {
43                branch = Some(name);
44            }
45        } else if !line.trim().is_empty() {
46            dirty += 1;
47        }
48    }
49    (branch, dirty)
50}
51