From a6a80486fc50d1cd2a2276dee1313b440a6c360c Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:47:04 -0400 Subject: [PATCH 01/12] test: cover nested repository gitignore reset --- packages/rust/tests/cli_e2e.rs | 28 ++++++++++++++++++++++++++++ packages/rust/tests/scanner.rs | 19 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/rust/tests/cli_e2e.rs b/packages/rust/tests/cli_e2e.rs index eb083d43f..78b04c155 100644 --- a/packages/rust/tests/cli_e2e.rs +++ b/packages/rust/tests/cli_e2e.rs @@ -174,6 +174,34 @@ fn help_flag_lists_the_subcommands_and_default_query_flags() { .stdout(predicates::str::contains("init")); } +#[test] +fn query_glob_does_not_apply_outer_gitignore_inside_nested_repo() { + let outer = TempDir::new().unwrap(); + fs::create_dir(outer.path().join(".git")).unwrap(); + fs::write(outer.path().join(".gitignore"), "*.log\n").unwrap(); + let nested = outer.path().join("repo"); + fs::create_dir(&nested).unwrap(); + fs::create_dir(nested.join(".git")).unwrap(); + fs::write(nested.join("z.log"), "nested").unwrap(); + + let output = std::process::Command::cargo_bin("dirsql") + .unwrap() + .args(["query", "SELECT path FROM './**'", "--format", "json"]) + .current_dir(outer.path()) + .output() + .unwrap(); + + assert!( + output.status.success(), + "query failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + serde_json::from_slice::>(&output.stdout).unwrap(), + vec![json!({"path": "repo/z.log"})] + ); +} + #[test] fn server_help_lists_the_bind_flags() { // #662: `--host`/`--port` are server-local now, documented under diff --git a/packages/rust/tests/scanner.rs b/packages/rust/tests/scanner.rs index 9e547b046..0bdf18551 100644 --- a/packages/rust/tests/scanner.rs +++ b/packages/rust/tests/scanner.rs @@ -5,7 +5,7 @@ use std::fs; use dirsql::matcher::TableMatcher; -use dirsql::scanner::{scan_directory, scan_subtree}; +use dirsql::scanner::{compile_glob, scan_directory, scan_glob, scan_subtree}; use tempfile::TempDir; #[test] @@ -177,3 +177,20 @@ fn scan_subtree_prunes_ignored_directories() { assert_eq!(results, vec![(moved.join("keep.csv"), "t".to_string())]); } + +#[test] +fn scan_glob_stops_outer_gitignore_rules_at_nested_repo() { + let outer = TempDir::new().unwrap(); + fs::create_dir(outer.path().join(".git")).unwrap(); + fs::write(outer.path().join(".gitignore"), "*.log\n").unwrap(); + let nested = outer.path().join("repo"); + fs::create_dir(&nested).unwrap(); + fs::create_dir(nested.join(".git")).unwrap(); + fs::write(nested.join("z.log"), "nested").unwrap(); + + let ignore = TableMatcher::new(&[], &[]).unwrap(); + let glob = compile_glob("**").unwrap(); + let results = scan_glob(outer.path(), &glob, &ignore, true); + + assert_eq!(results, vec![std::path::PathBuf::from("repo/z.log")]); +} From 0f97547857799b224c39743ad6bed95f81e015d6 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:01:51 -0400 Subject: [PATCH 02/12] fix: reset gitignore rules in nested repositories --- .../2026-10-05-nested-repo-gitignore.md | 3 ++ .../2026-10-05-nested-repo-gitignore.md | 34 +++++++++++++++++++ packages/rust/src/scanner.rs | 13 +++++-- 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 packages/rust/changelog.d/2026-10-05-nested-repo-gitignore.md create mode 100644 packages/rust/migrations.d/2026-10-05-nested-repo-gitignore.md diff --git a/packages/rust/changelog.d/2026-10-05-nested-repo-gitignore.md b/packages/rust/changelog.d/2026-10-05-nested-repo-gitignore.md new file mode 100644 index 000000000..35abca6eb --- /dev/null +++ b/packages/rust/changelog.d/2026-10-05-nested-repo-gitignore.md @@ -0,0 +1,3 @@ +**Fixed** Path-table scans stop applying an outer repository's `.gitignore` +rules inside a nested repository. A nested repo now starts a fresh ignore +boundary, matching Git; `'./**'` and narrower globs return the same files. diff --git a/packages/rust/migrations.d/2026-10-05-nested-repo-gitignore.md b/packages/rust/migrations.d/2026-10-05-nested-repo-gitignore.md new file mode 100644 index 000000000..ef79c1fb7 --- /dev/null +++ b/packages/rust/migrations.d/2026-10-05-nested-repo-gitignore.md @@ -0,0 +1,34 @@ +### Core: nested repositories reset `.gitignore` inheritance + +#### Summary + +Path-table scans no longer carry a parent repository's `.gitignore` rules +into a nested repository. This matches Git's ignore boundary behavior and can +make previously hidden files visible to path-table queries. No API changes. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- When `outer` is a repository with `*.log` and `outer/repo` is also a + repository, `'./**'` now lists `repo/z.log`; the outer rule no longer + applies inside `repo`. + +#### Verification + +```bash +cd "$(mktemp -d)" +git init -q +printf '*.log\n' > .gitignore +mkdir repo +git -C repo init -q +touch repo/z.log +dirsql query "SELECT path FROM './**' ORDER BY path" +# expected: [{"path":"repo/z.log"}] +``` diff --git a/packages/rust/src/scanner.rs b/packages/rust/src/scanner.rs index bc6e1f843..5d44521dd 100644 --- a/packages/rust/src/scanner.rs +++ b/packages/rust/src/scanner.rs @@ -364,8 +364,10 @@ impl Walk<'_> { linked: bool, visit: &mut dyn FnMut(PathBuf, &DirEntry), ) { - let entered_repo = self.gitignore && !self.in_repo && holds_git(dir); - if entered_repo { + let entered_repo = self.gitignore && holds_git(dir); + let was_in_repo = self.in_repo; + let inherited_frames = entered_repo.then(|| std::mem::take(&mut self.frames)); + if entered_repo && !was_in_repo { self.in_repo = true; } let mut pushed = false; @@ -398,7 +400,10 @@ impl Walk<'_> { if pushed { self.frames.pop(); } - if entered_repo { + if let Some(frames) = inherited_frames { + self.frames = frames; + } + if entered_repo && !was_in_repo { self.in_repo = false; } } @@ -1031,6 +1036,7 @@ mod tests { ignore: &ignore, glob: Some(&glob), gitignore: false, + in_repo: false, frames: Vec::new(), }; assert!(walk.follows(false, &[1], Kind::Dir)); @@ -1046,6 +1052,7 @@ mod tests { ignore: &ignore, glob: Some(&glob), gitignore: false, + in_repo: false, frames: Vec::new(), }; assert!(walk.follows(false, &[], Kind::Dir)); From e8cf66b23f7b8b67f3efcf9bd5af9c56809daefe Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:03:59 -0400 Subject: [PATCH 03/12] e2e attestation for claude/1286-nested-repo --- .../python/e2e-attestations/claude-1286-nested-repo.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/python/e2e-attestations/claude-1286-nested-repo.json diff --git a/packages/python/e2e-attestations/claude-1286-nested-repo.json b/packages/python/e2e-attestations/claude-1286-nested-repo.json new file mode 100644 index 000000000..b1fbed069 --- /dev/null +++ b/packages/python/e2e-attestations/claude-1286-nested-repo.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1791227039, + "exit_code": 0, + "commit": "0f97547857799b224c39743ad6bed95f81e015d6", + "branch": "claude/1286-nested-repo" +} From ab87115d0c83f199cea59dd34985f0afcc232a4b Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:05:09 -0400 Subject: [PATCH 04/12] e2e attestation for claude/1286-nested-repo --- packages/ts/e2e-attestations/claude-1286-nested-repo.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/ts/e2e-attestations/claude-1286-nested-repo.json diff --git a/packages/ts/e2e-attestations/claude-1286-nested-repo.json b/packages/ts/e2e-attestations/claude-1286-nested-repo.json new file mode 100644 index 000000000..e74cd3b6b --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1286-nested-repo.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1791227109, + "exit_code": 0, + "commit": "e8cf66b23f7b8b67f3efcf9bd5af9c56809daefe", + "branch": "claude/1286-nested-repo" +} From c881e13cf4f94ebf5862ade991ecfbcd81d4600a Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:02:18 -0400 Subject: [PATCH 05/12] e2e attestation for claude/1286-nested-repo --- packages/python/e2e-attestations/claude-1286-nested-repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1286-nested-repo.json b/packages/python/e2e-attestations/claude-1286-nested-repo.json index b1fbed069..82e68efb0 100644 --- a/packages/python/e2e-attestations/claude-1286-nested-repo.json +++ b/packages/python/e2e-attestations/claude-1286-nested-repo.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791227039, + "ran_at": 1791230538, "exit_code": 0, - "commit": "0f97547857799b224c39743ad6bed95f81e015d6", + "commit": "41eccbeb65dea081f78bd3b31bf9f5000f99bbed", "branch": "claude/1286-nested-repo" } From c0b56629df10f217a259bdb978107d8439c75513 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:02:53 -0400 Subject: [PATCH 06/12] e2e attestation for claude/1286-nested-repo --- packages/ts/e2e-attestations/claude-1286-nested-repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1286-nested-repo.json b/packages/ts/e2e-attestations/claude-1286-nested-repo.json index e74cd3b6b..eab50433a 100644 --- a/packages/ts/e2e-attestations/claude-1286-nested-repo.json +++ b/packages/ts/e2e-attestations/claude-1286-nested-repo.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791227109, + "ran_at": 1791230573, "exit_code": 0, - "commit": "e8cf66b23f7b8b67f3efcf9bd5af9c56809daefe", + "commit": "c881e13cf4f94ebf5862ade991ecfbcd81d4600a", "branch": "claude/1286-nested-repo" } From 750730d48413c9f1169d43cffac7c3bc7dcd0684 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:56:24 -0400 Subject: [PATCH 07/12] test: cover nested repository ignore transitions --- packages/rust/tests/scanner.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/rust/tests/scanner.rs b/packages/rust/tests/scanner.rs index 0bdf18551..891e2e04f 100644 --- a/packages/rust/tests/scanner.rs +++ b/packages/rust/tests/scanner.rs @@ -182,11 +182,15 @@ fn scan_subtree_prunes_ignored_directories() { fn scan_glob_stops_outer_gitignore_rules_at_nested_repo() { let outer = TempDir::new().unwrap(); fs::create_dir(outer.path().join(".git")).unwrap(); - fs::write(outer.path().join(".gitignore"), "*.log\n").unwrap(); + fs::write(outer.path().join(".gitignore"), "repo/*.log\n").unwrap(); let nested = outer.path().join("repo"); fs::create_dir(&nested).unwrap(); fs::create_dir(nested.join(".git")).unwrap(); fs::write(nested.join("z.log"), "nested").unwrap(); + let sibling = outer.path().join("zz-sibling"); + fs::create_dir(&sibling).unwrap(); + fs::write(sibling.join(".gitignore"), "*.log\n").unwrap(); + fs::write(sibling.join("z.log"), "ignored").unwrap(); let ignore = TableMatcher::new(&[], &[]).unwrap(); let glob = compile_glob("**").unwrap(); @@ -194,3 +198,23 @@ fn scan_glob_stops_outer_gitignore_rules_at_nested_repo() { assert_eq!(results, vec![std::path::PathBuf::from("repo/z.log")]); } + +#[test] +fn scan_glob_applies_gitignore_only_inside_a_repo() { + let outer = TempDir::new().unwrap(); + let nested = outer.path().join("repo"); + fs::create_dir(&nested).unwrap(); + fs::create_dir(nested.join(".git")).unwrap(); + fs::write(nested.join(".gitignore"), "*.log\n").unwrap(); + fs::write(nested.join("z.log"), "ignored").unwrap(); + let sibling = outer.path().join("zz-sibling"); + fs::create_dir(&sibling).unwrap(); + fs::write(sibling.join(".gitignore"), "*.log\n").unwrap(); + fs::write(sibling.join("z.log"), "visible").unwrap(); + + let ignore = TableMatcher::new(&[], &[]).unwrap(); + let glob = compile_glob("**").unwrap(); + let results = scan_glob(outer.path(), &glob, &ignore, true); + + assert_eq!(results, vec![std::path::PathBuf::from("zz-sibling/z.log")]); +} From 68858295a0d429345af616a28e2cee3946f186cc Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:15:03 -0400 Subject: [PATCH 08/12] Track nested-repository entry without equivalent branches --- packages/rust/src/scanner.rs | 8 ++------ testing-conventions.toml | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/rust/src/scanner.rs b/packages/rust/src/scanner.rs index 5d44521dd..4138a8cc2 100644 --- a/packages/rust/src/scanner.rs +++ b/packages/rust/src/scanner.rs @@ -367,9 +367,7 @@ impl Walk<'_> { let entered_repo = self.gitignore && holds_git(dir); let was_in_repo = self.in_repo; let inherited_frames = entered_repo.then(|| std::mem::take(&mut self.frames)); - if entered_repo && !was_in_repo { - self.in_repo = true; - } + self.in_repo |= entered_repo; let mut pushed = false; if self.in_repo && let Some(matcher) = load_gitignore(dir) @@ -403,9 +401,7 @@ impl Walk<'_> { if let Some(frames) = inherited_frames { self.frames = frames; } - if entered_repo && !was_in_repo { - self.in_repo = false; - } + self.in_repo = was_in_repo; } /// Whether the walk takes an entry at `depth`: the skip rules and the diff --git a/testing-conventions.toml b/testing-conventions.toml index 5c3800036..9fadcb54b 100644 --- a/testing-conventions.toml +++ b/testing-conventions.toml @@ -108,19 +108,19 @@ features = ["cli"] [[rust.exempt]] path = "src/scanner.rs" rules = ["mutation"] -lines = [72, 131, 330, 367, 445, 466] +lines = [72, 131, 330, 367, 446, 467] reason = "scan_below, scan_glob, walk, descend, kind_of and sorted_entries read the filesystem; tests/scanner.rs (scan_finds_matching_files, scan_recurses_into_subdirectories), tests/vtab.rs (glob_scopes_the_walk, rowids_count_up_from_zero_in_scan_order) and tests/path_table_symlinks.rs assert what they return" [[rust.exempt]] path = "src/scanner.rs" rules = ["mutation"] -lines = [384, 385, 388, 446, 449] +lines = [384, 385, 388, 447, 450] reason = "whether a walked entry is a symlink, and whether the walk is below one, is known only from a real tree; tests/path_table_symlinks.rs asserts every outcome against bash, and the decisions themselves are unit-tested in classify, Walk::follows and PathGlob::step" [[rust.exempt]] path = "src/scanner.rs" rules = ["mutation"] -lines = [368, 372, 401, 480, 493] +lines = [370, 372, 404, 481, 494] reason = "whether a repo encloses the walk, and which .gitignore files are in force, is known only from a real tree holding .git; the choice of directories is unit-tested in enclosing_repo and dirs_above, and tests/path_table_gitignore.rs (with_no_repo_a_gitignore_does_not_apply_at_the_index_root, inside_a_repo_the_same_gitignore_hides_everything, a_repo_root_gitignore_above_the_index_root_applies) asserts every outcome" [[rust.exempt]] From 9076974bdc38e6188d04777c1715b579982c53b9 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:17:09 -0400 Subject: [PATCH 09/12] e2e attestation for claude/1286-nested-repo --- packages/python/e2e-attestations/claude-1286-nested-repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1286-nested-repo.json b/packages/python/e2e-attestations/claude-1286-nested-repo.json index 82e68efb0..a71e61185 100644 --- a/packages/python/e2e-attestations/claude-1286-nested-repo.json +++ b/packages/python/e2e-attestations/claude-1286-nested-repo.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791230538, + "ran_at": 1791238629, "exit_code": 0, - "commit": "41eccbeb65dea081f78bd3b31bf9f5000f99bbed", + "commit": "68858295a0d429345af616a28e2cee3946f186cc", "branch": "claude/1286-nested-repo" } From 863b8042f1441011f26125f6b1d7c00f0c1b8697 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:17:44 -0400 Subject: [PATCH 10/12] e2e attestation for claude/1286-nested-repo --- packages/ts/e2e-attestations/claude-1286-nested-repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1286-nested-repo.json b/packages/ts/e2e-attestations/claude-1286-nested-repo.json index eab50433a..08ab095d5 100644 --- a/packages/ts/e2e-attestations/claude-1286-nested-repo.json +++ b/packages/ts/e2e-attestations/claude-1286-nested-repo.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791230573, + "ran_at": 1791238664, "exit_code": 0, - "commit": "c881e13cf4f94ebf5862ade991ecfbcd81d4600a", + "commit": "9076974bdc38e6188d04777c1715b579982c53b9", "branch": "claude/1286-nested-repo" } From cfff3f6bdbe4297949b451702c9c6e91c24aeb57 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:25:15 -0400 Subject: [PATCH 11/12] e2e attestation for claude/1286-nested-repo --- packages/ts/e2e-attestations/claude-1286-nested-repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1286-nested-repo.json b/packages/ts/e2e-attestations/claude-1286-nested-repo.json index 08ab095d5..90c594e16 100644 --- a/packages/ts/e2e-attestations/claude-1286-nested-repo.json +++ b/packages/ts/e2e-attestations/claude-1286-nested-repo.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791238664, + "ran_at": 1791239115, "exit_code": 0, - "commit": "9076974bdc38e6188d04777c1715b579982c53b9", + "commit": "0257c015584ee781df5b4cfd11b2dd3f35455f54", "branch": "claude/1286-nested-repo" } From 9c031494d3b5345c3c8b1d71e0f5d2909435c07e Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:28:20 -0400 Subject: [PATCH 12/12] ci: explain the TypeScript e2e scope correction skip-changelog: TypeScript CI scope correction only; published behavior is unchanged