reposnap 0.2.3__tar.gz → 0.3.0__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 (33) hide show
  1. {reposnap-0.2.3 → reposnap-0.3.0}/PKG-INFO +19 -1
  2. {reposnap-0.2.3 → reposnap-0.3.0}/README.md +18 -0
  3. {reposnap-0.2.3 → reposnap-0.3.0}/pyproject.toml +1 -1
  4. reposnap-0.3.0/src/reposnap/core/collector.py +64 -0
  5. reposnap-0.3.0/src/reposnap/core/file_system.py +30 -0
  6. reposnap-0.3.0/src/reposnap/core/git_repo.py +30 -0
  7. reposnap-0.3.0/src/reposnap/core/markdown_generator.py +72 -0
  8. {reposnap-0.2.3 → reposnap-0.3.0}/src/reposnap/interfaces/cli.py +23 -7
  9. {reposnap-0.2.3 → reposnap-0.3.0}/tests/reposnap/test_file_system.py +10 -28
  10. reposnap-0.3.0/tests/reposnap/test_git_repo.py +20 -0
  11. {reposnap-0.2.3 → reposnap-0.3.0}/tests/reposnap/test_markdown_generator.py +17 -14
  12. reposnap-0.2.3/src/reposnap/core/collector.py +0 -30
  13. reposnap-0.2.3/src/reposnap/core/file_system.py +0 -23
  14. reposnap-0.2.3/src/reposnap/core/git_repo.py +0 -21
  15. reposnap-0.2.3/src/reposnap/core/markdown_generator.py +0 -122
  16. reposnap-0.2.3/tests/reposnap/test_git_repo.py +0 -16
  17. {reposnap-0.2.3 → reposnap-0.3.0}/.gitignore +0 -0
  18. {reposnap-0.2.3 → reposnap-0.3.0}/.python-version +0 -0
  19. {reposnap-0.2.3 → reposnap-0.3.0}/LICENSE +0 -0
  20. {reposnap-0.2.3 → reposnap-0.3.0}/requirements-dev.lock +0 -0
  21. {reposnap-0.2.3 → reposnap-0.3.0}/requirements.lock +0 -0
  22. {reposnap-0.2.3 → reposnap-0.3.0}/src/reposnap/__init__.py +0 -0
  23. {reposnap-0.2.3 → reposnap-0.3.0}/src/reposnap/core/__init__.py +0 -0
  24. {reposnap-0.2.3 → reposnap-0.3.0}/src/reposnap/interfaces/__init__.py +0 -0
  25. {reposnap-0.2.3 → reposnap-0.3.0}/src/reposnap/utils/__init__.py +0 -0
  26. {reposnap-0.2.3 → reposnap-0.3.0}/src/reposnap/utils/path_utils.py +0 -0
  27. {reposnap-0.2.3 → reposnap-0.3.0}/tests/__init__.py +0 -0
  28. {reposnap-0.2.3 → reposnap-0.3.0}/tests/reposnap/__init__.py +0 -0
  29. {reposnap-0.2.3 → reposnap-0.3.0}/tests/reposnap/test_cli.py +0 -0
  30. {reposnap-0.2.3 → reposnap-0.3.0}/tests/reposnap/test_collector.py +0 -0
  31. {reposnap-0.2.3 → reposnap-0.3.0}/tests/reposnap/test_path_utils.py +0 -0
  32. {reposnap-0.2.3 → reposnap-0.3.0}/tests/resources/another_existing_file.py +0 -0
  33. {reposnap-0.2.3 → reposnap-0.3.0}/tests/resources/existing_file.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: reposnap
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Generate a Markdown file with all contents of your project
5
5
  Author: agoloborodko
6
6
  License-File: LICENSE
@@ -21,6 +21,19 @@ Description-Content-Type: text/markdown
21
21
  - **Syntax Highlighting**: Includes syntax highlighting for known file types in the generated Markdown file.
22
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.
23
23
 
24
+ ## Installation
25
+ ```bash
26
+ pip install reposnap
27
+ ```
28
+
29
+ Alternatively, you can clone the repository and install the required dependencies:
30
+
31
+ ```bash
32
+ git clone https://github.com/username/reposnap.git
33
+ cd reposnap
34
+ pip install -r requirements.lock
35
+ ```
36
+
24
37
  ## Usage
25
38
 
26
39
  ### Command-Line Interface
@@ -68,3 +81,8 @@ rye test
68
81
  ## License
69
82
 
70
83
  This project is licensed under the MIT License.
84
+
85
+ ## Acknowledgments
86
+
87
+ - [GitPython](https://gitpython.readthedocs.io/) - Used for interacting with Git repositories.
88
+ - [pathspec](https://pathspec.readthedocs.io/) - Used for pattern matching file paths.
@@ -9,6 +9,19 @@
9
9
  - **Syntax Highlighting**: Includes syntax highlighting for known file types in the generated Markdown file.
10
10
  - **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.
11
11
 
12
+ ## Installation
13
+ ```bash
14
+ pip install reposnap
15
+ ```
16
+
17
+ Alternatively, you can clone the repository and install the required dependencies:
18
+
19
+ ```bash
20
+ git clone https://github.com/username/reposnap.git
21
+ cd reposnap
22
+ pip install -r requirements.lock
23
+ ```
24
+
12
25
  ## Usage
13
26
 
14
27
  ### Command-Line Interface
@@ -56,3 +69,8 @@ rye test
56
69
  ## License
57
70
 
58
71
  This project is licensed under the MIT License.
72
+
73
+ ## Acknowledgments
74
+
75
+ - [GitPython](https://gitpython.readthedocs.io/) - Used for interacting with Git repositories.
76
+ - [pathspec](https://pathspec.readthedocs.io/) - Used for pattern matching file paths.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reposnap"
3
- version = "0.2.3"
3
+ version = "0.3.0"
4
4
  description = "Generate a Markdown file with all contents of your project"
5
5
  authors = [
6
6
  { name = "agoloborodko" }
@@ -0,0 +1,64 @@
1
+ # src/reposnap/core/collector.py
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ import pathspec
6
+ from .git_repo import GitRepo
7
+ from .file_system import FileSystem
8
+ from .markdown_generator import MarkdownGenerator
9
+
10
+
11
+ class ProjectContentCollector:
12
+ def __init__(self, root_dir: str, output_file: str, structure_only: bool, gitignore_patterns: list):
13
+ self.logger = logging.getLogger(__name__)
14
+ self.root_dir = Path(root_dir).resolve()
15
+ self.output_file = Path(output_file).resolve()
16
+ self.structure_only = structure_only
17
+ self.gitignore_patterns = gitignore_patterns
18
+ self.spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, gitignore_patterns)
19
+
20
+ # Initialize components
21
+ self.git_repo = GitRepo(self.root_dir)
22
+ self.file_system = FileSystem(self.root_dir)
23
+ self.markdown_generator = MarkdownGenerator(
24
+ root_dir=self.root_dir,
25
+ output_file=self.output_file,
26
+ structure_only=self.structure_only
27
+ )
28
+
29
+ # Collect files and build tree during initialization
30
+ self.files = self.collect_files()
31
+ self.tree_structure = self.build_tree_structure()
32
+
33
+ def collect_files(self):
34
+ """
35
+ Collects and filters files to be included in the documentation.
36
+ """
37
+ self.logger.info("Collecting git files.")
38
+ git_files = self.git_repo.get_git_files()
39
+ self.logger.debug(f"Git files before filtering: {git_files}")
40
+
41
+ # Filter files based on .gitignore patterns
42
+ filtered_files = [
43
+ f for f in git_files if not self.spec.match_file(str(f))
44
+ ]
45
+ self.logger.debug(f"Git files after filtering: {filtered_files}")
46
+
47
+ return filtered_files # Paths relative to root_dir
48
+
49
+ def build_tree_structure(self):
50
+ """
51
+ Builds the tree structure from the collected files.
52
+ """
53
+ self.logger.info("Building tree structure.")
54
+ tree = self.file_system.build_tree_structure(self.files)
55
+ self.logger.debug(f"Tree structure: {tree}")
56
+ return tree
57
+
58
+ def collect_and_generate(self):
59
+ """
60
+ Initiates the markdown generation process.
61
+ """
62
+ self.logger.info("Starting markdown generation.")
63
+ self.markdown_generator.generate_markdown(self.tree_structure, self.files)
64
+ self.logger.info("Markdown generation completed.")
@@ -0,0 +1,30 @@
1
+ # src/reposnap/core/file_system.py
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ class FileSystem:
7
+ def __init__(self, root_dir: Path):
8
+ self.root_dir = root_dir.resolve()
9
+ self.logger = logging.getLogger(__name__)
10
+
11
+ def build_tree_structure(self, files):
12
+ """
13
+ Builds a hierarchical tree structure from the list of files.
14
+
15
+ Args:
16
+ files (list of Path): List of file paths relative to root_dir.
17
+
18
+ Returns:
19
+ dict: Nested dictionary representing the directory structure.
20
+ """
21
+ tree = {}
22
+ self.logger.debug("Building tree structure.")
23
+ for relative_path in files:
24
+ parts = relative_path.parts
25
+ current_level = tree
26
+ for part in parts[:-1]:
27
+ current_level = current_level.setdefault(part, {})
28
+ current_level[parts[-1]] = relative_path.as_posix()
29
+ self.logger.debug(f"Tree structure built: {tree}")
30
+ return tree
@@ -0,0 +1,30 @@
1
+ # src/reposnap/core/git_repo.py
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from git import Repo, InvalidGitRepositoryError
6
+
7
+ class GitRepo:
8
+ def __init__(self, repo_path: Path):
9
+ self.repo_path = repo_path.resolve()
10
+ self.logger = logging.getLogger(__name__)
11
+
12
+ def get_git_files(self):
13
+ try:
14
+ repo = Repo(self.repo_path, search_parent_directories=True)
15
+ repo_root = Path(repo.working_tree_dir).resolve()
16
+ git_files = repo.git.ls_files().splitlines()
17
+ self.logger.debug(f"Git files from {repo_root}: {git_files}")
18
+ git_files_relative = []
19
+ for f in git_files:
20
+ absolute_path = (repo_root / f).resolve()
21
+ try:
22
+ relative_path = absolute_path.relative_to(self.repo_path)
23
+ git_files_relative.append(relative_path)
24
+ except ValueError:
25
+ # Skip files not under root_dir
26
+ continue
27
+ return git_files_relative
28
+ except InvalidGitRepositoryError:
29
+ self.logger.error(f"Invalid Git repository at: {self.repo_path}")
30
+ return []
@@ -0,0 +1,72 @@
1
+ # src/reposnap/core/markdown_generator.py
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from ..utils.path_utils import format_tree
6
+
7
+ class MarkdownGenerator:
8
+ def __init__(self, root_dir: Path, output_file: Path, structure_only: bool = False):
9
+ self.root_dir = root_dir.resolve()
10
+ self.output_file = output_file.resolve()
11
+ self.structure_only = structure_only
12
+ self.logger = logging.getLogger(__name__)
13
+
14
+ def generate_markdown(self, tree_structure: dict, files: list):
15
+ """
16
+ Generates the Markdown file based on the provided tree structure and files.
17
+
18
+ Args:
19
+ tree_structure (dict): The hierarchical structure of the project files.
20
+ files (list of Path): List of file paths to include in the markdown.
21
+ """
22
+ self._write_header(tree_structure)
23
+ if not self.structure_only:
24
+ self._write_file_contents(files)
25
+
26
+ def _write_header(self, tree_structure: dict):
27
+ """
28
+ Writes the header and project structure to the Markdown file.
29
+ """
30
+ self.logger.debug("Writing Markdown header and project structure.")
31
+ try:
32
+ with self.output_file.open('w', encoding='utf-8') as f:
33
+ f.write("# Project Structure\n\n")
34
+ f.write("```\n")
35
+ for line in format_tree(tree_structure):
36
+ f.write(line)
37
+ f.write("```\n\n")
38
+ self.logger.debug("Header and project structure written successfully.")
39
+ except IOError as e:
40
+ self.logger.error(f"Failed to write header to {self.output_file}: {e}")
41
+ raise
42
+
43
+ def _write_file_contents(self, files: list):
44
+ """
45
+ Writes the contents of each file to the Markdown file.
46
+
47
+ Args:
48
+ files (list of Path): List of file paths relative to root_dir.
49
+ """
50
+ self.logger.debug("Writing file contents to Markdown.")
51
+ for relative_path in files:
52
+ file_path = self.root_dir / relative_path
53
+
54
+ if not file_path.exists():
55
+ self.logger.debug(f"File not found: {file_path}. Skipping.")
56
+ continue
57
+
58
+ self._write_file_content(file_path, relative_path.as_posix())
59
+
60
+ def _write_file_content(self, file_path: Path, relative_path: str):
61
+ """
62
+ Writes the content of a single file to the Markdown file with syntax highlighting.
63
+ """
64
+ try:
65
+ with file_path.open('r', encoding='utf-8') as f:
66
+ content = f.read()
67
+ with self.output_file.open('a', encoding='utf-8') as f:
68
+ f.write(f"## {relative_path}\n\n")
69
+ f.write("```python\n" if file_path.suffix == '.py' else "```\n")
70
+ f.write(f"{content}\n```\n\n")
71
+ except IOError as e:
72
+ self.logger.error(f"Error reading or writing file {file_path}: {e}")
@@ -2,12 +2,14 @@
2
2
 
3
3
  import argparse
4
4
  import logging
5
+ import os
5
6
  from reposnap.core.collector import ProjectContentCollector
7
+ from pathlib import Path
6
8
 
7
9
 
8
10
  def main():
9
11
  parser = argparse.ArgumentParser(description='Generate a Markdown representation of a Git repository.')
10
- parser.add_argument('path', help='Path to the Git repository.')
12
+ parser.add_argument('path', help='Path to the Git repository or subdirectory.')
11
13
  parser.add_argument('-o', '--output', help='Output Markdown file', default='output.md')
12
14
  parser.add_argument('--structure-only', action='store_true',
13
15
  help='Only include the file structure without content.')
@@ -18,13 +20,27 @@ def main():
18
20
  log_level = logging.DEBUG if args.debug else logging.INFO
19
21
  logging.basicConfig(level=log_level, format='%(asctime)s - %(levelname)s - %(message)s')
20
22
 
21
- with open(f"{args.path}/.gitignore", 'r') as gitignore:
22
- patterns = gitignore.readlines()
23
- logging.debug(f"Patterns from .gitignore in {args.path}: {patterns}")
24
-
25
- collector = ProjectContentCollector(args.path, args.output, args.structure_only, patterns)
23
+ path = Path(args.path).resolve()
24
+ gitignore_path = path / '.gitignore'
25
+ if not gitignore_path.exists():
26
+ # Search for .gitignore in parent directories
27
+ for parent in path.parents:
28
+ gitignore_path = parent / '.gitignore'
29
+ if gitignore_path.exists():
30
+ break
31
+ else:
32
+ gitignore_path = None
33
+
34
+ if gitignore_path and gitignore_path.exists():
35
+ with gitignore_path.open('r') as gitignore:
36
+ patterns = gitignore.readlines()
37
+ logging.debug(f"Patterns from .gitignore in {gitignore_path.parent}: {patterns}")
38
+ else:
39
+ patterns = []
40
+ logging.debug(f"No .gitignore found starting from {args.path}. Proceeding without patterns.")
41
+
42
+ collector = ProjectContentCollector(str(path), args.output, args.structure_only, patterns)
26
43
  collector.collect_and_generate()
27
44
 
28
-
29
45
  if __name__ == "__main__":
30
46
  main()
@@ -8,7 +8,7 @@ from reposnap.core.file_system import FileSystem
8
8
  @pytest.fixture
9
9
  def file_system(tmp_path):
10
10
  # Initialize FileSystem with tmp_path as the root directory
11
- return FileSystem(str(tmp_path))
11
+ return FileSystem(tmp_path)
12
12
 
13
13
 
14
14
  def test_build_tree_structure(file_system, tmp_path):
@@ -21,10 +21,13 @@ def test_build_tree_structure(file_system, tmp_path):
21
21
  (tmp_path / 'dir2' / 'file2.py').write_text('content')
22
22
 
23
23
  # Build tree structure based on the files in tmp_path
24
- tree_structure = file_system.build_tree_structure([
25
- str(tmp_path / 'dir1' / 'file1.py'),
26
- str(tmp_path / 'dir2' / 'file2.py')
27
- ])
24
+ files = [
25
+ (tmp_path / 'dir1' / 'file1.py').relative_to(tmp_path),
26
+ (tmp_path / 'dir2' / 'file2.py').relative_to(tmp_path)
27
+ ]
28
+ tree_structure = file_system.build_tree_structure(files)
29
+
30
+ # Update assertions to match the new tree structure
28
31
  assert 'dir1' in tree_structure
29
32
  assert 'file1.py' in tree_structure['dir1']
30
33
  assert 'dir2' in tree_structure
@@ -36,11 +39,8 @@ def test_relative_path_resolution(file_system, tmp_path):
36
39
  gitignore_file = tmp_path / '.gitignore'
37
40
  gitignore_file.write_text('*.pyc\n')
38
41
 
39
- # Add a relative path to the git_files list
40
- git_files = ['.gitignore']
41
-
42
- # Mock the root_dir to simulate the environment
43
- root_dir = tmp_path
42
+ # Add a Path object to the git_files list, relative to tmp_path
43
+ git_files = [gitignore_file.relative_to(tmp_path)]
44
44
 
45
45
  # Simulate the file system processing
46
46
  tree_structure = file_system.build_tree_structure(git_files)
@@ -49,21 +49,3 @@ def test_relative_path_resolution(file_system, tmp_path):
49
49
  expected_path = '.gitignore'
50
50
  assert tree_structure['.gitignore'] == expected_path, \
51
51
  f"Expected {expected_path}, but got {tree_structure['.gitignore']}"
52
-
53
-
54
- def test_incorrect_path_resolution(file_system, tmp_path):
55
- # Create a mock .gitignore file in a different directory (simulating reposnap)
56
- wrong_dir = Path('/Users/andrey.goloborodko/PycharmProjects/reposnap')
57
- wrong_gitignore_file = wrong_dir / '.gitignore'
58
- wrong_gitignore_file.touch() # Create an empty .gitignore file
59
-
60
- # Add a relative path to the git_files list
61
- git_files = ['.gitignore']
62
-
63
- # Simulate the file system processing and manually resolve .gitignore incorrectly
64
- tree_structure = file_system.build_tree_structure(git_files)
65
-
66
- # Check if .gitignore was incorrectly resolved to the wrong directory
67
- incorrect_path = wrong_gitignore_file.resolve().as_posix()
68
- assert tree_structure['.gitignore'] != incorrect_path, \
69
- f"Expected path NOT to be {incorrect_path}, but it was resolved as such"
@@ -0,0 +1,20 @@
1
+ # tests/reposnap/test_git_repo.py
2
+
3
+ import pytest
4
+ from unittest.mock import patch, MagicMock
5
+ from reposnap.core.git_repo import GitRepo
6
+ from pathlib import Path
7
+
8
+ @patch('reposnap.core.git_repo.Repo')
9
+ def test_get_git_files(mock_repo):
10
+ mock_repo_instance = MagicMock()
11
+ mock_repo_instance.git.ls_files.return_value = 'file1.py\nsubdir/file2.py'
12
+ mock_repo_instance.working_tree_dir = '/path/to/repo'
13
+ mock_repo.return_value = mock_repo_instance
14
+
15
+ git_repo = GitRepo(Path('/path/to/repo/subdir'))
16
+ files = git_repo.get_git_files()
17
+
18
+ expected_files = [Path('file2.py')]
19
+
20
+ assert files == expected_files
@@ -1,6 +1,7 @@
1
+ # tests/reposnap/test_markdown_generator.py
2
+
1
3
  import pytest
2
4
  from reposnap.core.markdown_generator import MarkdownGenerator
3
- from unittest.mock import call
4
5
  from pathlib import Path
5
6
 
6
7
 
@@ -15,30 +16,32 @@ def markdown_generator_factory(resources_dir, tmp_path):
15
16
  # Use a temporary directory for the output file
16
17
  output_file = tmp_path / "output.md"
17
18
  return MarkdownGenerator(root_dir=resources_dir, output_file=output_file, structure_only=structure_only)
18
-
19
19
  return _factory
20
20
 
21
21
 
22
22
  @pytest.mark.parametrize("git_files,expected_calls", [
23
23
  (
24
- ['existing_file.py', 'missing_file.py', 'another_existing_file.py'],
25
- [
26
- "## existing_file.py\n\n",
27
- "```python\n",
28
- 'print("Hello, world!")\n',
29
- '```\n\n',
30
- "## another_existing_file.py\n\n",
31
- "```python\n",
32
- 'print("Another file")\n',
33
- '```\n\n'
34
- ]
24
+ ['existing_file.py', 'missing_file.py', 'another_existing_file.py'],
25
+ [
26
+ "## existing_file.py\n\n",
27
+ "```python\n",
28
+ 'print("Hello, world!")\n',
29
+ '```\n\n',
30
+ "## another_existing_file.py\n\n",
31
+ "```python\n",
32
+ 'print("Another file")\n',
33
+ '```\n\n'
34
+ ]
35
35
  ),
36
36
  ])
37
37
  def test_generate_markdown_with_missing_files(resources_dir, markdown_generator_factory, git_files, expected_calls):
38
38
  markdown_generator = markdown_generator_factory(structure_only=False)
39
39
 
40
+ # Use relative paths for git_files_paths
41
+ git_files_paths = [Path(f) for f in git_files]
42
+
40
43
  # Generate the markdown file and verify the output
41
- markdown_generator.generate_markdown({}, git_files)
44
+ markdown_generator.generate_markdown({}, git_files_paths)
42
45
  output_content = markdown_generator.output_file.read_text()
43
46
 
44
47
  # Join expected calls into a single string
@@ -1,30 +0,0 @@
1
- # src/reposnap/core/collector.py
2
-
3
- from .file_system import FileSystem
4
- from .git_repo import GitRepo
5
- from .markdown_generator import MarkdownGenerator
6
- import pathspec
7
- import logging
8
- from pathlib import Path
9
-
10
-
11
- class ProjectContentCollector:
12
- def __init__(self, root_dir: str, output_file: str, structure_only: bool, gitignore_patterns: list):
13
- self.root_dir = Path(root_dir).resolve()
14
- self.output_file = Path(output_file).resolve()
15
- self.structure_only = structure_only
16
- self.spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, gitignore_patterns)
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__)
25
-
26
- def collect_and_generate(self):
27
- self.logger.info("Starting project content collection.")
28
- git_files = self.git_repo.get_git_files()
29
- tree_structure = self.file_system.build_tree_structure(git_files)
30
- self.markdown_generator.generate_markdown(tree_structure, git_files, self.spec)
@@ -1,23 +0,0 @@
1
- # src/reposnap/core/file_system.py
2
-
3
- import logging
4
- from pathlib import Path
5
-
6
- class FileSystem:
7
- def __init__(self, root_dir: str):
8
- self.root_dir = Path(root_dir).resolve()
9
-
10
- def build_tree_structure(self, files):
11
- tree = {}
12
- logging.debug("\n>>> Processing Files for Tree Structure <<<")
13
- for file in files:
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}")
16
- relative_path = file_path.relative_to(self.root_dir).as_posix()
17
- parts = relative_path.split('/')
18
- current_level = tree
19
- for part in parts[:-1]:
20
- current_level = current_level.setdefault(part, {})
21
- current_level[parts[-1]] = relative_path
22
- logging.debug(">>> End of Processing <<<\n")
23
- return tree
@@ -1,21 +0,0 @@
1
- # src/reposnap/core/git_repo.py
2
-
3
- import logging
4
- from git import Repo, InvalidGitRepositoryError
5
-
6
- class GitRepo:
7
- def __init__(self, repo_path: str):
8
- self.repo_path = repo_path
9
-
10
- def get_git_files(self):
11
- try:
12
- repo = Repo(self.repo_path)
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
19
- except InvalidGitRepositoryError:
20
- logging.debug(f"Invalid Git repository at: {self.repo_path}")
21
- return []
@@ -1,122 +0,0 @@
1
- # src/reposnap/core/markdown_generator.py
2
-
3
- from pathlib import Path
4
- from ..utils.path_utils import format_tree
5
- import logging
6
-
7
-
8
- class MarkdownGenerator:
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}")
@@ -1,16 +0,0 @@
1
- # src/reposnap/tests/test_git_repo.py
2
-
3
- import pytest # noqa
4
- from unittest.mock import patch, MagicMock
5
- from reposnap.core.git_repo import GitRepo
6
-
7
- @patch('reposnap.core.git_repo.Repo')
8
- def test_get_git_files(mock_repo):
9
- mock_repo_instance = MagicMock()
10
- mock_repo_instance.git.ls_files.return_value = 'file1.py\nfile2.py'
11
- mock_repo.return_value = mock_repo_instance
12
-
13
- git_repo = GitRepo('test_repo')
14
- files = git_repo.get_git_files()
15
- assert 'file1.py' in files
16
- assert 'file2.py' in files
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes