fix(goose): load .gitignore in prompt_manager for hint file filtering (#7795)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -7,6 +7,7 @@ use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::agents::extension::ExtensionInfo;
|
||||
use crate::hints::load_hints::build_gitignore;
|
||||
use crate::hints::{get_context_filenames, load_hint_files, SubdirectoryHintTracker};
|
||||
use crate::{
|
||||
config::{Config, GooseMode},
|
||||
@@ -90,14 +91,7 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> {
|
||||
|
||||
pub fn with_hints(mut self, working_dir: &Path) -> Self {
|
||||
let hints_filenames = get_context_filenames();
|
||||
let ignore_patterns = {
|
||||
let builder = ignore::gitignore::GitignoreBuilder::new(working_dir);
|
||||
builder.build().unwrap_or_else(|_| {
|
||||
ignore::gitignore::GitignoreBuilder::new(working_dir)
|
||||
.build()
|
||||
.expect("Failed to build default gitignore")
|
||||
})
|
||||
};
|
||||
let ignore_patterns = build_gitignore(working_dir);
|
||||
|
||||
let hints = load_hint_files(working_dir, &hints_filenames, &ignore_patterns);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ignore::gitignore::Gitignore;
|
||||
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
@@ -201,6 +201,27 @@ fn get_local_directories(git_root: Option<&Path>, cwd: &Path) -> Vec<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a `Gitignore` that includes `.gitignore` files from the git root
|
||||
/// down to `cwd`, matching git's hierarchical ignore semantics. When there
|
||||
/// is no git root, only `cwd/.gitignore` is loaded.
|
||||
pub fn build_gitignore(cwd: &Path) -> Gitignore {
|
||||
let git_root = find_git_root(cwd);
|
||||
let directories = get_local_directories(git_root, cwd);
|
||||
|
||||
let mut builder = GitignoreBuilder::new(cwd);
|
||||
for dir in &directories {
|
||||
let gitignore_path = dir.join(".gitignore");
|
||||
if gitignore_path.is_file() {
|
||||
builder.add(&gitignore_path);
|
||||
}
|
||||
}
|
||||
builder.build().unwrap_or_else(|_| {
|
||||
GitignoreBuilder::new(cwd)
|
||||
.build()
|
||||
.expect("Failed to build default gitignore")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_hint_files(
|
||||
cwd: &Path,
|
||||
hints_filenames: &[String],
|
||||
@@ -719,3 +740,94 @@ End of hints"#;
|
||||
assert!(hints.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod gitignore_tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_hints_with_gitignore_filters_referenced_files() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let project_root = dir.path();
|
||||
|
||||
fs::create_dir(project_root.join(".git")).unwrap();
|
||||
fs::write(project_root.join("allowed.md"), "Allowed content").unwrap();
|
||||
fs::write(project_root.join("secret.env"), "SECRET_KEY=abc123").unwrap();
|
||||
fs::write(project_root.join(".gitignore"), "*.env\n").unwrap();
|
||||
|
||||
let hints_content = "Project hints\n@allowed.md\n@secret.env\nEnd of hints";
|
||||
fs::write(project_root.join(GOOSE_HINTS_FILENAME), hints_content).unwrap();
|
||||
|
||||
let gitignore = build_gitignore(project_root);
|
||||
|
||||
let hints = load_hint_files(
|
||||
project_root,
|
||||
&[GOOSE_HINTS_FILENAME.to_string()],
|
||||
&gitignore,
|
||||
);
|
||||
|
||||
assert!(hints.contains("Allowed content"));
|
||||
assert!(!hints.contains("SECRET_KEY=abc123"));
|
||||
assert!(hints.contains("@secret.env"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitignore_loads_from_git_root_in_subdirectory() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let project_root = dir.path();
|
||||
|
||||
fs::create_dir(project_root.join(".git")).unwrap();
|
||||
// Root .gitignore ignores .env files
|
||||
fs::write(project_root.join(".gitignore"), "*.env\n").unwrap();
|
||||
fs::write(project_root.join("secret.env"), "SECRET_KEY=abc123").unwrap();
|
||||
fs::write(project_root.join("allowed.md"), "Allowed content").unwrap();
|
||||
|
||||
let subdir = project_root.join("subdir");
|
||||
fs::create_dir(&subdir).unwrap();
|
||||
|
||||
let hints_content = "Subdir hints\n@../allowed.md\n@../secret.env\nEnd of hints";
|
||||
fs::write(subdir.join(GOOSE_HINTS_FILENAME), hints_content).unwrap();
|
||||
|
||||
// Build gitignore from the subdirectory — should still pick up root .gitignore
|
||||
let gitignore = build_gitignore(&subdir);
|
||||
|
||||
let hints = load_hint_files(&subdir, &[GOOSE_HINTS_FILENAME.to_string()], &gitignore);
|
||||
|
||||
assert!(hints.contains("Allowed content"));
|
||||
assert!(!hints.contains("SECRET_KEY=abc123"));
|
||||
assert!(hints.contains("@../secret.env"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitignore_merges_nested_gitignores() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let project_root = dir.path();
|
||||
|
||||
fs::create_dir(project_root.join(".git")).unwrap();
|
||||
// Root ignores *.log
|
||||
fs::write(project_root.join(".gitignore"), "*.log\n").unwrap();
|
||||
|
||||
let subdir = project_root.join("subdir");
|
||||
fs::create_dir(&subdir).unwrap();
|
||||
// Subdir ignores *.tmp
|
||||
fs::write(subdir.join(".gitignore"), "*.tmp\n").unwrap();
|
||||
|
||||
fs::write(project_root.join("debug.log"), "debug log").unwrap();
|
||||
fs::write(subdir.join("cache.tmp"), "temp data").unwrap();
|
||||
fs::write(subdir.join("readme.md"), "Readme content").unwrap();
|
||||
|
||||
let hints_content = "Hints\n@../debug.log\n@cache.tmp\n@readme.md\nEnd";
|
||||
fs::write(subdir.join(GOOSE_HINTS_FILENAME), hints_content).unwrap();
|
||||
|
||||
let gitignore = build_gitignore(&subdir);
|
||||
let hints = load_hint_files(&subdir, &[GOOSE_HINTS_FILENAME.to_string()], &gitignore);
|
||||
|
||||
assert!(hints.contains("Readme content"));
|
||||
assert!(!hints.contains("debug log"));
|
||||
assert!(!hints.contains("temp data"));
|
||||
assert!(hints.contains("@../debug.log"));
|
||||
assert!(hints.contains("@cache.tmp"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ mod import_files;
|
||||
pub mod load_hints;
|
||||
|
||||
pub use load_hints::{
|
||||
get_context_filenames, load_hint_files, SubdirectoryHintTracker, AGENTS_MD_FILENAME,
|
||||
GOOSE_HINTS_FILENAME,
|
||||
build_gitignore, get_context_filenames, load_hint_files, SubdirectoryHintTracker,
|
||||
AGENTS_MD_FILENAME, GOOSE_HINTS_FILENAME,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user