smith.infrastructure.gitignore

Gitignore adapter — manage the smith-managed section in smith.infrastructure.gitignore.

 1"""Gitignore adapter — manage the smith-managed section in .gitignore."""
 2
 3from pathlib import Path
 4
 5from smith.domain.value_objects import GitignoreSection
 6
 7START_MARKER = "# smith managed"
 8END_MARKER = "# end smith managed"
 9
10
11class GitignoreManager:
12    """Read and mutate the smith-managed section of a project's .gitignore."""
13
14    def __init__(self, project_dir: Path) -> None:
15        """Initialise with the project root directory."""
16        self._gitignore = project_dir / ".gitignore"
17
18    def add_section(self, patterns: list[str]) -> None:
19        """Add or replace the smith-managed section in .gitignore."""
20        section = GitignoreSection(patterns=patterns)
21        lines = self._read_lines()
22        if self._find_section_bounds(lines) is not None:
23            self._replace_section(lines, section)
24        else:
25            self._append_section(lines, section)
26
27    def has_section(self) -> bool:
28        """Return whether a smith-managed section exists in .gitignore."""
29        lines = self._read_lines()
30        return self._find_section_bounds(lines) is not None
31
32    def get_patterns(self) -> list[str]:
33        """Return the patterns listed inside the smith-managed section."""
34        lines = self._read_lines()
35        bounds = self._find_section_bounds(lines)
36        if bounds is None:
37            return []
38        start, end = bounds
39        patterns = []
40        for line in lines[start + 1 : end]:
41            stripped = line.strip()
42            if stripped and not stripped.startswith("#"):
43                patterns.append(stripped)
44        return patterns
45
46    def _read_lines(self) -> list[str]:
47        if not self._gitignore.exists():
48            return []
49        return self._gitignore.read_text().splitlines(keepends=True)
50
51    def _write_lines(self, lines: list[str]) -> None:
52        content = "".join(lines)
53        if content and not content.endswith("\n"):
54            content += "\n"
55        self._gitignore.write_text(content)
56
57    def _find_section_bounds(self, lines: list[str]) -> tuple[int, int] | None:
58        start = None
59        for i, line in enumerate(lines):
60            if line.strip().startswith(START_MARKER):
61                start = i
62                break
63        if start is None:
64            return None
65        for i in range(start + 1, len(lines)):
66            if lines[i].strip() == END_MARKER:
67                return (start, i)
68        return None
69
70    def _replace_section(self, lines: list[str], section: GitignoreSection) -> None:
71        bounds = self._find_section_bounds(lines)
72        if bounds is None:
73            self._append_section(lines, section)
74            return
75        start, end = bounds
76        header = lines[start].rstrip("\n")
77        source_match = [p for p in header.split() if p.startswith("source:")]
78        source_part = f" {source_match[0]}" if source_match else ""
79        new_section = [f"{START_MARKER}{source_part}\n"]
80        for p in section.patterns:
81            new_section.append(f"{p}\n")
82        new_section.append(f"{END_MARKER}\n")
83        lines[start : end + 1] = new_section
84        self._write_lines(lines)
85
86    def _append_section(self, lines: list[str], section: GitignoreSection) -> None:
87        new_lines = [f"{START_MARKER}\n"]
88        for p in section.patterns:
89            new_lines.append(f"{p}\n")
90        new_lines.append(f"{END_MARKER}\n")
91        if lines and lines[-1].strip():
92            new_lines.insert(0, "\n")
93        lines.extend(new_lines)
94        self._write_lines(lines)
START_MARKER = '# smith managed'
END_MARKER = '# end smith managed'
class GitignoreManager:
12class GitignoreManager:
13    """Read and mutate the smith-managed section of a project's .gitignore."""
14
15    def __init__(self, project_dir: Path) -> None:
16        """Initialise with the project root directory."""
17        self._gitignore = project_dir / ".gitignore"
18
19    def add_section(self, patterns: list[str]) -> None:
20        """Add or replace the smith-managed section in .gitignore."""
21        section = GitignoreSection(patterns=patterns)
22        lines = self._read_lines()
23        if self._find_section_bounds(lines) is not None:
24            self._replace_section(lines, section)
25        else:
26            self._append_section(lines, section)
27
28    def has_section(self) -> bool:
29        """Return whether a smith-managed section exists in .gitignore."""
30        lines = self._read_lines()
31        return self._find_section_bounds(lines) is not None
32
33    def get_patterns(self) -> list[str]:
34        """Return the patterns listed inside the smith-managed section."""
35        lines = self._read_lines()
36        bounds = self._find_section_bounds(lines)
37        if bounds is None:
38            return []
39        start, end = bounds
40        patterns = []
41        for line in lines[start + 1 : end]:
42            stripped = line.strip()
43            if stripped and not stripped.startswith("#"):
44                patterns.append(stripped)
45        return patterns
46
47    def _read_lines(self) -> list[str]:
48        if not self._gitignore.exists():
49            return []
50        return self._gitignore.read_text().splitlines(keepends=True)
51
52    def _write_lines(self, lines: list[str]) -> None:
53        content = "".join(lines)
54        if content and not content.endswith("\n"):
55            content += "\n"
56        self._gitignore.write_text(content)
57
58    def _find_section_bounds(self, lines: list[str]) -> tuple[int, int] | None:
59        start = None
60        for i, line in enumerate(lines):
61            if line.strip().startswith(START_MARKER):
62                start = i
63                break
64        if start is None:
65            return None
66        for i in range(start + 1, len(lines)):
67            if lines[i].strip() == END_MARKER:
68                return (start, i)
69        return None
70
71    def _replace_section(self, lines: list[str], section: GitignoreSection) -> None:
72        bounds = self._find_section_bounds(lines)
73        if bounds is None:
74            self._append_section(lines, section)
75            return
76        start, end = bounds
77        header = lines[start].rstrip("\n")
78        source_match = [p for p in header.split() if p.startswith("source:")]
79        source_part = f" {source_match[0]}" if source_match else ""
80        new_section = [f"{START_MARKER}{source_part}\n"]
81        for p in section.patterns:
82            new_section.append(f"{p}\n")
83        new_section.append(f"{END_MARKER}\n")
84        lines[start : end + 1] = new_section
85        self._write_lines(lines)
86
87    def _append_section(self, lines: list[str], section: GitignoreSection) -> None:
88        new_lines = [f"{START_MARKER}\n"]
89        for p in section.patterns:
90            new_lines.append(f"{p}\n")
91        new_lines.append(f"{END_MARKER}\n")
92        if lines and lines[-1].strip():
93            new_lines.insert(0, "\n")
94        lines.extend(new_lines)
95        self._write_lines(lines)

Read and mutate the smith-managed section of a project's smith.infrastructure.gitignore.

GitignoreManager(project_dir: pathlib._local.Path)
15    def __init__(self, project_dir: Path) -> None:
16        """Initialise with the project root directory."""
17        self._gitignore = project_dir / ".gitignore"

Initialise with the project root directory.

def add_section(self, patterns: list[str]) -> None:
19    def add_section(self, patterns: list[str]) -> None:
20        """Add or replace the smith-managed section in .gitignore."""
21        section = GitignoreSection(patterns=patterns)
22        lines = self._read_lines()
23        if self._find_section_bounds(lines) is not None:
24            self._replace_section(lines, section)
25        else:
26            self._append_section(lines, section)

Add or replace the smith-managed section in smith.infrastructure.gitignore.

def has_section(self) -> bool:
28    def has_section(self) -> bool:
29        """Return whether a smith-managed section exists in .gitignore."""
30        lines = self._read_lines()
31        return self._find_section_bounds(lines) is not None

Return whether a smith-managed section exists in smith.infrastructure.gitignore.

def get_patterns(self) -> list[str]:
33    def get_patterns(self) -> list[str]:
34        """Return the patterns listed inside the smith-managed section."""
35        lines = self._read_lines()
36        bounds = self._find_section_bounds(lines)
37        if bounds is None:
38            return []
39        start, end = bounds
40        patterns = []
41        for line in lines[start + 1 : end]:
42            stripped = line.strip()
43            if stripped and not stripped.startswith("#"):
44                patterns.append(stripped)
45        return patterns

Return the patterns listed inside the smith-managed section.