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:
Douwe Osinga
2026-03-11 16:33:02 -04:00
committed by GitHub
parent 3016fdd8a7
commit 4578697ddd
3 changed files with 117 additions and 11 deletions
+2 -8
View File
@@ -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);
+113 -1
View File
@@ -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 -2
View File
@@ -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,
};