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.
- reposnap-0.6.4/.coverage +0 -0
- reposnap-0.6.4/.github/workflows/python-package.yml +32 -0
- reposnap-0.6.4/.github/workflows/release.yml +52 -0
- {reposnap-0.6.2 → reposnap-0.6.4}/.gitignore +3 -0
- reposnap-0.6.4/.pre-commit-config.yaml +47 -0
- reposnap-0.6.4/.vscode/launch.json +13 -0
- reposnap-0.6.4/CONTRIBUTING.md +155 -0
- {reposnap-0.6.2 → reposnap-0.6.4}/PKG-INFO +1 -1
- {reposnap-0.6.2 → reposnap-0.6.4}/pyproject.toml +9 -2
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/controllers/project_controller.py +110 -72
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/core/git_repo.py +0 -1
- reposnap-0.6.4/reposnap/core/markdown_generator.py +91 -0
- reposnap-0.6.4/reposnap/interfaces/cli.py +56 -0
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/interfaces/gui.py +29 -26
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/models/file_tree.py +22 -9
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/utils/path_utils.py +5 -3
- {reposnap-0.6.2 → reposnap-0.6.4}/requirements-dev.lock +12 -12
- {reposnap-0.6.2 → reposnap-0.6.4}/requirements.lock +0 -8
- {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_cli.py +25 -25
- reposnap-0.6.4/tests/reposnap/test_collected_tree.py +149 -0
- {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_file_system.py +16 -15
- {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_file_tree.py +5 -15
- {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_git_repo.py +6 -6
- {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_gui.py +27 -25
- {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/test_markdown_generator.py +31 -21
- reposnap-0.6.4/tests/reposnap/test_path_utils.py +11 -0
- reposnap-0.6.4/tests/reposnap/test_project_controller.py +352 -0
- reposnap-0.6.4/tests/resources/another_existing_file.py +1 -0
- reposnap-0.6.4/tests/resources/existing_file.py +1 -0
- reposnap-0.6.2/src/reposnap/core/markdown_generator.py +0 -79
- reposnap-0.6.2/src/reposnap/interfaces/cli.py +0 -37
- reposnap-0.6.2/tests/reposnap/test_path_utils.py +0 -15
- reposnap-0.6.2/tests/reposnap/test_project_controller.py +0 -311
- reposnap-0.6.2/tests/resources/another_existing_file.py +0 -1
- reposnap-0.6.2/tests/resources/existing_file.py +0 -1
- {reposnap-0.6.2 → reposnap-0.6.4}/.python-version +0 -0
- {reposnap-0.6.2 → reposnap-0.6.4}/LICENSE +0 -0
- {reposnap-0.6.2 → reposnap-0.6.4}/README.md +0 -0
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/__init__.py +0 -0
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/controllers/__init__.py +0 -0
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/core/__init__.py +0 -0
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/core/file_system.py +0 -0
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/interfaces/__init__.py +0 -0
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/models/__init__.py +0 -0
- {reposnap-0.6.2/src → reposnap-0.6.4}/reposnap/utils/__init__.py +0 -0
- {reposnap-0.6.2 → reposnap-0.6.4}/tests/__init__.py +0 -0
- {reposnap-0.6.2 → reposnap-0.6.4}/tests/reposnap/__init__.py +0 -0
reposnap-0.6.4/.coverage
ADDED
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}
|
@@ -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
|
[project]
|
2
2
|
name = "reposnap"
|
3
|
-
version = "0.6.
|
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 = ["
|
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
|
-
#
|
22
|
-
|
23
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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.
|
51
|
+
self.args = None
|
39
52
|
self.input_paths = []
|
40
|
-
self.output_file =
|
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
|
53
|
-
use 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(
|
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
|
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(
|
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(
|
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
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
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
|
124
|
-
|
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(
|
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 /
|
225
|
+
gitignore_path = self.root_dir / ".gitignore"
|
195
226
|
if not gitignore_path.exists():
|
196
227
|
for parent in self.root_dir.parents:
|
197
|
-
|
198
|
-
if
|
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(
|
204
|
-
patterns =
|
205
|
-
|
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}.")
|