Source code for contree_sdk.docker.dockerignore
"""Parse and match `.dockerignore` rules against build-context paths.
Rules are matched in order against POSIX-style paths relative to the context
root. The last matching rule wins (`!` re-includes a previously ignored
path). Globs: `*` matches anything except `/`, `**` matches zero or more
path components, `?` matches one character, `[...]` is a character class.
Only a pattern containing `**` (explicit) crosses directory depth; a bare
pattern with no `/` at all (e.g. `secret.txt`) is anchored to the context
root, matching Docker's own documented `.dockerignore` behavior.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
[docs]
@dataclass(frozen=True)
class DockerignoreRule:
"""One compiled `.dockerignore` line."""
negate: bool
regex: re.Pattern[str]
raw: str
[docs]
def parse_dockerignore(context_dir: Path) -> tuple[DockerignoreRule, ...]:
path = context_dir / ".dockerignore"
if not path.is_file():
return ()
rules: list[DockerignoreRule] = []
for raw_line in path.read_text().splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
negate = line.startswith("!")
if negate:
line = line[1:].strip()
rules.append(DockerignoreRule(negate=negate, regex=ignored_pattern(line), raw=raw_line))
return tuple(rules)
[docs]
def is_ignored(rel_path: str, rules: tuple[DockerignoreRule, ...]) -> bool:
# rules apply in order; the last match wins (negation re-includes)
ignored = False
for rule in rules:
if rule.regex.fullmatch(rel_path):
ignored = not rule.negate
return ignored
def ignored_pattern(line: str) -> re.Pattern[str]:
# trailing / = dir+contents, ** = any path depth, * = one segment, ? = one char, [...] = class
is_dir = line.endswith("/")
if is_dir:
line = line.rstrip("/")
line = line.lstrip("/")
out: list[str] = []
i = 0
while i < len(line):
match line[i : i + 3], line[i : i + 2], line[i]:
case ("**/", _, _):
out.append("(?:.*/)?")
i += 3
case (_, "**", _):
out.append(".*")
i += 2
case (_, _, "*"):
out.append("[^/]*")
i += 1
case (_, _, "?"):
out.append("[^/]")
i += 1
case (_, _, "["):
end = line.find("]", i + 1)
if end == -1:
out.append(re.escape("["))
i += 1
else:
out.append(line[i : end + 1])
i = end + 1
case (_, _, "/"):
out.append("/")
i += 1
case (_, _, ch):
out.append(re.escape(ch))
i += 1
regex = "".join(out)
if is_dir:
regex += "(?:/.*)?"
return re.compile(regex)