reposnap 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
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/core/collector.py +15 -6
- reposnap/core/file_system.py +5 -1
- reposnap/core/git_repo.py +8 -1
- reposnap/core/markdown_generator.py +116 -18
- reposnap/interfaces/cli.py +9 -0
- {reposnap-0.2.0.dist-info → reposnap-0.2.2.dist-info}/METADATA +9 -30
- reposnap-0.2.2.dist-info/RECORD +15 -0
- reposnap-0.2.2.dist-info/entry_points.txt +2 -0
- reposnap-0.2.0.dist-info/RECORD +0 -14
- {reposnap-0.2.0.dist-info → reposnap-0.2.2.dist-info}/WHEEL +0 -0
- {reposnap-0.2.0.dist-info → reposnap-0.2.2.dist-info}/licenses/LICENSE +0 -0
reposnap/core/collector.py
CHANGED
@@ -4,18 +4,27 @@ from .file_system import FileSystem
|
|
4
4
|
from .git_repo import GitRepo
|
5
5
|
from .markdown_generator import MarkdownGenerator
|
6
6
|
import pathspec
|
7
|
+
import logging
|
8
|
+
from pathlib import Path
|
9
|
+
|
7
10
|
|
8
11
|
class ProjectContentCollector:
|
9
12
|
def __init__(self, root_dir: str, output_file: str, structure_only: bool, gitignore_patterns: list):
|
10
|
-
self.root_dir = root_dir
|
11
|
-
self.output_file = output_file
|
13
|
+
self.root_dir = Path(root_dir).resolve()
|
14
|
+
self.output_file = Path(output_file).resolve()
|
12
15
|
self.structure_only = structure_only
|
13
16
|
self.spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, gitignore_patterns)
|
14
|
-
self.file_system = FileSystem(root_dir)
|
15
|
-
self.git_repo = GitRepo(root_dir)
|
16
|
-
self.markdown_generator = MarkdownGenerator(
|
17
|
+
self.file_system = FileSystem(self.root_dir)
|
18
|
+
self.git_repo = GitRepo(self.root_dir)
|
19
|
+
self.markdown_generator = MarkdownGenerator(
|
20
|
+
root_dir=self.root_dir,
|
21
|
+
output_file=self.output_file,
|
22
|
+
structure_only=self.structure_only
|
23
|
+
)
|
24
|
+
self.logger = logging.getLogger(__name__)
|
17
25
|
|
18
26
|
def collect_and_generate(self):
|
27
|
+
self.logger.info("Starting project content collection.")
|
19
28
|
git_files = self.git_repo.get_git_files()
|
20
29
|
tree_structure = self.file_system.build_tree_structure(git_files)
|
21
|
-
self.markdown_generator.generate_markdown(
|
30
|
+
self.markdown_generator.generate_markdown(tree_structure, git_files, self.spec)
|
reposnap/core/file_system.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# src/reposnap/core/file_system.py
|
2
2
|
|
3
|
+
import logging
|
3
4
|
from pathlib import Path
|
4
5
|
|
5
6
|
class FileSystem:
|
@@ -8,12 +9,15 @@ class FileSystem:
|
|
8
9
|
|
9
10
|
def build_tree_structure(self, files):
|
10
11
|
tree = {}
|
12
|
+
logging.debug("\n>>> Processing Files for Tree Structure <<<")
|
11
13
|
for file in files:
|
12
|
-
file_path =
|
14
|
+
file_path = (self.root_dir / file).resolve()
|
15
|
+
logging.debug(f"Processing file:\n File Path: {file_path}\n Root Dir: {self.root_dir}")
|
13
16
|
relative_path = file_path.relative_to(self.root_dir).as_posix()
|
14
17
|
parts = relative_path.split('/')
|
15
18
|
current_level = tree
|
16
19
|
for part in parts[:-1]:
|
17
20
|
current_level = current_level.setdefault(part, {})
|
18
21
|
current_level[parts[-1]] = relative_path
|
22
|
+
logging.debug(">>> End of Processing <<<\n")
|
19
23
|
return tree
|
reposnap/core/git_repo.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# src/reposnap/core/git_repo.py
|
2
2
|
|
3
|
+
import logging
|
3
4
|
from git import Repo, InvalidGitRepositoryError
|
4
5
|
|
5
6
|
class GitRepo:
|
@@ -9,6 +10,12 @@ class GitRepo:
|
|
9
10
|
def get_git_files(self):
|
10
11
|
try:
|
11
12
|
repo = Repo(self.repo_path)
|
12
|
-
|
13
|
+
files = repo.git.ls_files().splitlines()
|
14
|
+
logging.debug(f"\n--- Retrieved Git Files from {repo.working_tree_dir} ---")
|
15
|
+
for file in files:
|
16
|
+
logging.debug(f" - {file}")
|
17
|
+
logging.debug("--- End of Git Files ---\n")
|
18
|
+
return files
|
13
19
|
except InvalidGitRepositoryError:
|
20
|
+
logging.debug(f"Invalid Git repository at: {self.repo_path}")
|
14
21
|
return []
|
@@ -2,23 +2,121 @@
|
|
2
2
|
|
3
3
|
from pathlib import Path
|
4
4
|
from ..utils.path_utils import format_tree
|
5
|
+
import logging
|
6
|
+
|
5
7
|
|
6
8
|
class MarkdownGenerator:
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
9
|
+
def __init__(self, root_dir: Path, output_file: Path, structure_only: bool = False):
|
10
|
+
"""
|
11
|
+
Initializes the MarkdownGenerator.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
root_dir (Path): The root directory of the project.
|
15
|
+
output_file (Path): The path to the output Markdown file.
|
16
|
+
structure_only (bool): If True, only the directory structure is included without file contents.
|
17
|
+
"""
|
18
|
+
self.root_dir = root_dir.resolve()
|
19
|
+
self.output_file = output_file.resolve()
|
20
|
+
self.structure_only = structure_only
|
21
|
+
self.logger = logging.getLogger(__name__)
|
22
|
+
self.logger.debug(f"Initialized MarkdownGenerator with root_dir={self.root_dir}, "
|
23
|
+
f"output_file={self.output_file}, structure_only={self.structure_only}")
|
24
|
+
|
25
|
+
def generate_markdown(self, tree_structure: dict, git_files: list, spec=None):
|
26
|
+
"""
|
27
|
+
Generates the Markdown file based on the provided tree structure and git files.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
tree_structure (dict): The hierarchical structure of the project files.
|
31
|
+
git_files (list): List of files tracked by Git.
|
32
|
+
spec (pathspec.PathSpec, optional): PathSpec object for file exclusion based on patterns.
|
33
|
+
"""
|
34
|
+
self.logger.info("Starting Markdown generation.")
|
35
|
+
self._write_header(tree_structure)
|
36
|
+
|
37
|
+
if not self.structure_only:
|
38
|
+
self._write_file_contents(git_files, spec)
|
39
|
+
|
40
|
+
self.logger.info(f"Markdown file generated successfully at: {self.output_file}")
|
41
|
+
|
42
|
+
def _write_header(self, tree_structure: dict):
|
43
|
+
"""
|
44
|
+
Writes the header and project structure to the Markdown file.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
tree_structure (dict): The hierarchical structure of the project files.
|
48
|
+
"""
|
49
|
+
self.logger.debug("Writing Markdown header and project structure.")
|
50
|
+
try:
|
51
|
+
with self.output_file.open('w', encoding='utf-8') as f:
|
52
|
+
f.write("# Project Structure\n\n")
|
53
|
+
f.write("```\n")
|
54
|
+
for line in format_tree(tree_structure):
|
55
|
+
f.write(line)
|
56
|
+
f.write("```\n\n")
|
57
|
+
self.logger.debug("Header and project structure written successfully.")
|
58
|
+
except IOError as e:
|
59
|
+
self.logger.error(f"Failed to write header to {self.output_file}: {e}")
|
60
|
+
raise
|
61
|
+
|
62
|
+
def _write_file_contents(self, git_files: list, spec):
|
63
|
+
"""
|
64
|
+
Writes the contents of each file to the Markdown file, excluding those matching the spec.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
git_files (list): List of files tracked by Git.
|
68
|
+
spec (pathspec.PathSpec, optional): PathSpec object for file exclusion based on patterns.
|
69
|
+
"""
|
70
|
+
self.logger.debug("Writing file contents to Markdown.")
|
71
|
+
for file in git_files:
|
72
|
+
file_path = self._resolve_file_path(file)
|
73
|
+
|
74
|
+
if not file_path.exists():
|
75
|
+
self.logger.debug(f"File not found: {file_path}. Skipping.")
|
76
|
+
continue
|
77
|
+
|
78
|
+
relative_path = file_path.relative_to(self.root_dir).as_posix()
|
79
|
+
if spec and spec.match_file(relative_path):
|
80
|
+
self.logger.debug(f"File excluded by spec: {relative_path}. Skipping.")
|
81
|
+
continue
|
82
|
+
|
83
|
+
self.logger.debug(f"Processing file: {file_path}")
|
84
|
+
self._write_file_content(file_path, relative_path)
|
85
|
+
|
86
|
+
def _resolve_file_path(self, file: str) -> Path:
|
87
|
+
"""
|
88
|
+
Resolves the absolute path of a file relative to the root directory.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
file (str): The file path relative to the root directory.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Path: The absolute path to the file.
|
95
|
+
"""
|
96
|
+
resolved_path = self.root_dir / file
|
97
|
+
self.logger.debug(f"Resolved file path: {file} to {resolved_path}")
|
98
|
+
return resolved_path
|
99
|
+
|
100
|
+
def _write_file_content(self, file_path: Path, relative_path: str):
|
101
|
+
"""
|
102
|
+
Writes the content of a single file to the Markdown file with syntax highlighting.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
file_path (Path): The absolute path to the file.
|
106
|
+
relative_path (str): The file path relative to the root directory.
|
107
|
+
"""
|
108
|
+
try:
|
109
|
+
print(f"Attempting to read file: {file_path}")
|
110
|
+
with file_path.open('r', encoding='utf-8') as f:
|
111
|
+
content = f.read()
|
112
|
+
self.logger.debug(f"Read content from {file_path}")
|
113
|
+
|
114
|
+
with self.output_file.open('a', encoding='utf-8') as f:
|
115
|
+
f.write(f"## {relative_path}\n\n")
|
116
|
+
f.write("```python\n" if file_path.suffix == '.py' else "```\n")
|
117
|
+
f.write(f"{content}\n```\n\n")
|
118
|
+
self.logger.debug(f"Wrote content of {relative_path} to Markdown.")
|
119
|
+
except IOError as e:
|
120
|
+
self.logger.error(f"Error reading or writing file {file_path}: {e}")
|
121
|
+
except Exception as e:
|
122
|
+
self.logger.error(f"Unexpected error processing file {file_path}: {e}")
|
reposnap/interfaces/cli.py
CHANGED
@@ -1,21 +1,30 @@
|
|
1
1
|
# src/reposnap/interfaces/cli.py
|
2
2
|
|
3
3
|
import argparse
|
4
|
+
import logging
|
4
5
|
from reposnap.core.collector import ProjectContentCollector
|
5
6
|
|
7
|
+
|
6
8
|
def main():
|
7
9
|
parser = argparse.ArgumentParser(description='Generate a Markdown representation of a Git repository.')
|
8
10
|
parser.add_argument('path', help='Path to the Git repository.')
|
9
11
|
parser.add_argument('-o', '--output', help='Output Markdown file', default='output.md')
|
10
12
|
parser.add_argument('--structure-only', action='store_true',
|
11
13
|
help='Only include the file structure without content.')
|
14
|
+
parser.add_argument('--debug', action='store_true', help='Enable debug-level logging.')
|
15
|
+
|
12
16
|
args = parser.parse_args()
|
13
17
|
|
18
|
+
log_level = logging.DEBUG if args.debug else logging.INFO
|
19
|
+
logging.basicConfig(level=log_level, format='%(asctime)s - %(levelname)s - %(message)s')
|
20
|
+
|
14
21
|
with open(f"{args.path}/.gitignore", 'r') as gitignore:
|
15
22
|
patterns = gitignore.readlines()
|
23
|
+
logging.debug(f"Patterns from .gitignore in {args.path}: {patterns}")
|
16
24
|
|
17
25
|
collector = ProjectContentCollector(args.path, args.output, args.structure_only, patterns)
|
18
26
|
collector.collect_and_generate()
|
19
27
|
|
28
|
+
|
20
29
|
if __name__ == "__main__":
|
21
30
|
main()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: reposnap
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.2
|
4
4
|
Summary: Generate a Markdown file with all contents of your project
|
5
5
|
Author: agoloborodko
|
6
6
|
License-File: LICENSE
|
@@ -18,8 +18,6 @@ Description-Content-Type: text/markdown
|
|
18
18
|
|
19
19
|
## Features
|
20
20
|
|
21
|
-
- **Recursive Directory Traversal**: Automatically traverses through all subdirectories of the specified project root.
|
22
|
-
- **File Exclusion**: Supports excluding specific files and directories using patterns similar to `.gitignore`.
|
23
21
|
- **Syntax Highlighting**: Includes syntax highlighting for known file types in the generated Markdown file.
|
24
22
|
- **Structure Only Option**: The `--structure-only` flag can be used to generate the Markdown file with just the directory structure, omitting the contents of the files.
|
25
23
|
|
@@ -30,62 +28,43 @@ Description-Content-Type: text/markdown
|
|
30
28
|
To use the `collect.py` script, run it with the following options:
|
31
29
|
|
32
30
|
```bash
|
33
|
-
|
31
|
+
reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] path
|
34
32
|
```
|
35
33
|
|
36
|
-
-
|
37
|
-
- `-
|
38
|
-
- `-
|
39
|
-
- `-x, --exclude-file`: A file (e.g., `.gitignore`) containing patterns of files or directories to exclude.
|
34
|
+
- `path`: Path to the Git repository root folder
|
35
|
+
- `-h, --help`: show help message and exit
|
36
|
+
- `-o, --output`: The name of the output Markdown file. Defaults to `output.md`.
|
40
37
|
- `--structure-only`: Generate a Markdown file that includes only the project structure, without file contents.
|
38
|
+
- `--debug`: Enable debug-level logging.
|
41
39
|
|
42
40
|
### Examples
|
43
41
|
|
44
42
|
1. **Generate a full project structure with file contents**:
|
45
43
|
|
46
44
|
```bash
|
47
|
-
|
45
|
+
reposnap my_project/
|
48
46
|
```
|
49
47
|
|
50
48
|
2. **Generate a project structure only**:
|
51
49
|
|
52
50
|
```bash
|
53
|
-
|
51
|
+
reposnap my_project/ --structure-only
|
54
52
|
```
|
55
53
|
|
56
54
|
3. **Generate a Markdown file excluding certain files and directories**:
|
57
55
|
|
58
56
|
```bash
|
59
|
-
|
60
|
-
```
|
61
|
-
|
62
|
-
4. **Use exclusion patterns from a `.gitignore` file**:
|
63
|
-
|
64
|
-
```bash
|
65
|
-
python -m collect my_project/ -x .gitignore
|
57
|
+
reposnap my_project/ -o output.md
|
66
58
|
```
|
67
59
|
|
68
60
|
## Testing
|
69
61
|
|
70
|
-
This project includes unit tests to ensure the correctness of the code:
|
71
|
-
|
72
|
-
- **`test_collect.py`**: Tests the `file_extension_to_language` function, verifying that file extensions are correctly mapped to programming languages for syntax highlighting in the Markdown file.
|
73
|
-
- **`test_cli.py`**: Tests the command-line interface of the `collect.py` script, including the `--structure-only` flag.
|
74
|
-
|
75
|
-
### Running the Tests
|
76
|
-
|
77
62
|
To run the tests, use `rye`:
|
78
63
|
|
79
64
|
```bash
|
80
65
|
rye test
|
81
66
|
```
|
82
67
|
|
83
|
-
This will execute all test cases and provide a summary of the results.
|
84
|
-
|
85
|
-
## Dependencies
|
86
|
-
|
87
|
-
This project uses standard Python libraries and does not require any external dependencies.
|
88
|
-
|
89
68
|
## License
|
90
69
|
|
91
70
|
This project is licensed under the MIT License.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
reposnap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
reposnap/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
reposnap/core/collector.py,sha256=TY42k2pN8B9vpuop_g6Dj5PACnr6ncpCYs3b08P1Rog,1253
|
4
|
+
reposnap/core/file_system.py,sha256=X3JdnClpNUqbz_Taj6G3GIozk63wpQ_nG3Jd0yNpxsY,886
|
5
|
+
reposnap/core/git_repo.py,sha256=Zx6DU87KuKAD-0JcKKqcb0OK1glzKMdS_przDlP4ZV0,707
|
6
|
+
reposnap/core/markdown_generator.py,sha256=dcQYDMyhZXjz1DY9e1zMsVS_dDGRVPm9f31Ug0QHANc,5126
|
7
|
+
reposnap/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
reposnap/interfaces/cli.py,sha256=3bfyVpVNN5LiuxJ2UxUIpIdNugy75mLpuE-cgjI2J3I,1186
|
9
|
+
reposnap/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
reposnap/utils/path_utils.py,sha256=lMuZMMIX2lEE1o6V3QqifmyO5dO5fuA5OUXMxSvYcHI,290
|
11
|
+
reposnap-0.2.2.dist-info/METADATA,sha256=2ESL-1HA7WntV5eeyvT7yBjHAQ9kRvYN2gFZjsV7iak,1906
|
12
|
+
reposnap-0.2.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
13
|
+
reposnap-0.2.2.dist-info/entry_points.txt,sha256=EBQ8Fn4zZuWJPPddf_m1LnObGFfcb5OgWeZqzw0O_EE,42
|
14
|
+
reposnap-0.2.2.dist-info/licenses/LICENSE,sha256=Aj7WCYBXi98pvi723HPn4GDRyjxToNWb3PC6j1_lnPk,1069
|
15
|
+
reposnap-0.2.2.dist-info/RECORD,,
|
reposnap-0.2.0.dist-info/RECORD
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
reposnap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
reposnap/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
reposnap/core/collector.py,sha256=R9G7kEjnDNqJ4FztBZq1pl7bKni9T17z27qcaYFLty0,960
|
4
|
-
reposnap/core/file_system.py,sha256=ICz0B4MswDlCCuLJzE7LRuLeiH-kFmsxqSnDrqRTULY,632
|
5
|
-
reposnap/core/git_repo.py,sha256=JYuYnjAg_LBTd5cEXUZnbSiMXisH6JFcr8Ix4od6qCo,371
|
6
|
-
reposnap/core/markdown_generator.py,sha256=xgShE2o5d77_U5MNczrYrWiM5K8YHqmvHy7LcYDx0Ro,1079
|
7
|
-
reposnap/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
reposnap/interfaces/cli.py,sha256=qF25C579BM2wF1fj2D7EEt7siVhztMZFGJknMSCdgXo,842
|
9
|
-
reposnap/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
reposnap/utils/path_utils.py,sha256=lMuZMMIX2lEE1o6V3QqifmyO5dO5fuA5OUXMxSvYcHI,290
|
11
|
-
reposnap-0.2.0.dist-info/METADATA,sha256=sUNQzLumZjTvvP1-N2buQKj4_a6OfXBwpvQUPBfsgk4,3103
|
12
|
-
reposnap-0.2.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
13
|
-
reposnap-0.2.0.dist-info/licenses/LICENSE,sha256=Aj7WCYBXi98pvi723HPn4GDRyjxToNWb3PC6j1_lnPk,1069
|
14
|
-
reposnap-0.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|