diff --git a/packages/python/e2e-attestations/bot-1579-dot-double-slash-1104.json b/packages/python/e2e-attestations/bot-1579-dot-double-slash-1104.json new file mode 100644 index 00000000..433d3512 --- /dev/null +++ b/packages/python/e2e-attestations/bot-1579-dot-double-slash-1104.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/query_contract_test.py tests/e2e/use_case_join_test.py tests/e2e/use_case_dossier_test.py tests/e2e/glob_speed_one_level_test.py tests/e2e/stat_speed_test.py -x -q", + "ran_at": 1793822894, + "exit_code": 0, + "commit": "ea337e83bcf950dfe28707326259f82681dbb56f", + "branch": "bot/1579-dot-double-slash-1104" +} diff --git a/packages/rust/changelog.d/2026-11-04-dot-double-slash-path-table.md b/packages/rust/changelog.d/2026-11-04-dot-double-slash-path-table.md new file mode 100644 index 00000000..a68f12e9 --- /dev/null +++ b/packages/rust/changelog.d/2026-11-04-dot-double-slash-path-table.md @@ -0,0 +1,3 @@ +**Fixed** + +A path-table written with repeated separators after the leading dot (`'.//docs/a.md'`, `'.//'`) resolves under the index root, the same as `'./docs/a.md'`, instead of against the filesystem root. diff --git a/packages/rust/migrations.d/2026-11-04-dot-double-slash-path-table.md b/packages/rust/migrations.d/2026-11-04-dot-double-slash-path-table.md new file mode 100644 index 00000000..68919dc3 --- /dev/null +++ b/packages/rust/migrations.d/2026-11-04-dot-double-slash-path-table.md @@ -0,0 +1,25 @@ +### `.//` path-tables resolve under the index root + +#### Summary + +A path-table whose leading `./` is followed by more separators (`'.//docs/a.md'`) now resolves under the index root instead of the filesystem root. Nothing breaks. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +`'.//x'` returns the rows `'./x'` returns. It no longer reads `/x`. + +#### Verification + +```bash +dirsql "SELECT path FROM './/docs/*.md'" +``` + +Prints the same rows as `dirsql "SELECT path FROM './docs/*.md'"`. diff --git a/packages/rust/src/path_table.rs b/packages/rust/src/path_table.rs index 26ccf6b3..cd190c1a 100644 --- a/packages/rust/src/path_table.rs +++ b/packages/rust/src/path_table.rs @@ -141,6 +141,9 @@ fn resolve_as( ) -> Resolution { let target = trailing_separator_as_star::(name); if let Some(rest) = target.strip_prefix("./") { + // Left on, a separator after `./` would make the rest an absolute + // path that replaces the index root when joined to it. + let rest = rest.trim_start_matches('/'); return Resolution::Table(split_relative(index_root, rest, is_dir)); } if target.starts_with("../") { @@ -512,6 +515,22 @@ mod tests { assert_eq!(t.path_prefix, "docs//"); } + #[test] + fn separators_after_the_leading_dot_resolve_under_the_index_root() { + let t = table(".//docs/a.md", ¬hing_is_a_dir); + assert_eq!(t.root, Path::new("/index/docs")); + assert_eq!(t.glob, "a.md"); + assert_eq!(t.path_prefix, "docs"); + assert_eq!( + table(".///docs", &everything_is_a_dir), + table("./docs", &everything_is_a_dir) + ); + assert_eq!( + table(".//", ¬hing_is_a_dir), + table("./", ¬hing_is_a_dir) + ); + } + #[test] fn a_current_directory_component_in_the_literal_prefix_is_kept_as_written() { let t = table("./docs/./*.md", ¬hing_is_a_dir); diff --git a/packages/rust/tests/path_table_cli_e2e.rs b/packages/rust/tests/path_table_cli_e2e.rs index f3913a8e..bfada7e9 100644 --- a/packages/rust/tests/path_table_cli_e2e.rs +++ b/packages/rust/tests/path_table_cli_e2e.rs @@ -425,6 +425,25 @@ fn a_brace_sequence_expands_like_bash() { assert_eq!(paths(&out), vec!["br/a1.md", "br/a3.md"]); } +#[test] +fn a_doubled_separator_after_the_dot_resolves_under_the_working_directory() { + let dir = nested(); + + assert_eq!( + paths(&run(&dir, "SELECT path FROM './/folder/a.md'")), + vec!["folder/a.md"] + ); + assert_eq!( + paths(&run(&dir, "SELECT path FROM './/folder'")), + vec!["folder/a.md"] + ); + assert_eq!( + paths(&run(&dir, "SELECT path FROM './/folder/*.md'")), + vec!["folder/a.md"] + ); + assert_eq!(paths(&run(&dir, "SELECT path FROM './/'")), vec!["root.md"]); +} + #[cfg(unix)] mod parent_through_a_symlink { use super::*; diff --git a/packages/rust/tests/path_table_query.rs b/packages/rust/tests/path_table_query.rs index 5c5227cd..ef21ec96 100644 --- a/packages/rust/tests/path_table_query.rs +++ b/packages/rust/tests/path_table_query.rs @@ -416,3 +416,15 @@ fn rows_folded_from_a_path_table_are_the_rows_sqlite_steps() { assert_eq!(folded, stepped, "{sql}"); } } + +#[test] +fn a_doubled_separator_after_the_dot_resolves_under_the_index_root() { + let root = fixture(); + let db = open(&root); + + let file = db.query("SELECT path FROM './/docs/a.md'").unwrap(); + let glob = db.query("SELECT path FROM './/docs/*.md'").unwrap(); + + assert_eq!(texts(&file, "path"), vec!["docs/a.md"]); + assert_eq!(texts(&glob, "path"), vec!["docs/a.md", "docs/b.md"]); +} diff --git a/packages/ts/e2e-attestations/bot-1579-dot-double-slash-1104.json b/packages/ts/e2e-attestations/bot-1579-dot-double-slash-1104.json new file mode 100644 index 00000000..fd563c31 --- /dev/null +++ b/packages/ts/e2e-attestations/bot-1579-dot-double-slash-1104.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1793819260, + "exit_code": 0, + "commit": "b2f173f175000fc92a0385a50ef943bb59c2e7f3", + "branch": "bot/1579-dot-double-slash-1104" +}