reposnap 0.6.2__tar.gz → 0.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. reposnap-0.6.4/.coverage +0 -0
  2. reposnap-0.6.4/.github/workflows/python-package.yml +32 -0
  3. reposnap-0.6.4/.github/workflows/release.yml +52 -0
  4. {reposnap-0.6.2 → reposnap-0.6.4}/.gitignore +3 -0
  5. reposnap-0.6.4/.pre-commit-config.yaml +47 -0
  6. reposnap-0.6.4/.vscode/launch.json +13 -0
  7. reposnap-0.6.4/CONTRIBUTING.md +155 -0
  8. {reposnap-0.6.2 → reposnap-0.6.4}/PKG-INFO +1 -1
  9. {reposnap-0.6.2 → reposnap-0.6.4}/pyproject.toml +9 -2
  10. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/controllers/project_controller.py +110 -72
  11. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/core/git_repo.py +0 -1
  12. reposnap-0.6.4/reposnap/core/markdown_generator.py +91 -0
  13. reposnap-0.6.4/reposnap/interfaces/cli.py +56 -0
  14. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/interfaces/gui.py +29 -26
  15. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/models/file_tree.py +22 -9
  16. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/utils/path_utils.py +5 -3
  17. {reposnap-0.6.2 → reposnap-0.6.4}/requirements-dev.lock +12 -12
  18. {reposnap-0.6.2 → reposnap-0.6.4}/requirements.lock +0 -8
  19. {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_cli.py +25 -25
  20. reposnap-0.6.4/tests/reposnap/test_collected_tree.py +149 -0
  21. {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_file_system.py +16 -15
  22. {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_file_tree.py +5 -15
  23. {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_git_repo.py +6 -6
  24. {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_gui.py +27 -25
  25. {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_markdown_generator.py +31 -21
  26. reposnap-0.6.4/tests/reposnap/test_path_utils.py +11 -0
  27. reposnap-0.6.4/tests/reposnap/test_project_controller.py +352 -0
  28. reposnap-0.6.4/tests/resources/another_existing_file.py +1 -0
  29. reposnap-0.6.4/tests/resources/existing_file.py +1 -0
  30. reposnap-0.6.2/src/reposnap/core/markdown_generator.py +0 -79
  31. reposnap-0.6.2/src/reposnap/interfaces/cli.py +0 -37
  32. reposnap-0.6.2/tests/reposnap/test_path_utils.py +0 -15
  33. reposnap-0.6.2/tests/reposnap/test_project_controller.py +0 -311
  34. reposnap-0.6.2/tests/resources/another_existing_file.py +0 -1
  35. reposnap-0.6.2/tests/resources/existing_file.py +0 -1
  36. {reposnap-0.6.2 → reposnap-0.6.4}/.python-version +0 -0
  37. {reposnap-0.6.2 → reposnap-0.6.4}/LICENSE +0 -0
  38. {reposnap-0.6.2 → reposnap-0.6.4}/README.md +0 -0
  39. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/__init__.py +0 -0
  40. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/controllers/__init__.py +0 -0
  41. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/core/__init__.py +0 -0
  42. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/core/file_system.py +0 -0
  43. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/interfaces/__init__.py +0 -0
  44. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/models/__init__.py +0 -0
  45. {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/utils/__init__.py +0 -0
  46. {reposnap-0.6.2 → reposnap-0.6.4}/tests/__init__.py +0 -0
  47. {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/__init__.py +0 -0
Binary file
@@ -0,0 +1,32 @@
1
+ name: CI (Rye)
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ branches: ["main"]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout repository
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Install Rye
18
+ uses: eifinger/setup-rye@v4
19
+ with:
20
+ enable-cache: true
21
+
22
+ - name: Sync dependencies & install package
23
+ run: |
24
+ rye sync
25
+
26
+ - name: Lint
27
+ run: |
28
+ rye lint
29
+
30
+ - name: Test
31
+ run: |
32
+ rye test
@@ -0,0 +1,52 @@
1
+ name: Release to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v[0-9]+.[0-9]+.[0-9]+*' # any SemVer tag, eg v1.2.3 or v1.2.3-rc.1
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ runs-on: ubuntu-latest
11
+
12
+ # Block publication unless the normal CI passed on this commit
13
+ needs: build # <— refers to the existing job id in python-package.yml
14
+
15
+ permissions:
16
+ contents: read # principle of least privilege
17
+ id-token: write # enable OIDC if you switch to trusted publishing later :contentReference[oaicite:1]{index=1}
18
+
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Install Rye
24
+ uses: eifinger/setup-rye@v4
25
+ with:
26
+ enable-cache: true
27
+
28
+ - name: Build wheel & sdist
29
+ run: |
30
+ rye sync
31
+ rye build --clean # Hatchling is the backend; Rye shells out to it :contentReference[oaicite:2]{index=2}
32
+
33
+ - name: Verify metadata (optional)
34
+ run: |
35
+ rye run -e 'twine>=5' twine check dist/* :contentReference[oaicite:3]{index=3}
36
+
37
+ # Select real PyPI vs TestPyPI
38
+ - name: Set target repository
39
+ id: repository
40
+ run: |
41
+ if [[ "${GITHUB_REF_NAME}" == *"-"* ]]; then
42
+ echo "index_url=https://test.pypi.org/legacy/" >> "$GITHUB_OUTPUT"
43
+ else
44
+ echo "index_url=https://upload.pypi.org/legacy/" >> "$GITHUB_OUTPUT"
45
+ fi
46
+
47
+ - name: Publish
48
+ uses: pypa/gh-action-pypi-publish@release/v1
49
+ with:
50
+ password: ${{ secrets.PYPI_API_TOKEN }}
51
+ repository-url: ${{ steps.repository.outputs.index_url }}
52
+ skip-existing: true # idempotent re-runs :contentReference[oaicite:4]{index=4}
@@ -12,3 +12,6 @@ wheels/
12
12
  output.md
13
13
  pytest.ini
14
14
  .envrc
15
+
16
+ # pytest
17
+ .pytest_cache/
@@ -0,0 +1,47 @@
1
+ # Pre‑commit configuration for **RepoSnap**
2
+ # Install hooks with:
3
+ # rye add --dev pre-commit
4
+ # pre-commit install
5
+ #
6
+ # Hooks are kept minimal and fast; they rely on Rye’s built‑in commands.
7
+
8
+ repos:
9
+ # Generic housekeeping hooks
10
+ - repo: https://github.com/pre-commit/pre-commit-hooks
11
+ rev: v4.5.0
12
+ hooks:
13
+ - id: end-of-file-fixer
14
+ - id: trailing-whitespace
15
+ - id: check-yaml
16
+ - id: check-added-large-files
17
+ - id: check-merge-conflict
18
+
19
+ # Project‑specific commands driven by Rye
20
+ - repo: local
21
+ hooks:
22
+ - id: rye-fmt
23
+ name: rye fmt --check
24
+ entry: rye
25
+ language: system
26
+ args: ["fmt", "--check"]
27
+ pass_filenames: false
28
+
29
+ - id: rye-lint
30
+ name: rye lint
31
+ entry: rye
32
+ language: system
33
+ args: ["lint"]
34
+ pass_filenames: false
35
+
36
+ - id: rye-test
37
+ name: rye test (quick)
38
+ entry: rye
39
+ language: system
40
+ args: ["test", "-q"]
41
+ pass_filenames: false
42
+ always_run: true
43
+ # Skip if only docs/markdown changed
44
+ files: "^(src/|tests/)"
45
+
46
+ # Run hooks only on commit; skip CI or manual stages unless invoked.
47
+ default_stages: [commit]
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Python Debugger: Run reposnap",
6
+ "type": "debugpy",
7
+ "request": "launch",
8
+ "module": "reposnap.interfaces.cli",
9
+ "console": "integratedTerminal",
10
+ "args": ["src/reposnap/controllers", "src/reposnap/interfaces", "README.md", "tests/reposnap"]
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,155 @@
1
+ # Contributing to **RepoSnap**
2
+
3
+ ---
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Getting Started](#getting-started)
8
+ 2. [Development Workflow](#development-workflow)
9
+ 3. [Coding Standards](#coding-standards)
10
+ 4. [Testing Guidelines](#testing-guidelines)
11
+ 5. [Pull‑Request Checklist](#pull-request-checklist)
12
+ 6. [Release Process](#release-process)
13
+ 7. [Security & Responsible Disclosure](#security--responsible-disclosure)
14
+ 8. [Code of Conduct](#code-of-conduct)
15
+
16
+ ---
17
+
18
+ ## Getting Started
19
+
20
+ ### 1. Fork & Clone
21
+
22
+ ```bash
23
+ git clone https://github.com/<your-handle>/reposnap.git
24
+ cd reposnap
25
+ ```
26
+
27
+ ### 2. Python 3.12 Environment
28
+
29
+ We pin to **Python 3.12.6** (see `.python-version`). If you are not using Rye’s built‑in virtual‑env management, create one manually:
30
+
31
+ ```bash
32
+ python -m venv .venv
33
+ source .venv/bin/activate
34
+ ```
35
+
36
+ ### 3. Install Dependencies
37
+
38
+ **Using Rye (recommended)**
39
+
40
+ ```bash
41
+ rye sync # installs runtime + dev deps and refreshes lockfiles
42
+ ```
43
+
44
+ *Fallback — vanilla tooling only*
45
+
46
+ ```bash
47
+ pip install -e .[dev] # editable install with dev extras (pytest, etc.)
48
+ ```
49
+
50
+ ### 4. Optional Pre‑Commit Hooks
51
+
52
+ Automate formatting and linting on every commit with [pre‑commit](https://pre-commit.com/):
53
+
54
+ ```bash
55
+ pre-commit install # install hooks into .git
56
+ ```
57
+
58
+ The default hook runs `rye fmt --check` and `rye lint` before each commit. Skip this step if you prefer manual control.
59
+
60
+ ---
61
+
62
+ ## Development Workflow
63
+
64
+ 1. **Create a branch**
65
+
66
+ * Feature → `feat/<short-slug>`
67
+ * Bugfix → `fix/<issue-number>`
68
+ 2. **Make focused changes** inside `src/` (and matching tests under `tests/`).
69
+ 3. **Run the full test suite**: `rye test -q`.
70
+ 4. **Format & lint**: `rye fmt` then `rye lint --fix` (to auto‑apply safe fixes).
71
+ 5. **Commit, push, and open a Pull Request**.
72
+
73
+ > CI mirrors these steps. A failing check blocks merging.
74
+
75
+ ### Architectural Boundaries
76
+
77
+ ```
78
+ interfaces ─┐ (CLI, GUI)
79
+ controllers ─┼─> orchestrate use cases
80
+ core ─┼─> pure, reusable logic (no side‑effects)
81
+ models ─┘ data structures (dumb, typed)
82
+ ```
83
+
84
+ * **Never import from a higher layer.**
85
+ * Keep `utils/` minimal — prefer domain‑specific modules in `core/`.
86
+
87
+ ---
88
+
89
+ ## Coding Standards
90
+
91
+ RepoSnap relies on **Rye’s built‑in helpers** for code quality:
92
+
93
+ | Command | Description |
94
+ | ---------- | --------------------------------------------- |
95
+ | `rye fmt` | Formats code (powered under‑the‑hood by Ruff) |
96
+ | `rye lint` | Lints and can auto‑fix issues with `--fix` |
97
+
98
+ Guidelines:
99
+
100
+ * Follow **PEP 8**; `rye lint` will flag deviations.
101
+ * All public functions **must** be type‑hinted.
102
+ * Use **Google‑style** docstrings (`Args:`, `Returns:`).
103
+ * Log via `logging`; avoid `print()` in library code.
104
+ * Prefer `pathlib.Path` over `os.path`.
105
+
106
+ ---
107
+
108
+ ## Testing Guidelines
109
+
110
+ * Framework: **pytest**.
111
+ * Mirror `src/` structure under `tests/`.
112
+ * **Minimum coverage:** 90 %. New code should raise the bar.
113
+ * Write *unit tests* for pure functions and *integration tests* for I/O flows (e.g., CLI interaction).
114
+ * Measure coverage locally: `pytest --cov=reposnap`.
115
+
116
+ ---
117
+
118
+ ## Pull‑Request Checklist
119
+
120
+ Before requesting review, confirm:
121
+
122
+ * [ ] Tests pass (`rye test -q`) and coverage ≥ 90 %.
123
+ * [ ] `rye fmt --check` and `rye lint` report no issues.
124
+ * [ ] Docstrings & docs updated where relevant.
125
+ * [ ] No new circular dependencies (`pipdeptree --warn`).
126
+ * [ ] CI is green.
127
+ * [ ] PR description explains **why** the change is needed.
128
+ * [ ] Commits follow Conventional Commits.
129
+
130
+ At least one core maintainer must approve before merging. We use **Squash & Merge** to keep history linear.
131
+
132
+ ---
133
+
134
+ ## Release Process
135
+
136
+ 1. Merge PRs into `main`.
137
+ 2. Bump version manually in [pyproject.toml](pyproject.toml).
138
+ 3. Tag release: `git tag vX.Y.Z && git push --tags`.
139
+ 4. CI publishes to PyPI.
140
+
141
+ We adhere to **Semantic Versioning 2.0.0**.
142
+
143
+ ---
144
+
145
+ ## Security & Responsible Disclosure
146
+
147
+ If you discover a vulnerability, email [maintainer@example.com](mailto:maintainer@example.com) **privately**. We will coordinate a fix and issue a CVE if appropriate.
148
+
149
+ ---
150
+
151
+ ## Code of Conduct
152
+
153
+ Participation in this project is covered by the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
154
+
155
+ ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reposnap
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Generate a Markdown file with all contents of your project
5
5
  Author: agoloborodko
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reposnap"
3
- version = "0.6.2"
3
+ version = "0.6.4"
4
4
  description = "Generate a Markdown file with all contents of your project"
5
5
  authors = [
6
6
  { name = "agoloborodko" }
@@ -22,13 +22,20 @@ build-backend = "hatchling.build"
22
22
  managed = true
23
23
  dev-dependencies = [
24
24
  "pytest>=8.3.2",
25
+ "pre-commit>=4.2.0",
26
+ "pytest-cov>=6.2.1",
25
27
  ]
26
28
 
27
29
  [tool.hatch.metadata]
28
30
  allow-direct-references = true
29
31
 
32
+ [tool.hatch.build]
33
+ sources = ["src"]
34
+ dev-mode-dirs = ["src"]
35
+
30
36
  [tool.hatch.build.targets.wheel]
31
- packages = ["src/reposnap"]
37
+ packages = ["reposnap"]
38
+ include = ["reposnap"]
32
39
 
33
40
  [project.scripts]
34
41
  reposnap = "reposnap.interfaces.cli:main"
@@ -1,45 +1,57 @@
1
- # src/reposnap/controllers/project_controller.py
2
-
3
1
  import logging
4
2
  from pathlib import Path
5
3
  from reposnap.core.file_system import FileSystem
6
- from reposnap.core.markdown_generator import MarkdownGenerator
7
4
  from reposnap.models.file_tree import FileTree
8
5
  import pathspec
9
6
  from typing import List, Optional
10
7
 
8
+
11
9
  class ProjectController:
12
10
  def __init__(self, args: Optional[object] = None):
13
11
  self.logger = logging.getLogger(__name__)
12
+ # Always determine repository root using Git (or cwd)
13
+ self.root_dir = self._get_repo_root().resolve()
14
14
  if args:
15
- # Support both new 'paths' (multiple paths) and legacy 'path'
16
- if hasattr(args, 'paths'):
17
- input_paths = [Path(p) for p in args.paths]
18
- else:
19
- input_paths = [Path(args.path)]
20
15
  self.args = args
21
- # Determine repository root (using provided paths’ common parent if available)
22
- self.root_dir = self._get_repo_root().resolve()
23
- # Convert provided paths to be relative to the repository root.
16
+ # Treat positional arguments as literal file/directory names.
17
+ input_paths = [
18
+ Path(p) for p in (args.paths if hasattr(args, "paths") else [args.path])
19
+ ]
24
20
  self.input_paths = []
25
21
  for p in input_paths:
26
- try:
27
- candidate = (self.root_dir / p).resolve()
28
- rel = candidate.relative_to(self.root_dir)
29
- if rel != Path('.'):
30
- self.input_paths.append(rel)
31
- except ValueError:
32
- self.logger.warning(f"Path {p} is not under repository root {self.root_dir}. Ignoring.")
33
- self.output_file: Path = Path(args.output).resolve() if args.output else self.root_dir / 'output.md'
34
- self.structure_only: bool = args.structure_only if hasattr(args, 'structure_only') else False
35
- self.include_patterns: List[str] = args.include if hasattr(args, 'include') else []
36
- self.exclude_patterns: List[str] = args.exclude if hasattr(args, 'exclude') else []
22
+ candidate = (self.root_dir / p).resolve()
23
+ if candidate.exists():
24
+ try:
25
+ rel = candidate.relative_to(self.root_dir)
26
+ if rel != Path("."):
27
+ self.input_paths.append(rel)
28
+ except ValueError:
29
+ self.logger.warning(
30
+ f"Path {p} is not under repository root {self.root_dir}. Ignoring."
31
+ )
32
+ else:
33
+ self.logger.warning(
34
+ f"Path {p} does not exist relative to repository root {self.root_dir}."
35
+ )
36
+ self.output_file: Path = (
37
+ Path(args.output).resolve()
38
+ if args.output
39
+ else self.root_dir / "output.md"
40
+ )
41
+ self.structure_only: bool = (
42
+ args.structure_only if hasattr(args, "structure_only") else False
43
+ )
44
+ self.include_patterns: List[str] = (
45
+ args.include if hasattr(args, "include") else []
46
+ )
47
+ self.exclude_patterns: List[str] = (
48
+ args.exclude if hasattr(args, "exclude") else []
49
+ )
37
50
  else:
38
- self.root_dir = Path('.').resolve()
51
+ self.args = None
39
52
  self.input_paths = []
40
- self.output_file = Path('output.md').resolve()
53
+ self.output_file = self.root_dir / "output.md"
41
54
  self.structure_only = False
42
- self.args = None
43
55
  self.include_patterns = []
44
56
  self.exclude_patterns = []
45
57
  self.file_tree: Optional[FileTree] = None
@@ -49,31 +61,18 @@ class ProjectController:
49
61
 
50
62
  def _get_repo_root(self) -> Path:
51
63
  """
52
- Determine the repository root. If arguments were provided and those paths exist,
53
- use the common parent directory of all provided paths. Otherwise, fall back to the
54
- git repository working tree directory (or current directory if not a git repo).
64
+ Determine the repository root using Git if available,
65
+ otherwise use the current directory.
55
66
  """
56
- if self.args is not None:
57
- candidate_paths = []
58
- if hasattr(self.args, 'paths'):
59
- for p in self.args.paths:
60
- candidate = Path(p).resolve()
61
- if candidate.exists():
62
- candidate_paths.append(candidate)
63
- elif hasattr(self.args, 'path'):
64
- candidate = Path(self.args.path).resolve()
65
- if candidate.exists():
66
- candidate_paths.append(candidate)
67
- if candidate_paths:
68
- from os.path import commonpath
69
- common = Path(commonpath([str(p) for p in candidate_paths]))
70
- return common
71
67
  from git import Repo, InvalidGitRepositoryError
68
+
72
69
  try:
73
70
  repo = Repo(Path.cwd(), search_parent_directories=True)
74
71
  return Path(repo.working_tree_dir).resolve()
75
72
  except InvalidGitRepositoryError:
76
- self.logger.warning("Not a git repository. Using current directory as root.")
73
+ self.logger.warning(
74
+ "Not a git repository. Using current directory as root."
75
+ )
77
76
  return Path.cwd().resolve()
78
77
 
79
78
  def set_root_dir(self, root_dir: Path) -> None:
@@ -85,43 +84,65 @@ class ProjectController:
85
84
 
86
85
  def _apply_include_exclude(self, files: List[Path]) -> List[Path]:
87
86
  """Filter a list of file paths using include and exclude patterns."""
87
+
88
88
  def adjust_patterns(patterns):
89
89
  adjusted = []
90
90
  for p in patterns:
91
- if any(ch in p for ch in ['*', '?', '[']):
91
+ if any(ch in p for ch in ["*", "?", "["]):
92
92
  adjusted.append(p)
93
93
  else:
94
- adjusted.append(f'*{p}*')
94
+ adjusted.append(f"*{p}*")
95
95
  return adjusted
96
+
96
97
  if self.include_patterns:
97
98
  inc = adjust_patterns(self.include_patterns)
98
- spec_inc = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, inc)
99
+ spec_inc = pathspec.PathSpec.from_lines(
100
+ pathspec.patterns.GitWildMatchPattern, inc
101
+ )
99
102
  files = [f for f in files if spec_inc.match_file(f.as_posix())]
100
103
  if self.exclude_patterns:
101
104
  exc = adjust_patterns(self.exclude_patterns)
102
- spec_exc = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, exc)
105
+ spec_exc = pathspec.PathSpec.from_lines(
106
+ pathspec.patterns.GitWildMatchPattern, exc
107
+ )
103
108
  files = [f for f in files if not spec_exc.match_file(f.as_posix())]
104
109
  return files
105
110
 
106
111
  def collect_file_tree(self) -> None:
107
- self.logger.info("Collecting files by walking the repository root.")
108
- all_files = []
109
- for path in self.root_dir.rglob("*"):
110
- if path.is_file():
111
- try:
112
- rel = path.relative_to(self.root_dir)
113
- all_files.append(rel)
114
- except ValueError:
115
- continue
116
- # Apply include/exclude filtering.
112
+ self.logger.info("Collecting files from Git tracked files if available.")
113
+ try:
114
+ from reposnap.core.git_repo import GitRepo
115
+
116
+ git_repo = GitRepo(self.root_dir)
117
+ all_files = git_repo.get_git_files()
118
+ self.logger.debug(f"Git tracked files: {all_files}")
119
+ except Exception as e:
120
+ self.logger.warning(f"Error obtaining Git tracked files: {e}.")
121
+ all_files = []
122
+ # If Git returns an empty list but files exist on disk, fall back to filesystem scan.
123
+ if not all_files:
124
+ file_list = [p for p in self.root_dir.rglob("*") if p.is_file()]
125
+ if file_list:
126
+ self.logger.info(
127
+ "Git tracked files empty, using filesystem scan fallback."
128
+ )
129
+ all_files = []
130
+ for path in file_list:
131
+ try:
132
+ rel = path.relative_to(self.root_dir)
133
+ all_files.append(rel)
134
+ except ValueError:
135
+ continue
117
136
  all_files = self._apply_include_exclude(all_files)
118
- self.logger.debug(f"All files after include/exclude filtering: {all_files}")
137
+ self.logger.debug(f"All files after applying include/exclude: {all_files}")
119
138
  if self.input_paths:
120
139
  trees = []
121
140
  for input_path in self.input_paths:
122
141
  subset = [
123
- f for f in all_files
124
- if f == input_path or f.parts[:len(input_path.parts)] == input_path.parts
142
+ f
143
+ for f in all_files
144
+ if f == input_path
145
+ or list(f.parts[: len(input_path.parts)]) == list(input_path.parts)
125
146
  ]
126
147
  self.logger.debug(f"Files for input path '{input_path}': {subset}")
127
148
  if subset:
@@ -158,30 +179,40 @@ class ProjectController:
158
179
 
159
180
  def apply_filters(self) -> None:
160
181
  self.logger.info("Applying .gitignore filters to the merged tree.")
161
- spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, self.gitignore_patterns)
182
+ spec = pathspec.PathSpec.from_lines(
183
+ pathspec.patterns.GitWildMatchPattern, self.gitignore_patterns
184
+ )
162
185
  self.logger.debug(f".gitignore patterns: {self.gitignore_patterns}")
163
186
  self.file_tree.filter_files(spec)
164
187
 
165
188
  def generate_output(self) -> None:
166
189
  self.logger.info("Starting Markdown generation.")
190
+ from reposnap.core.markdown_generator import MarkdownGenerator
191
+
167
192
  markdown_generator = MarkdownGenerator(
168
193
  root_dir=self.root_dir,
169
194
  output_file=self.output_file,
170
- structure_only=self.structure_only
195
+ structure_only=self.structure_only,
196
+ )
197
+ markdown_generator.generate_markdown(
198
+ self.file_tree.structure, self.file_tree.get_all_files()
171
199
  )
172
- markdown_generator.generate_markdown(self.file_tree.structure, self.file_tree.get_all_files())
173
200
  self.logger.info(f"Markdown generated at {self.output_file}.")
174
201
 
175
202
  def generate_output_from_selected(self, selected_files: set) -> None:
176
203
  self.logger.info("Generating Markdown from selected files.")
177
204
  pruned_tree = self.file_tree.prune_tree(selected_files)
205
+ from reposnap.core.markdown_generator import MarkdownGenerator
206
+
178
207
  markdown_generator = MarkdownGenerator(
179
208
  root_dir=self.root_dir,
180
209
  output_file=self.output_file,
181
210
  structure_only=False,
182
- hide_untoggled=True
211
+ hide_untoggled=True,
212
+ )
213
+ markdown_generator.generate_markdown(
214
+ pruned_tree, [Path(f) for f in selected_files]
183
215
  )
184
- markdown_generator.generate_markdown(pruned_tree, [Path(f) for f in selected_files])
185
216
  self.logger.info(f"Markdown generated at {self.output_file}.")
186
217
 
187
218
  def run(self) -> None:
@@ -191,18 +222,25 @@ class ProjectController:
191
222
  self.generate_output()
192
223
 
193
224
  def _load_gitignore_patterns(self) -> List[str]:
194
- gitignore_path = self.root_dir / '.gitignore'
225
+ gitignore_path = self.root_dir / ".gitignore"
195
226
  if not gitignore_path.exists():
196
227
  for parent in self.root_dir.parents:
197
- gitignore_path = parent / '.gitignore'
198
- if gitignore_path.exists():
228
+ candidate = parent / ".gitignore"
229
+ if candidate.exists():
230
+ gitignore_path = candidate
199
231
  break
200
232
  else:
201
233
  gitignore_path = None
202
234
  if gitignore_path and gitignore_path.exists():
203
- with gitignore_path.open('r') as gitignore:
204
- patterns = gitignore.readlines()
205
- self.logger.debug(f"Loaded .gitignore patterns from {gitignore_path.parent}: {patterns}")
235
+ with gitignore_path.open("r") as gitignore:
236
+ patterns = [
237
+ line.strip()
238
+ for line in gitignore
239
+ if line.strip() and not line.strip().startswith("#")
240
+ ]
241
+ self.logger.debug(
242
+ f"Loaded .gitignore patterns from {gitignore_path.parent}: {patterns}"
243
+ )
206
244
  return patterns
207
245
  else:
208
246
  self.logger.debug(f"No .gitignore found starting from {self.root_dir}.")
@@ -30,4 +30,3 @@ class GitRepo:
30
30
  except InvalidGitRepositoryError:
31
31
  self.logger.error(f"Invalid Git repository at: {self.repo_path}")
32
32
  return []
33
-