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.