reposnap 0.3.0__tar.gz → 0.3.2__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 (35) hide show
  1. {reposnap-0.3.0 → reposnap-0.3.2}/PKG-INFO +2 -2
  2. {reposnap-0.3.0 → reposnap-0.3.2}/README.md +1 -1
  3. {reposnap-0.3.0 → reposnap-0.3.2}/pyproject.toml +1 -1
  4. reposnap-0.3.2/src/reposnap/controllers/project_controller.py +74 -0
  5. {reposnap-0.3.0 → reposnap-0.3.2}/src/reposnap/core/file_system.py +8 -6
  6. {reposnap-0.3.0 → reposnap-0.3.2}/src/reposnap/core/git_repo.py +11 -8
  7. {reposnap-0.3.0 → reposnap-0.3.2}/src/reposnap/core/markdown_generator.py +16 -13
  8. reposnap-0.3.2/src/reposnap/interfaces/cli.py +25 -0
  9. reposnap-0.3.2/src/reposnap/models/file_tree.py +54 -0
  10. {reposnap-0.3.0 → reposnap-0.3.2}/src/reposnap/utils/path_utils.py +3 -1
  11. reposnap-0.3.2/tests/__init__.py +0 -0
  12. reposnap-0.3.2/tests/reposnap/__init__.py +0 -0
  13. {reposnap-0.3.0 → reposnap-0.3.2}/tests/reposnap/test_cli.py +9 -6
  14. {reposnap-0.3.0 → reposnap-0.3.2}/tests/reposnap/test_file_system.py +1 -2
  15. reposnap-0.3.2/tests/reposnap/test_file_tree.py +28 -0
  16. reposnap-0.3.2/tests/reposnap/test_project_controller.py +97 -0
  17. reposnap-0.3.0/src/reposnap/core/collector.py +0 -64
  18. reposnap-0.3.0/src/reposnap/interfaces/cli.py +0 -46
  19. reposnap-0.3.0/tests/reposnap/test_collector.py +0 -27
  20. {reposnap-0.3.0 → reposnap-0.3.2}/.gitignore +0 -0
  21. {reposnap-0.3.0 → reposnap-0.3.2}/.python-version +0 -0
  22. {reposnap-0.3.0 → reposnap-0.3.2}/LICENSE +0 -0
  23. {reposnap-0.3.0 → reposnap-0.3.2}/requirements-dev.lock +0 -0
  24. {reposnap-0.3.0 → reposnap-0.3.2}/requirements.lock +0 -0
  25. {reposnap-0.3.0 → reposnap-0.3.2}/src/reposnap/__init__.py +0 -0
  26. {reposnap-0.3.0/src/reposnap/core → reposnap-0.3.2/src/reposnap/controllers}/__init__.py +0 -0
  27. {reposnap-0.3.0/src/reposnap/interfaces → reposnap-0.3.2/src/reposnap/core}/__init__.py +0 -0
  28. {reposnap-0.3.0/src/reposnap/utils → reposnap-0.3.2/src/reposnap/interfaces}/__init__.py +0 -0
  29. {reposnap-0.3.0/tests → reposnap-0.3.2/src/reposnap/models}/__init__.py +0 -0
  30. {reposnap-0.3.0/tests/reposnap → reposnap-0.3.2/src/reposnap/utils}/__init__.py +0 -0
  31. {reposnap-0.3.0 → reposnap-0.3.2}/tests/reposnap/test_git_repo.py +0 -0
  32. {reposnap-0.3.0 → reposnap-0.3.2}/tests/reposnap/test_markdown_generator.py +0 -0
  33. {reposnap-0.3.0 → reposnap-0.3.2}/tests/reposnap/test_path_utils.py +0 -0
  34. {reposnap-0.3.0 → reposnap-0.3.2}/tests/resources/another_existing_file.py +0 -0
  35. {reposnap-0.3.0 → reposnap-0.3.2}/tests/resources/existing_file.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: reposnap
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Generate a Markdown file with all contents of your project
5
5
  Author: agoloborodko
6
6
  License-File: LICENSE
@@ -44,7 +44,7 @@ To use `reposnap`, run it with the following options:
44
44
  reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] path
45
45
  ```
46
46
 
47
- - `path`: Path to the Git repository root folder
47
+ - `path`: Path to the Git repository or subdirectory
48
48
  - `-h, --help`: show help message and exit
49
49
  - `-o, --output`: The name of the output Markdown file. Defaults to `output.md`.
50
50
  - `--structure-only`: Generate a Markdown file that includes only the project structure, without file contents.
@@ -32,7 +32,7 @@ To use `reposnap`, run it with the following options:
32
32
  reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] path
33
33
  ```
34
34
 
35
- - `path`: Path to the Git repository root folder
35
+ - `path`: Path to the Git repository or subdirectory
36
36
  - `-h, --help`: show help message and exit
37
37
  - `-o, --output`: The name of the output Markdown file. Defaults to `output.md`.
38
38
  - `--structure-only`: Generate a Markdown file that includes only the project structure, without file contents.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reposnap"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Generate a Markdown file with all contents of your project"
5
5
  authors = [
6
6
  { name = "agoloborodko" }
@@ -0,0 +1,74 @@
1
+ # src/reposnap/controllers/project_controller.py
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from reposnap.core.git_repo import GitRepo
6
+ from reposnap.core.file_system import FileSystem
7
+ from reposnap.core.markdown_generator import MarkdownGenerator
8
+ from reposnap.models.file_tree import FileTree
9
+ import pathspec
10
+ from typing import List, Optional
11
+
12
+
13
+ class ProjectController:
14
+ def __init__(self, args: object):
15
+ self.logger = logging.getLogger(__name__)
16
+ self.root_dir: Path = Path(args.path).resolve()
17
+ self.output_file: Path = Path(args.output).resolve()
18
+ self.structure_only: bool = args.structure_only
19
+ self.args: object = args
20
+ self.file_tree: Optional[FileTree] = None
21
+ self.gitignore_patterns: List[str] = self._load_gitignore_patterns()
22
+
23
+ def run(self) -> None:
24
+ self.collect_file_tree()
25
+ self.apply_filters()
26
+ self.generate_output()
27
+
28
+ def collect_file_tree(self) -> None:
29
+ self.logger.info("Collecting git files.")
30
+ git_repo: GitRepo = GitRepo(self.root_dir)
31
+ git_files: List[Path] = git_repo.get_git_files()
32
+ self.logger.debug(f"Git files before filtering: {git_files}")
33
+
34
+ self.logger.info("Building tree structure.")
35
+ file_system: FileSystem = FileSystem(self.root_dir)
36
+ tree_structure: dict = file_system.build_tree_structure(git_files)
37
+
38
+ self.file_tree = FileTree(tree_structure)
39
+ self.logger.debug(f"Tree structure: {self.file_tree.structure}")
40
+
41
+ def apply_filters(self) -> None:
42
+ self.logger.info("Applying filters to the file tree.")
43
+ spec: pathspec.PathSpec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, self.gitignore_patterns)
44
+ self.logger.debug(f"Filter patterns: {self.gitignore_patterns}")
45
+ self.file_tree.filter_files(spec)
46
+
47
+ def generate_output(self) -> None:
48
+ self.logger.info("Starting markdown generation.")
49
+ markdown_generator: MarkdownGenerator = MarkdownGenerator(
50
+ root_dir=self.root_dir,
51
+ output_file=self.output_file,
52
+ structure_only=self.structure_only
53
+ )
54
+ markdown_generator.generate_markdown(self.file_tree.structure, self.file_tree.get_all_files())
55
+ self.logger.info("Markdown generation completed.")
56
+
57
+ def _load_gitignore_patterns(self) -> List[str]:
58
+ gitignore_path: Path = self.root_dir / '.gitignore'
59
+ if not gitignore_path.exists():
60
+ for parent in self.root_dir.parents:
61
+ gitignore_path = parent / '.gitignore'
62
+ if gitignore_path.exists():
63
+ break
64
+ else:
65
+ gitignore_path = None
66
+
67
+ if gitignore_path and gitignore_path.exists():
68
+ with gitignore_path.open('r') as gitignore:
69
+ patterns: List[str] = gitignore.readlines()
70
+ self.logger.debug(f"Patterns from .gitignore in {gitignore_path.parent}: {patterns}")
71
+ return patterns
72
+ else:
73
+ self.logger.debug(f"No .gitignore found starting from {self.root_dir}. Proceeding without patterns.")
74
+ return []
@@ -2,29 +2,31 @@
2
2
 
3
3
  import logging
4
4
  from pathlib import Path
5
+ from typing import List, Dict, Any
6
+
5
7
 
6
8
  class FileSystem:
7
9
  def __init__(self, root_dir: Path):
8
- self.root_dir = root_dir.resolve()
10
+ self.root_dir: Path = root_dir.resolve()
9
11
  self.logger = logging.getLogger(__name__)
10
12
 
11
- def build_tree_structure(self, files):
13
+ def build_tree_structure(self, files: List[Path]) -> Dict[str, Any]:
12
14
  """
13
15
  Builds a hierarchical tree structure from the list of files.
14
16
 
15
17
  Args:
16
- files (list of Path): List of file paths relative to root_dir.
18
+ files (List[Path]): List of file paths relative to root_dir.
17
19
 
18
20
  Returns:
19
- dict: Nested dictionary representing the directory structure.
21
+ Dict[str, Any]: Nested dictionary representing the directory structure.
20
22
  """
21
- tree = {}
23
+ tree: Dict[str, Any] = {}
22
24
  self.logger.debug("Building tree structure.")
23
25
  for relative_path in files:
24
26
  parts = relative_path.parts
25
27
  current_level = tree
26
28
  for part in parts[:-1]:
27
29
  current_level = current_level.setdefault(part, {})
28
- current_level[parts[-1]] = relative_path.as_posix()
30
+ current_level[parts[-1]] = None # Indicate a file node
29
31
  self.logger.debug(f"Tree structure built: {tree}")
30
32
  return tree
@@ -3,23 +3,25 @@
3
3
  import logging
4
4
  from pathlib import Path
5
5
  from git import Repo, InvalidGitRepositoryError
6
+ from typing import List
7
+
6
8
 
7
9
  class GitRepo:
8
10
  def __init__(self, repo_path: Path):
9
- self.repo_path = repo_path.resolve()
11
+ self.repo_path: Path = repo_path.resolve()
10
12
  self.logger = logging.getLogger(__name__)
11
13
 
12
- def get_git_files(self):
14
+ def get_git_files(self) -> List[Path]:
13
15
  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()
16
+ repo: Repo = Repo(self.repo_path, search_parent_directories=True)
17
+ repo_root: Path = Path(repo.working_tree_dir).resolve()
18
+ git_files: List[str] = repo.git.ls_files().splitlines()
17
19
  self.logger.debug(f"Git files from {repo_root}: {git_files}")
18
- git_files_relative = []
20
+ git_files_relative: List[Path] = []
19
21
  for f in git_files:
20
- absolute_path = (repo_root / f).resolve()
22
+ absolute_path: Path = (repo_root / f).resolve()
21
23
  try:
22
- relative_path = absolute_path.relative_to(self.repo_path)
24
+ relative_path: Path = absolute_path.relative_to(self.repo_path)
23
25
  git_files_relative.append(relative_path)
24
26
  except ValueError:
25
27
  # Skip files not under root_dir
@@ -28,3 +30,4 @@ class GitRepo:
28
30
  except InvalidGitRepositoryError:
29
31
  self.logger.error(f"Invalid Git repository at: {self.repo_path}")
30
32
  return []
33
+
@@ -2,28 +2,30 @@
2
2
 
3
3
  import logging
4
4
  from pathlib import Path
5
- from ..utils.path_utils import format_tree
5
+ from reposnap.utils.path_utils import format_tree
6
+ from typing import List, Dict, Any
7
+
6
8
 
7
9
  class MarkdownGenerator:
8
10
  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
11
+ self.root_dir: Path = root_dir.resolve()
12
+ self.output_file: Path = output_file.resolve()
13
+ self.structure_only: bool = structure_only
12
14
  self.logger = logging.getLogger(__name__)
13
15
 
14
- def generate_markdown(self, tree_structure: dict, files: list):
16
+ def generate_markdown(self, tree_structure: Dict[str, Any], files: List[Path]) -> None:
15
17
  """
16
18
  Generates the Markdown file based on the provided tree structure and files.
17
19
 
18
20
  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
+ tree_structure (Dict[str, Any]): The hierarchical structure of the project files.
22
+ files (List[Path]): List of file paths to include in the markdown.
21
23
  """
22
24
  self._write_header(tree_structure)
23
25
  if not self.structure_only:
24
26
  self._write_file_contents(files)
25
27
 
26
- def _write_header(self, tree_structure: dict):
28
+ def _write_header(self, tree_structure: Dict[str, Any]) -> None:
27
29
  """
28
30
  Writes the header and project structure to the Markdown file.
29
31
  """
@@ -40,16 +42,16 @@ class MarkdownGenerator:
40
42
  self.logger.error(f"Failed to write header to {self.output_file}: {e}")
41
43
  raise
42
44
 
43
- def _write_file_contents(self, files: list):
45
+ def _write_file_contents(self, files: List[Path]) -> None:
44
46
  """
45
47
  Writes the contents of each file to the Markdown file.
46
48
 
47
49
  Args:
48
- files (list of Path): List of file paths relative to root_dir.
50
+ files (List[Path]): List of file paths relative to root_dir.
49
51
  """
50
52
  self.logger.debug("Writing file contents to Markdown.")
51
53
  for relative_path in files:
52
- file_path = self.root_dir / relative_path
54
+ file_path: Path = self.root_dir / relative_path
53
55
 
54
56
  if not file_path.exists():
55
57
  self.logger.debug(f"File not found: {file_path}. Skipping.")
@@ -57,16 +59,17 @@ class MarkdownGenerator:
57
59
 
58
60
  self._write_file_content(file_path, relative_path.as_posix())
59
61
 
60
- def _write_file_content(self, file_path: Path, relative_path: str):
62
+ def _write_file_content(self, file_path: Path, relative_path: str) -> None:
61
63
  """
62
64
  Writes the content of a single file to the Markdown file with syntax highlighting.
63
65
  """
64
66
  try:
65
67
  with file_path.open('r', encoding='utf-8') as f:
66
- content = f.read()
68
+ content: str = f.read()
67
69
  with self.output_file.open('a', encoding='utf-8') as f:
68
70
  f.write(f"## {relative_path}\n\n")
69
71
  f.write("```python\n" if file_path.suffix == '.py' else "```\n")
70
72
  f.write(f"{content}\n```\n\n")
71
73
  except IOError as e:
72
74
  self.logger.error(f"Error reading or writing file {file_path}: {e}")
75
+
@@ -0,0 +1,25 @@
1
+ # src/reposnap/interfaces/cli.py
2
+
3
+ import argparse
4
+ import logging
5
+ from reposnap.controllers.project_controller import ProjectController
6
+
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(description='Generate a Markdown representation of a Git repository.')
10
+ parser.add_argument('path', help='Path to the Git repository or subdirectory.')
11
+ parser.add_argument('-o', '--output', help='Output Markdown file', default='output.md')
12
+ parser.add_argument('--structure-only', action='store_true',
13
+ help='Only include the file structure without content.')
14
+ parser.add_argument('--debug', action='store_true', help='Enable debug-level logging.')
15
+
16
+ args = parser.parse_args()
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
+
21
+ controller = ProjectController(args)
22
+ controller.run()
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -0,0 +1,54 @@
1
+ # src/reposnap/models/file_tree.py
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Dict, List, Any
6
+ import pathspec
7
+
8
+
9
+ class FileTree:
10
+ def __init__(self, structure: Dict[str, Any]):
11
+ self.structure: Dict[str, Any] = structure
12
+ self.logger = logging.getLogger(__name__)
13
+
14
+ def get_all_files(self) -> List[Path]:
15
+ """
16
+ Recursively retrieve all file paths from the tree.
17
+ Returns:
18
+ List[Path]: List of file paths relative to root_dir.
19
+ """
20
+ return self._extract_files(self.structure)
21
+
22
+ def _extract_files(self, subtree: Dict[str, Any], path_prefix: str = '') -> List[Path]:
23
+ files: List[Path] = []
24
+ for key, value in subtree.items():
25
+ current_path: str = f"{path_prefix}/{key}".lstrip('/')
26
+ if isinstance(value, dict):
27
+ files.extend(self._extract_files(value, current_path))
28
+ else:
29
+ files.append(Path(current_path))
30
+ return files
31
+
32
+ def filter_files(self, spec: pathspec.PathSpec) -> None:
33
+ """
34
+ Filters files in the tree structure based on the provided pathspec.
35
+
36
+ Args:
37
+ spec (pathspec.PathSpec): The pathspec for filtering files.
38
+ """
39
+ self.logger.debug("Filtering files in the file tree.")
40
+ self.structure = self._filter_tree(self.structure, spec)
41
+
42
+ def _filter_tree(self, subtree: Dict[str, Any], spec: pathspec.PathSpec, path_prefix: str = '') -> Dict[str, Any]:
43
+ filtered_subtree: Dict[str, Any] = {}
44
+ for key, value in subtree.items():
45
+ current_path: str = f"{path_prefix}/{key}".lstrip('/')
46
+ if isinstance(value, dict):
47
+ filtered_value: Dict[str, Any] = self._filter_tree(value, spec, current_path)
48
+ if filtered_value:
49
+ filtered_subtree[key] = filtered_value
50
+ else:
51
+ if not spec.match_file(current_path):
52
+ filtered_subtree[key] = value
53
+ return filtered_subtree
54
+
@@ -1,6 +1,8 @@
1
1
  # src/reposnap/utils/path_utils.py
2
+ from typing import Dict, Generator, Any
2
3
 
3
- def format_tree(tree, indent=''):
4
+
5
+ def format_tree(tree: Dict[str, Any], indent: str = '') -> Generator[str, None, None]:
4
6
  for key, value in tree.items():
5
7
  if isinstance(value, dict):
6
8
  yield f"{indent}{key}/\n"
File without changes
File without changes
@@ -1,4 +1,4 @@
1
- # src/reposnap/tests/test_cli.py
1
+ # tests/reposnap/test_cli.py
2
2
 
3
3
  import pytest
4
4
  from unittest.mock import patch, MagicMock
@@ -6,11 +6,13 @@ import os
6
6
  import tempfile
7
7
  from reposnap.interfaces.cli import main
8
8
 
9
+
9
10
  def create_file(file_path: str, content: str = ''):
10
11
  """Helper function to create a file with the given content."""
11
12
  with open(file_path, 'w') as f:
12
13
  f.write(content)
13
14
 
15
+
14
16
  def create_directory_structure(base_dir: str, structure: dict):
15
17
  """
16
18
  Helper function to create directories and files based on a given structure.
@@ -66,12 +68,13 @@ def temp_dir():
66
68
  create_directory_structure(temp_dir, structure)
67
69
  yield temp_dir
68
70
 
69
- @patch('reposnap.interfaces.cli.ProjectContentCollector')
70
- def test_cli(mock_collector, temp_dir):
71
- mock_collector_instance = MagicMock()
72
- mock_collector.return_value = mock_collector_instance
71
+
72
+ @patch('reposnap.interfaces.cli.ProjectController')
73
+ def test_cli(mock_controller, temp_dir):
74
+ mock_controller_instance = MagicMock()
75
+ mock_controller.return_value = mock_controller_instance
73
76
 
74
77
  with patch('sys.argv', ['cli.py', str(temp_dir), '--structure-only']):
75
78
  main()
76
79
 
77
- mock_collector_instance.collect_and_generate.assert_called_once()
80
+ mock_controller_instance.run.assert_called_once()
@@ -47,5 +47,4 @@ def test_relative_path_resolution(file_system, tmp_path):
47
47
 
48
48
  # Check if .gitignore was correctly resolved relative to the root directory
49
49
  expected_path = '.gitignore'
50
- assert tree_structure['.gitignore'] == expected_path, \
51
- f"Expected {expected_path}, but got {tree_structure['.gitignore']}"
50
+ assert '.gitignore' in tree_structure, "'.gitignore' not found in tree_structure"
@@ -0,0 +1,28 @@
1
+ # tests/reposnap/test_file_tree.py
2
+
3
+ import pytest
4
+ from reposnap.models.file_tree import FileTree
5
+ from pathlib import Path
6
+ import pathspec
7
+
8
+
9
+ def test_file_tree_filter_files():
10
+ tree_structure = {
11
+ 'dir1': {
12
+ 'file1.py': 'dir1/file1.py',
13
+ 'file2.log': 'dir1/file2.log'
14
+ },
15
+ 'file3.py': 'file3.py',
16
+ 'file4.log': 'file4.log'
17
+ }
18
+ file_tree = FileTree(tree_structure)
19
+ patterns = ['*.log']
20
+ spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, patterns)
21
+ file_tree.filter_files(spec)
22
+ expected_structure = {
23
+ 'dir1': {
24
+ 'file1.py': 'dir1/file1.py'
25
+ },
26
+ 'file3.py': 'file3.py'
27
+ }
28
+ assert file_tree.structure == expected_structure
@@ -0,0 +1,97 @@
1
+ # tests/reposnap/test_project_controller.py
2
+
3
+ import pytest
4
+ import tempfile
5
+ import os
6
+ from pathlib import Path
7
+ from unittest.mock import patch, MagicMock
8
+ from reposnap.controllers.project_controller import ProjectController
9
+
10
+
11
+ def create_file(file_path: str, content: str = ''):
12
+ with open(file_path, 'w') as f:
13
+ f.write(content)
14
+
15
+
16
+ def create_directory_structure(base_dir: str, structure: dict):
17
+ for name, content in structure.items():
18
+ path = os.path.join(base_dir, name)
19
+ if isinstance(content, dict):
20
+ os.makedirs(path, exist_ok=True)
21
+ create_directory_structure(path, content)
22
+ else:
23
+ create_file(path, content)
24
+
25
+
26
+ def test_project_controller_includes_py_files():
27
+ with tempfile.TemporaryDirectory() as temp_dir:
28
+ gitignore_content = """
29
+ *.py[oc]
30
+ """
31
+ structure = {
32
+ 'src': {
33
+ 'module': {
34
+ 'file1.py': 'print("File 1")',
35
+ 'file2.py': 'print("File 2")',
36
+ 'file3.pyc': 'Compiled code',
37
+ }
38
+ },
39
+ '.gitignore': gitignore_content,
40
+ }
41
+
42
+ create_directory_structure(temp_dir, structure)
43
+
44
+ args = type('Args', (object,), {
45
+ 'path': temp_dir,
46
+ 'output': os.path.join(temp_dir, 'output.md'),
47
+ 'structure_only': False,
48
+ 'debug': False
49
+ })
50
+
51
+ with patch('reposnap.controllers.project_controller.GitRepo') as MockGitRepo:
52
+ mock_git_repo_instance = MockGitRepo.return_value
53
+ mock_git_repo_instance.get_git_files.return_value = [
54
+ Path('src/module/file1.py'),
55
+ Path('src/module/file2.py'),
56
+ Path('.gitignore')
57
+ ]
58
+
59
+ controller = ProjectController(args)
60
+ controller.run()
61
+
62
+ # Read the output file
63
+ with open(args.output, 'r') as f:
64
+ output_content = f.read()
65
+
66
+ # Check that contents of .py files are included
67
+ assert 'print("File 1")' in output_content
68
+ assert 'print("File 2")' in output_content
69
+ # .pyc files should be ignored
70
+ assert 'Compiled code' not in output_content
71
+
72
+
73
+ @patch('reposnap.controllers.project_controller.MarkdownGenerator')
74
+ @patch('reposnap.controllers.project_controller.FileSystem')
75
+ @patch('reposnap.controllers.project_controller.GitRepo')
76
+ def test_project_controller_run(mock_git_repo, mock_file_system, mock_markdown_generator):
77
+ # Setup mocks
78
+ mock_git_repo_instance = MagicMock()
79
+ mock_file_system_instance = MagicMock()
80
+ mock_markdown_generator_instance = MagicMock()
81
+
82
+ mock_git_repo.return_value = mock_git_repo_instance
83
+ mock_file_system.return_value = mock_file_system_instance
84
+ mock_markdown_generator.return_value = mock_markdown_generator_instance
85
+
86
+ mock_git_repo_instance.get_git_files.return_value = ['file1.py', 'file2.py']
87
+ mock_file_system_instance.build_tree_structure.return_value = {'dir': {'file1.py': 'file1.py'}}
88
+
89
+ args = MagicMock()
90
+ args.path = 'root_dir'
91
+ args.output = 'output.md'
92
+ args.structure_only = False
93
+
94
+ controller = ProjectController(args)
95
+ controller.run()
96
+
97
+ mock_markdown_generator_instance.generate_markdown.assert_called_once()
@@ -1,64 +0,0 @@
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.")
@@ -1,46 +0,0 @@
1
- # src/reposnap/interfaces/cli.py
2
-
3
- import argparse
4
- import logging
5
- import os
6
- from reposnap.core.collector import ProjectContentCollector
7
- from pathlib import Path
8
-
9
-
10
- def main():
11
- parser = argparse.ArgumentParser(description='Generate a Markdown representation of a Git repository.')
12
- parser.add_argument('path', help='Path to the Git repository or subdirectory.')
13
- parser.add_argument('-o', '--output', help='Output Markdown file', default='output.md')
14
- parser.add_argument('--structure-only', action='store_true',
15
- help='Only include the file structure without content.')
16
- parser.add_argument('--debug', action='store_true', help='Enable debug-level logging.')
17
-
18
- args = parser.parse_args()
19
-
20
- log_level = logging.DEBUG if args.debug else logging.INFO
21
- logging.basicConfig(level=log_level, format='%(asctime)s - %(levelname)s - %(message)s')
22
-
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)
43
- collector.collect_and_generate()
44
-
45
- if __name__ == "__main__":
46
- main()
@@ -1,27 +0,0 @@
1
- # src/reposnap/tests/test_collector.py
2
-
3
- import pytest # noqa
4
- from unittest.mock import patch, MagicMock
5
- from reposnap.core.collector import ProjectContentCollector
6
-
7
-
8
- @patch('reposnap.core.collector.MarkdownGenerator')
9
- @patch('reposnap.core.collector.FileSystem')
10
- @patch('reposnap.core.collector.GitRepo')
11
- def test_collect_and_generate(mock_git_repo, mock_file_system, mock_markdown_generator):
12
- # Setup mocks
13
- mock_git_repo_instance = MagicMock()
14
- mock_file_system_instance = MagicMock()
15
- mock_markdown_generator_instance = MagicMock()
16
-
17
- mock_git_repo.return_value = mock_git_repo_instance
18
- mock_file_system.return_value = mock_file_system_instance
19
- mock_markdown_generator.return_value = mock_markdown_generator_instance
20
-
21
- mock_git_repo_instance.get_git_files.return_value = ['file1.py', 'file2.py']
22
- mock_file_system_instance.build_tree_structure.return_value = {'dir': {'file1.py': 'file1.py'}}
23
-
24
- collector = ProjectContentCollector('root_dir', 'output.md', False, ['*.log'])
25
- collector.collect_and_generate()
26
-
27
- mock_markdown_generator_instance.generate_markdown.assert_called_once()
File without changes
File without changes
File without changes
File without changes
File without changes