Skip to main content

winx_code_agent/utils/
repo.rs

1use crate::errors::Result;
2use std::fmt::Write as FmtWrite;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7const MAX_DEPTH: usize = 10;
8const MAX_CONTEXT_FILES: usize = 160;
9const MAX_RECENT_FILES: usize = 30;
10
11const IMPORTANT_NAMES: &[&str] = &[
12    "Cargo.toml",
13    "README.md",
14    "AGENTS.md",
15    "package.json",
16    "pnpm-workspace.yaml",
17    "pyproject.toml",
18    "go.mod",
19    "Dockerfile",
20    "docker-compose.yml",
21    ".github/workflows/ci.yml",
22];
23
24const SKIP_DIRS: &[&str] =
25    &[".git", "target", "node_modules", ".next", "dist", "build", ".venv", "__pycache__"];
26
27#[derive(Debug, Clone)]
28pub struct RepoContext {
29    pub root: PathBuf,
30    pub is_git_repo: bool,
31    pub project_summary: String,
32    pub recent_files: Vec<String>,
33    pub important_files: Vec<String>,
34    pub project_files: Vec<String>,
35}
36
37pub struct RepoContextAnalyzer;
38
39impl RepoContextAnalyzer {
40    pub fn analyze(path: &Path) -> Result<RepoContext> {
41        let root = workspace_root(path);
42        let is_git_repo = root.join(".git").exists();
43        let mut project_files = collect_project_files(&root)?;
44        project_files.sort_by_key(|path| (path_score(path), path.clone()));
45        project_files.truncate(MAX_CONTEXT_FILES);
46
47        let recent_files = if is_git_repo { recent_git_files(&root) } else { Vec::new() };
48        let important_files = important_files(&project_files);
49        let project_summary = project_summary(&root, is_git_repo, project_files.len());
50
51        Ok(RepoContext {
52            root,
53            is_git_repo,
54            project_summary,
55            recent_files,
56            important_files,
57            project_files,
58        })
59    }
60}
61
62pub fn get_repo_context(path: &Path) -> Result<(String, Vec<String>)> {
63    let context = RepoContextAnalyzer::analyze(path)?;
64    let mut output = String::new();
65
66    let _ = writeln!(output, "Project root: {}", context.root.display());
67    let _ = writeln!(output, "Git repository: {}", if context.is_git_repo { "yes" } else { "no" });
68    let _ = writeln!(output, "{}", context.project_summary);
69
70    if !context.important_files.is_empty() {
71        output.push_str("\nImportant files:\n");
72        for file in &context.important_files {
73            let _ = writeln!(output, "- {file}");
74        }
75    }
76
77    if !context.recent_files.is_empty() {
78        output.push_str("\nRecent git files:\n");
79        for file in &context.recent_files {
80            let _ = writeln!(output, "- {file}");
81        }
82    }
83
84    output.push_str("\nWorkspace files:\n");
85    for file in &context.project_files {
86        let _ = writeln!(output, "- {file}");
87    }
88
89    Ok((output, context.project_files))
90}
91
92fn workspace_root(path: &Path) -> PathBuf {
93    if path.is_file() {
94        path.parent().unwrap_or(path).to_path_buf()
95    } else {
96        path.to_path_buf()
97    }
98}
99
100fn collect_project_files(root: &Path) -> Result<Vec<String>> {
101    let mut files = Vec::new();
102    collect_files(root, root, 0, &mut files)?;
103    Ok(files)
104}
105
106fn collect_files(root: &Path, current: &Path, depth: usize, files: &mut Vec<String>) -> Result<()> {
107    if depth > MAX_DEPTH {
108        return Ok(());
109    }
110
111    let mut entries = fs::read_dir(current)?.collect::<std::result::Result<Vec<_>, _>>()?;
112    entries.sort_by_key(std::fs::DirEntry::path);
113
114    for entry in entries {
115        let path = entry.path();
116        let name = entry.file_name();
117        let name = name.to_string_lossy();
118        if path.is_dir() {
119            if !SKIP_DIRS.contains(&name.as_ref()) {
120                collect_files(root, &path, depth + 1, files)?;
121            }
122        } else if path.is_file() {
123            if let Ok(relative) = path.strip_prefix(root) {
124                files.push(relative.to_string_lossy().to_string());
125            }
126        }
127    }
128
129    Ok(())
130}
131
132fn important_files(files: &[String]) -> Vec<String> {
133    files.iter().filter(|file| IMPORTANT_NAMES.contains(&file.as_str())).cloned().collect()
134}
135
136fn recent_git_files(root: &Path) -> Vec<String> {
137    let output = Command::new("git")
138        .args(["-C"])
139        .arg(root)
140        .args(["log", "--name-only", "--pretty=format:", "-n", "50"])
141        .output();
142
143    let Ok(output) = output else {
144        return Vec::new();
145    };
146    if !output.status.success() {
147        return Vec::new();
148    }
149
150    let mut recent = Vec::new();
151    for line in String::from_utf8_lossy(&output.stdout).lines().map(str::trim) {
152        if !line.is_empty() && !recent.iter().any(|existing| existing == line) {
153            recent.push(line.to_string());
154        }
155        if recent.len() >= MAX_RECENT_FILES {
156            break;
157        }
158    }
159    recent
160}
161
162fn project_summary(root: &Path, is_git_repo: bool, file_count: usize) -> String {
163    let manifest = if root.join("Cargo.toml").exists() {
164        "Rust/Cargo"
165    } else if root.join("package.json").exists() {
166        "Node.js"
167    } else if root.join("pyproject.toml").exists() {
168        "Python"
169    } else {
170        "generic"
171    };
172    format!("Detected {manifest} workspace with {file_count} indexed files; git={is_git_repo}.")
173}
174
175fn path_score(path: &str) -> usize {
176    let important = usize::from(!IMPORTANT_NAMES.contains(&path));
177    let depth = path.matches('/').count();
178    let test_penalty = usize::from(path.contains("test") || path.contains("spec"));
179    important * 10 + depth + test_penalty
180}
181
182#[cfg(test)]
183mod tests {
184    use super::get_repo_context;
185    use crate::errors::Result;
186    use tempfile::TempDir;
187
188    #[test]
189    fn builds_repo_context_from_files() -> Result<()> {
190        let temp_dir = TempDir::new()?;
191        std::fs::write(temp_dir.path().join("Cargo.toml"), "[package]\nname='x'\n")?;
192        std::fs::create_dir(temp_dir.path().join("src"))?;
193        std::fs::write(temp_dir.path().join("src/lib.rs"), "pub fn x() {}\n")?;
194
195        let (context, files) = get_repo_context(temp_dir.path())?;
196        assert!(context.contains("Cargo.toml"));
197        assert!(files.iter().any(|file| file == "src/lib.rs"));
198        Ok(())
199    }
200}