Skip to content

Commit d735e9f

Browse files
Alex Jonesclaude
andcommitted
Fix .juliabundleignore: directory patterns, comments, and content exclusion
Three bugs in the bundleignore parser: 1. Directory patterns (e.g. "output-data/") were compiled into Glob.FilenameMatch which only matched the literal string, not files inside the directory. A pattern "foo/" would exclude the entry "foo/" but NOT "foo/bar.jl". Fix: plain directory prefixes (no glob chars) are matched via startswith(). Glob-containing directory patterns (e.g. "*/bar/") emit both the dir match and a "*/bar/*" contents match. 2. Comment lines (starting with #) and blank lines were compiled into Glob.FilenameMatch patterns, potentially causing spurious matches. Fix: _parse_bundleignore_line() skips comments and blanks. 3. A file named "foo.csv" could be incorrectly excluded by a directory pattern "foo.csv/" because joinpath("foo.csv", "") == "foo.csv/". Fix: the rpath_dir startswith check is only applied when the path is actually a directory (isdir guard). Updates existing tests to reflect the corrected behavior: files inside directories matched by "*/bar/" patterns are now properly excluded. Relates to #130 (negated paths — this fix is a prerequisite, cleaning up the same code path). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 60ed8bb commit d735e9f

2 files changed

Lines changed: 56 additions & 22 deletions

File tree

src/PackageBundler/utils.jl

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,46 @@ function is_subpath(dir, subpath)
7373
return startswith(norm_subpath, norm_dir)
7474
end
7575

76+
"""
77+
_parse_bundleignore_line(line) -> Vector of patterns (Glob.FilenameMatch or String), or nothing
78+
79+
Parse a single line from a `.juliabundleignore` file. Returns `nothing` for
80+
blank lines and comments (lines starting with `#`).
81+
82+
For directory patterns (ending with `/`):
83+
- If the pattern contains glob characters (`*`, `?`, `[`), emit both the
84+
directory glob itself AND a `<pattern>*` glob to match contents.
85+
- If it's a plain prefix (e.g. `output-data/`), emit it as a `String` for
86+
`startswith` matching.
87+
88+
All other lines are compiled into `Glob.FilenameMatch` patterns.
89+
"""
90+
function _parse_bundleignore_line(line)
91+
s = strip(line)
92+
(isempty(s) || startswith(s, '#')) && return nothing
93+
if endswith(s, '/')
94+
has_glob = any(c -> c in ('*', '?', '['), s)
95+
if has_glob
96+
# Glob directory pattern: match both the dir and its contents
97+
return [Glob.FilenameMatch(s), Glob.FilenameMatch(s * "*")]
98+
else
99+
# Plain directory prefix: use startswith matching
100+
return [String(s)]
101+
end
102+
end
103+
return [Glob.FilenameMatch(s)]
104+
end
105+
76106
function get_bundleignore(file, top)
77107
dir = dirname(file)
78108
patterns = Set{Any}()
79109
try
80110
while true
81111
if isfile(joinpath(dir, ".juliabundleignore"))
82-
union!(
83-
patterns,
84-
Glob.FilenameMatch.(strip.(readlines(joinpath(dir, ".juliabundleignore")))),
85-
)
112+
for line in readlines(joinpath(dir, ".juliabundleignore"))
113+
parsed = _parse_bundleignore_line(line)
114+
parsed !== nothing && union!(patterns, parsed)
115+
end
86116
return patterns, dir
87117
end
88118
if dir == dirname(dir) || dir == top
@@ -117,16 +147,22 @@ function path_filterer(top)
117147

118148
patterns, ignorepath = get_bundleignore(path, top)
119149

120-
rpath = relpath(path, ignorepath)
121-
122-
return !(
123-
any(p -> occursin(p, sanitize_windows_path(rpath)), patterns) ||
124-
# directories specifically can be excluded by patterns that end with a
125-
# path separator, and to match them in case `path` does not have that
126-
# path separator appended, we append it ourselves before matching
127-
isdir(path) &&
128-
any(p -> occursin(p, sanitize_windows_path(joinpath(rpath, ""))), patterns)
129-
)
150+
rpath = sanitize_windows_path(relpath(path, ignorepath))
151+
# Ensure rpath uses forward slashes for consistent matching
152+
rpath_dir = sanitize_windows_path(joinpath(relpath(path, ignorepath), ""))
153+
154+
return !(any(patterns) do p
155+
if p isa Glob.FilenameMatch
156+
# Glob pattern: match against the relative path
157+
occursin(p, rpath) || (isdir(path) && occursin(p, rpath_dir))
158+
else
159+
# String directory prefix (e.g. "script/experiment/"):
160+
# exclude if the relative path starts with the prefix.
161+
# Only match the rpath_dir form for actual directories,
162+
# to avoid "foo.csv/" matching a FILE named "foo.csv".
163+
startswith(rpath, p) || (isdir(path) && startswith(rpath_dir, p))
164+
end
165+
end)
130166
end
131167
end
132168

test/packagebundler.jl

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,9 @@ end
240240
@test !pred(joinpath(dir, "Pkg3", "test", "foo"))
241241
@test pred(joinpath(dir, "Pkg3", "test", "fooo", "test"))
242242

243-
# Note: even though the test/foo and test/bar directories are
244-
# excluded, the predicate function does not return false if you
245-
# check for file within the directories.
246-
@test pred(joinpath(dir, "Pkg3", "test", "bar", "test"))
243+
# Directory patterns (*/bar/) now correctly exclude contents
244+
@test !pred(joinpath(dir, "Pkg3", "test", "bar", "test"))
245+
# */foo is a file glob (no trailing /), so only matches the file, not contents
247246
@test pred(joinpath(dir, "Pkg3", "test", "foo", "test"))
248247
end
249248
@testset "toplevel" begin
@@ -259,10 +258,9 @@ end
259258
@test !pred(joinpath(dir, "test", "foo"))
260259
@test pred(joinpath(dir, "test", "fooo", "test"))
261260

262-
# Note: even though the test/foo and test/bar directories are
263-
# excluded, the predicate function does not return false if you
264-
# check for file within the directories.
265-
@test pred(joinpath(dir, "test", "bar", "test"))
261+
# Directory patterns (*/bar/) now correctly exclude contents
262+
@test !pred(joinpath(dir, "test", "bar", "test"))
263+
# */foo is a file glob (no trailing /), so only matches the file, not contents
266264
@test pred(joinpath(dir, "test", "foo", "test"))
267265
end
268266
end

0 commit comments

Comments
 (0)