reposnap 0.6.3__py3-none-any.whl → 0.6.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reposnap
3
- Version: 0.6.3
3
+ Version: 0.6.4
4
4
  Summary: Generate a Markdown file with all contents of your project
5
5
  Author: agoloborodko
6
6
  License-File: LICENSE
@@ -0,0 +1,5 @@
1
+ reposnap-0.6.4.dist-info/METADATA,sha256=ZD55J-JVXgwC_ENrX8dmcIT5s3KlJqG26gPAzj74wlU,5348
2
+ reposnap-0.6.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
3
+ reposnap-0.6.4.dist-info/entry_points.txt,sha256=o3GyO7bpR0dujPCjsvvZMPv4pXNJlFwD49_pA1r5FOA,102
4
+ reposnap-0.6.4.dist-info/licenses/LICENSE,sha256=Aj7WCYBXi98pvi723HPn4GDRyjxToNWb3PC6j1_lnPk,1069
5
+ reposnap-0.6.4.dist-info/RECORD,,
reposnap/__init__.py DELETED
File without changes
File without changes
@@ -1,199 +0,0 @@
1
- import logging
2
- from pathlib import Path
3
- from reposnap.core.file_system import FileSystem
4
- from reposnap.models.file_tree import FileTree
5
- import pathspec
6
- from typing import List, Optional
7
-
8
- class ProjectController:
9
- def __init__(self, args: Optional[object] = None):
10
- self.logger = logging.getLogger(__name__)
11
- # Always determine repository root using Git (or cwd)
12
- self.root_dir = self._get_repo_root().resolve()
13
- if args:
14
- self.args = args
15
- # Treat positional arguments as literal file/directory names.
16
- input_paths = [Path(p) for p in (args.paths if hasattr(args, 'paths') else [args.path])]
17
- self.input_paths = []
18
- for p in input_paths:
19
- candidate = (self.root_dir / p).resolve()
20
- if candidate.exists():
21
- try:
22
- rel = candidate.relative_to(self.root_dir)
23
- if rel != Path('.'):
24
- self.input_paths.append(rel)
25
- except ValueError:
26
- self.logger.warning(f"Path {p} is not under repository root {self.root_dir}. Ignoring.")
27
- else:
28
- self.logger.warning(f"Path {p} does not exist relative to repository root {self.root_dir}.")
29
- self.output_file: Path = Path(args.output).resolve() if args.output else self.root_dir / 'output.md'
30
- self.structure_only: bool = args.structure_only if hasattr(args, 'structure_only') else False
31
- self.include_patterns: List[str] = args.include if hasattr(args, 'include') else []
32
- self.exclude_patterns: List[str] = args.exclude if hasattr(args, 'exclude') else []
33
- else:
34
- self.args = None
35
- self.input_paths = []
36
- self.output_file = self.root_dir / 'output.md'
37
- self.structure_only = False
38
- self.include_patterns = []
39
- self.exclude_patterns = []
40
- self.file_tree: Optional[FileTree] = None
41
- self.gitignore_patterns: List[str] = []
42
- if self.root_dir:
43
- self.gitignore_patterns = self._load_gitignore_patterns()
44
-
45
- def _get_repo_root(self) -> Path:
46
- """
47
- Determine the repository root using Git if available,
48
- otherwise use the current directory.
49
- """
50
- from git import Repo, InvalidGitRepositoryError
51
- try:
52
- repo = Repo(Path.cwd(), search_parent_directories=True)
53
- return Path(repo.working_tree_dir).resolve()
54
- except InvalidGitRepositoryError:
55
- self.logger.warning("Not a git repository. Using current directory as root.")
56
- return Path.cwd().resolve()
57
-
58
- def set_root_dir(self, root_dir: Path) -> None:
59
- self.root_dir = root_dir.resolve()
60
- self.gitignore_patterns = self._load_gitignore_patterns()
61
-
62
- def get_file_tree(self) -> Optional[FileTree]:
63
- return self.file_tree
64
-
65
- def _apply_include_exclude(self, files: List[Path]) -> List[Path]:
66
- """Filter a list of file paths using include and exclude patterns."""
67
- def adjust_patterns(patterns):
68
- adjusted = []
69
- for p in patterns:
70
- if any(ch in p for ch in ['*', '?', '[']):
71
- adjusted.append(p)
72
- else:
73
- adjusted.append(f'*{p}*')
74
- return adjusted
75
- if self.include_patterns:
76
- inc = adjust_patterns(self.include_patterns)
77
- spec_inc = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, inc)
78
- files = [f for f in files if spec_inc.match_file(f.as_posix())]
79
- if self.exclude_patterns:
80
- exc = adjust_patterns(self.exclude_patterns)
81
- spec_exc = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, exc)
82
- files = [f for f in files if not spec_exc.match_file(f.as_posix())]
83
- return files
84
-
85
- def collect_file_tree(self) -> None:
86
- self.logger.info("Collecting files from Git tracked files if available.")
87
- try:
88
- from reposnap.core.git_repo import GitRepo
89
- git_repo = GitRepo(self.root_dir)
90
- all_files = git_repo.get_git_files()
91
- self.logger.debug(f"Git tracked files: {all_files}")
92
- except Exception as e:
93
- self.logger.warning(f"Error obtaining Git tracked files: {e}.")
94
- all_files = []
95
- # If Git returns an empty list but files exist on disk, fall back to filesystem scan.
96
- if not all_files:
97
- file_list = [p for p in self.root_dir.rglob("*") if p.is_file()]
98
- if file_list:
99
- self.logger.info("Git tracked files empty, using filesystem scan fallback.")
100
- all_files = []
101
- for path in file_list:
102
- try:
103
- rel = path.relative_to(self.root_dir)
104
- all_files.append(rel)
105
- except ValueError:
106
- continue
107
- all_files = self._apply_include_exclude(all_files)
108
- self.logger.debug(f"All files after applying include/exclude: {all_files}")
109
- if self.input_paths:
110
- trees = []
111
- for input_path in self.input_paths:
112
- subset = [f for f in all_files if f == input_path or list(f.parts[:len(input_path.parts)]) == list(input_path.parts)]
113
- self.logger.debug(f"Files for input path '{input_path}': {subset}")
114
- if subset:
115
- tree = FileSystem(self.root_dir).build_tree_structure(subset)
116
- trees.append(tree)
117
- if trees:
118
- merged_tree = self.merge_trees(trees)
119
- else:
120
- merged_tree = {}
121
- else:
122
- merged_tree = FileSystem(self.root_dir).build_tree_structure(all_files)
123
- self.logger.info("Merged tree built from input paths.")
124
- self.file_tree = FileTree(merged_tree)
125
- self.logger.debug(f"Merged tree structure: {self.file_tree.structure}")
126
-
127
- def merge_trees(self, trees: List[dict]) -> dict:
128
- """Recursively merge a list of tree dictionaries."""
129
- merged = {}
130
- for tree in trees:
131
- merged = self._merge_two_trees(merged, tree)
132
- return merged
133
-
134
- def _merge_two_trees(self, tree1: dict, tree2: dict) -> dict:
135
- merged = dict(tree1)
136
- for key, value in tree2.items():
137
- if key in merged:
138
- if isinstance(merged[key], dict) and isinstance(value, dict):
139
- merged[key] = self._merge_two_trees(merged[key], value)
140
- else:
141
- merged[key] = value
142
- else:
143
- merged[key] = value
144
- return merged
145
-
146
- def apply_filters(self) -> None:
147
- self.logger.info("Applying .gitignore filters to the merged tree.")
148
- spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, self.gitignore_patterns)
149
- self.logger.debug(f".gitignore patterns: {self.gitignore_patterns}")
150
- self.file_tree.filter_files(spec)
151
-
152
- def generate_output(self) -> None:
153
- self.logger.info("Starting Markdown generation.")
154
- from reposnap.core.markdown_generator import MarkdownGenerator
155
- markdown_generator = MarkdownGenerator(
156
- root_dir=self.root_dir,
157
- output_file=self.output_file,
158
- structure_only=self.structure_only
159
- )
160
- markdown_generator.generate_markdown(self.file_tree.structure, self.file_tree.get_all_files())
161
- self.logger.info(f"Markdown generated at {self.output_file}.")
162
-
163
- def generate_output_from_selected(self, selected_files: set) -> None:
164
- self.logger.info("Generating Markdown from selected files.")
165
- pruned_tree = self.file_tree.prune_tree(selected_files)
166
- from reposnap.core.markdown_generator import MarkdownGenerator
167
- markdown_generator = MarkdownGenerator(
168
- root_dir=self.root_dir,
169
- output_file=self.output_file,
170
- structure_only=False,
171
- hide_untoggled=True
172
- )
173
- markdown_generator.generate_markdown(pruned_tree, [Path(f) for f in selected_files])
174
- self.logger.info(f"Markdown generated at {self.output_file}.")
175
-
176
- def run(self) -> None:
177
- """Run the entire process: collect files, apply filters, and generate Markdown."""
178
- self.collect_file_tree()
179
- self.apply_filters()
180
- self.generate_output()
181
-
182
- def _load_gitignore_patterns(self) -> List[str]:
183
- gitignore_path = self.root_dir / '.gitignore'
184
- if not gitignore_path.exists():
185
- for parent in self.root_dir.parents:
186
- candidate = parent / '.gitignore'
187
- if candidate.exists():
188
- gitignore_path = candidate
189
- break
190
- else:
191
- gitignore_path = None
192
- if gitignore_path and gitignore_path.exists():
193
- with gitignore_path.open('r') as gitignore:
194
- patterns = [line.strip() for line in gitignore if line.strip() and not line.strip().startswith('#')]
195
- self.logger.debug(f"Loaded .gitignore patterns from {gitignore_path.parent}: {patterns}")
196
- return patterns
197
- else:
198
- self.logger.debug(f"No .gitignore found starting from {self.root_dir}.")
199
- return []
reposnap/core/__init__.py DELETED
File without changes
@@ -1,32 +0,0 @@
1
- # src/reposnap/core/file_system.py
2
-
3
- import logging
4
- from pathlib import Path
5
- from typing import List, Dict, Any
6
-
7
-
8
- class FileSystem:
9
- def __init__(self, root_dir: Path):
10
- self.root_dir: Path = root_dir.resolve()
11
- self.logger = logging.getLogger(__name__)
12
-
13
- def build_tree_structure(self, files: List[Path]) -> Dict[str, Any]:
14
- """
15
- Builds a hierarchical tree structure from the list of files.
16
-
17
- Args:
18
- files (List[Path]): List of file paths relative to root_dir.
19
-
20
- Returns:
21
- Dict[str, Any]: Nested dictionary representing the directory structure.
22
- """
23
- tree: Dict[str, Any] = {}
24
- self.logger.debug("Building tree structure.")
25
- for relative_path in files:
26
- parts = relative_path.parts
27
- current_level = tree
28
- for part in parts[:-1]:
29
- current_level = current_level.setdefault(part, {})
30
- current_level[parts[-1]] = None # Indicate a file node
31
- self.logger.debug(f"Tree structure built: {tree}")
32
- return tree
reposnap/core/git_repo.py DELETED
@@ -1,33 +0,0 @@
1
- # src/reposnap/core/git_repo.py
2
-
3
- import logging
4
- from pathlib import Path
5
- from git import Repo, InvalidGitRepositoryError
6
- from typing import List
7
-
8
-
9
- class GitRepo:
10
- def __init__(self, repo_path: Path):
11
- self.repo_path: Path = repo_path.resolve()
12
- self.logger = logging.getLogger(__name__)
13
-
14
- def get_git_files(self) -> List[Path]:
15
- try:
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()
19
- self.logger.debug(f"Git files from {repo_root}: {git_files}")
20
- git_files_relative: List[Path] = []
21
- for f in git_files:
22
- absolute_path: Path = (repo_root / f).resolve()
23
- try:
24
- relative_path: Path = absolute_path.relative_to(self.repo_path)
25
- git_files_relative.append(relative_path)
26
- except ValueError:
27
- # Skip files not under root_dir
28
- continue
29
- return git_files_relative
30
- except InvalidGitRepositoryError:
31
- self.logger.error(f"Invalid Git repository at: {self.repo_path}")
32
- return []
33
-
@@ -1,79 +0,0 @@
1
- # src/reposnap/core/markdown_generator.py
2
-
3
- import logging
4
- from pathlib import Path
5
- from reposnap.utils.path_utils import format_tree
6
- from typing import List, Dict, Any
7
-
8
-
9
- class MarkdownGenerator:
10
- def __init__(self, root_dir: Path, output_file: Path, structure_only: bool = False, hide_untoggled: bool = False):
11
- self.root_dir: Path = root_dir.resolve()
12
- self.output_file: Path = output_file.resolve()
13
- self.structure_only: bool = structure_only
14
- self.hide_untoggled: bool = hide_untoggled
15
- self.logger = logging.getLogger(__name__)
16
-
17
- def generate_markdown(self, tree_structure: Dict[str, Any], files: List[Path]) -> None:
18
- """
19
- Generates the Markdown file based on the provided tree structure and files.
20
-
21
- Args:
22
- tree_structure (Dict[str, Any]): The hierarchical structure of the project files.
23
- files (List[Path]): List of file paths to include in the markdown.
24
- """
25
- self._write_header(tree_structure)
26
- if not self.structure_only:
27
- self._write_file_contents(files)
28
-
29
- def _write_header(self, tree_structure: Dict[str, Any]) -> None:
30
- """
31
- Writes the header and project structure to the Markdown file.
32
- """
33
- self.logger.debug("Writing Markdown header and project structure.")
34
- try:
35
- with self.output_file.open('w', encoding='utf-8') as f:
36
- f.write("# Project Structure\n\n")
37
- f.write("```\n")
38
- for line in format_tree(tree_structure, hide_untoggled=self.hide_untoggled):
39
- f.write(line)
40
- f.write("```\n\n")
41
- self.logger.debug("Header and project structure written successfully.")
42
- except IOError as e:
43
- self.logger.error(f"Failed to write header to {self.output_file}: {e}")
44
- raise
45
-
46
- def _write_file_contents(self, files: List[Path]) -> None:
47
- """
48
- Writes the contents of each file to the Markdown file.
49
-
50
- Args:
51
- files (List[Path]): List of file paths relative to root_dir.
52
- """
53
- self.logger.debug("Writing file contents to Markdown.")
54
- for relative_path in files:
55
- file_path: Path = self.root_dir / relative_path
56
-
57
- if not file_path.exists():
58
- self.logger.debug(f"File not found: {file_path}. Skipping.")
59
- continue
60
-
61
- try:
62
- self._write_file_content(file_path, relative_path.as_posix())
63
- except UnicodeDecodeError as e:
64
- self.logger.error(f"UnicodeDecodeError for file {file_path}: {e}")
65
-
66
-
67
- def _write_file_content(self, file_path: Path, relative_path: str) -> None:
68
- """
69
- Writes the content of a single file to the Markdown file with syntax highlighting.
70
- """
71
- try:
72
- with file_path.open('r', encoding='utf-8') as f:
73
- content: str = f.read()
74
- with self.output_file.open('a', encoding='utf-8') as f:
75
- f.write(f"## {relative_path}\n\n")
76
- f.write("```python\n" if file_path.suffix == '.py' else "```\n")
77
- f.write(f"{content}\n```\n\n")
78
- except IOError as e:
79
- self.logger.error(f"Error reading or writing file {file_path}: {e}")
File without changes
@@ -1,37 +0,0 @@
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(
10
- description='Generate a Markdown representation of a Git repository.'
11
- )
12
- # Changed positional argument to allow one or more paths.
13
- parser.add_argument(
14
- 'paths',
15
- nargs='+',
16
- help='One or more paths (files or directories) to include in the Markdown output.'
17
- )
18
- parser.add_argument('-o', '--output', help='Output Markdown file', default='output.md')
19
- parser.add_argument('--structure-only', action='store_true',
20
- help='Only include the file structure without content.')
21
- parser.add_argument('--debug', action='store_true', help='Enable debug-level logging.')
22
- parser.add_argument('-i', '--include', nargs='*', default=[],
23
- help='File/folder patterns to include.')
24
- parser.add_argument('-e', '--exclude', nargs='*', default=[],
25
- help='File/folder patterns to exclude.')
26
-
27
- args = parser.parse_args()
28
-
29
- log_level = logging.DEBUG if args.debug else logging.INFO
30
- logging.basicConfig(level=log_level, format='%(asctime)s - %(levelname)s - %(message)s')
31
-
32
- controller = ProjectController(args)
33
- controller.run()
34
-
35
-
36
- if __name__ == "__main__":
37
- main()
@@ -1,155 +0,0 @@
1
- # src/reposnap/interfaces/gui.py
2
-
3
- import urwid
4
- from pathlib import Path
5
- from reposnap.controllers.project_controller import ProjectController
6
-
7
-
8
- class MyCheckBox(urwid.CheckBox):
9
- def __init__(self, label, user_data=None, **kwargs):
10
- super().__init__(label, **kwargs)
11
- self.user_data = user_data
12
-
13
-
14
- class RepoSnapGUI:
15
- def __init__(self):
16
- self.controller = ProjectController()
17
- self.root_dir = Path('.').resolve()
18
- self.file_tree = None
19
- self.selected_files = set()
20
-
21
- self.main_loop = None
22
- self.build_main_menu()
23
-
24
- def build_main_menu(self):
25
- self.root_dir_edit = urwid.Edit(('bold', "Root Directory: "), str(self.root_dir))
26
- scan_button = urwid.Button("Scan", on_press=self.on_scan)
27
-
28
- main_menu = urwid.Frame(
29
- header=urwid.Text(('bold', "RepoSnap - Main Menu")),
30
- body=urwid.Padding(
31
- urwid.LineBox(
32
- urwid.ListBox(
33
- urwid.SimpleFocusListWalker([
34
- self.root_dir_edit
35
- ])
36
- ),
37
- title="Enter Root Directory"
38
- ),
39
- left=2, right=2
40
- ),
41
- footer=urwid.Padding(scan_button, align='center')
42
- )
43
-
44
- self.main_widget = main_menu
45
-
46
- def on_scan(self, button):
47
- self.root_dir = Path(self.root_dir_edit.edit_text).resolve()
48
- self.controller.set_root_dir(self.root_dir)
49
- self.controller.collect_file_tree()
50
- self.file_tree = self.controller.get_file_tree()
51
- self.build_file_tree_menu()
52
-
53
- def build_file_tree_menu(self):
54
- tree_widgets = self.build_tree_widget(self.file_tree.structure)
55
- tree_listbox = urwid.ListBox(urwid.SimpleFocusListWalker(tree_widgets))
56
- render_button = urwid.Button("Render", on_press=self.on_render)
57
-
58
- tree_menu = urwid.Frame(
59
- header=urwid.Text(('bold', f"File Tree of {self.root_dir}")),
60
- body=urwid.LineBox(tree_listbox),
61
- footer=urwid.Padding(render_button, align='center')
62
- )
63
-
64
- self.main_widget = tree_menu
65
- self.refresh()
66
-
67
- def build_tree_widget(self, tree_structure, parent_path="", level=0):
68
- widgets = []
69
- for key, value in sorted(tree_structure.items()):
70
- node_path = f"{parent_path}/{key}".lstrip('/')
71
- checkbox = MyCheckBox(
72
- key,
73
- user_data={'path': node_path, 'level': level},
74
- state=False,
75
- on_state_change=self.on_checkbox_change
76
- )
77
- indented_checkbox = urwid.Padding(checkbox, left=4*level)
78
- widgets.append(indented_checkbox)
79
- if isinstance(value, dict):
80
- widgets.extend(self.build_tree_widget(value, node_path, level=level+1))
81
- return widgets
82
-
83
- def on_checkbox_change(self, checkbox, state):
84
- user_data = checkbox.user_data
85
- node_path = user_data['path']
86
- level = user_data['level']
87
- if state:
88
- self.selected_files.add(node_path)
89
- else:
90
- self.selected_files.discard(node_path)
91
- # Handle toggling all children
92
- self.toggle_children(checkbox, state, level)
93
-
94
- def toggle_children(self, checkbox, state, level):
95
- listbox = self.main_widget.body.original_widget.body
96
- walker = listbox
97
- # Find the index of the Padding widget that contains the checkbox
98
- idx = None
99
- for i, widget in enumerate(walker):
100
- if isinstance(widget, urwid.Padding) and widget.original_widget == checkbox:
101
- idx = i
102
- break
103
- if idx is None:
104
- return
105
- idx += 1
106
- while idx < len(walker):
107
- widget = walker[idx]
108
- if isinstance(widget, urwid.Padding):
109
- checkbox_widget = widget.original_widget
110
- widget_user_data = checkbox_widget.user_data
111
- widget_level = widget_user_data['level']
112
- if widget_level > level:
113
- checkbox_widget.set_state(state, do_callback=False)
114
- node_path = widget_user_data['path']
115
- if state:
116
- self.selected_files.add(node_path)
117
- else:
118
- self.selected_files.discard(node_path)
119
- idx += 1
120
- else:
121
- break
122
- else:
123
- idx += 1
124
-
125
- def on_render(self, button):
126
- self.controller.generate_output_from_selected(self.selected_files)
127
- message = urwid.Text(('bold', f"Markdown generated at {self.controller.output_file}"))
128
- exit_button = urwid.Button("Exit", on_press=self.exit_program)
129
- result_menu = urwid.Frame(
130
- header=urwid.Text(('bold', "Success")),
131
- body=urwid.Filler(message, valign='middle'),
132
- footer=urwid.Padding(exit_button, align='center')
133
- )
134
- self.main_widget = result_menu
135
- self.refresh()
136
-
137
- def refresh(self):
138
- if self.main_loop:
139
- self.main_loop.widget = self.main_widget
140
-
141
- def exit_program(self, button):
142
- raise urwid.ExitMainLoop()
143
-
144
- def run(self):
145
- self.main_loop = urwid.MainLoop(self.main_widget)
146
- self.main_loop.run()
147
-
148
-
149
- def main():
150
- app = RepoSnapGUI()
151
- app.run()
152
-
153
-
154
- if __name__ == "__main__":
155
- main()
File without changes
@@ -1,79 +0,0 @@
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
- # Exclude the file if either the full path OR its basename matches a .gitignore pattern.
52
- if not spec.match_file(current_path) and not spec.match_file(Path(current_path).name):
53
- filtered_subtree[key] = value
54
- return filtered_subtree
55
-
56
- def prune_tree(self, selected_files: set) -> Dict[str, Any]:
57
- """
58
- Prunes the tree to include only the selected files and their directories.
59
-
60
- Args:
61
- selected_files (set): Set of selected file paths.
62
-
63
- Returns:
64
- Dict[str, Any]: Pruned tree structure.
65
- """
66
- return self._prune_tree(self.structure, selected_files)
67
-
68
- def _prune_tree(self, subtree: Dict[str, Any], selected_files: set, path_prefix: str = '') -> Dict[str, Any]:
69
- pruned_subtree: Dict[str, Any] = {}
70
- for key, value in subtree.items():
71
- current_path: str = f"{path_prefix}/{key}".lstrip('/')
72
- if isinstance(value, dict):
73
- pruned_value: Dict[str, Any] = self._prune_tree(value, selected_files, current_path)
74
- if pruned_value:
75
- pruned_subtree[key] = pruned_value
76
- else:
77
- if current_path in selected_files:
78
- pruned_subtree[key] = value
79
- return pruned_subtree
File without changes
@@ -1,13 +0,0 @@
1
- # src/reposnap/utils/path_utils.py
2
- from typing import Dict, Generator, Any
3
-
4
-
5
- def format_tree(tree: Dict[str, Any], indent: str = '', hide_untoggled: bool = False) -> Generator[str, None, None]:
6
- for key, value in tree.items():
7
- if value == '<hidden>':
8
- yield f"{indent}<...>\n"
9
- elif isinstance(value, dict):
10
- yield f"{indent}{key}/\n"
11
- yield from format_tree(value, indent + ' ', hide_untoggled)
12
- else:
13
- yield f"{indent}{key}\n"
@@ -1,19 +0,0 @@
1
- reposnap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- reposnap/controllers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- reposnap/controllers/project_controller.py,sha256=u3agILanSms5Gx4D5e6EWhHrb6B08saz5udct8yVS-s,9353
4
- reposnap/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- reposnap/core/file_system.py,sha256=82gwvmgrsWf63paMrIz-Z0eqIjbqt9_-vujdXlJJoFE,1074
6
- reposnap/core/git_repo.py,sha256=2u_ILkV-Ur7qr1WHmHM2yg44Ggft61RsdbZLsZaQ5NU,1256
7
- reposnap/core/markdown_generator.py,sha256=Ld6ix4gzkLJJyeUoWHwhpbAf3DvEC5E0S1DykYnLGnQ,3297
8
- reposnap/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- reposnap/interfaces/cli.py,sha256=JzTNDibzuRRmnWg-gBfKJ2tSlh-NYSL_3q6J-Erjrr8,1374
10
- reposnap/interfaces/gui.py,sha256=pzWQbW55gBNZu4tXRdBFic39upGtYxew91FSiEvalj0,5421
11
- reposnap/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- reposnap/models/file_tree.py,sha256=0WcSDbFH5pSZHyWxWtmz-FF4_ELnZ3Byz2iXN4Tpijw,3206
13
- reposnap/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- reposnap/utils/path_utils.py,sha256=7072816LCP8Q8XBydn0iknmfrObPO_-2rFqpbAvPrjY,501
15
- reposnap-0.6.3.dist-info/METADATA,sha256=wPRE4NKJuzwMKTyYD8e2nPUYUNjwM81kCEsVz18yWTY,5348
16
- reposnap-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- reposnap-0.6.3.dist-info/entry_points.txt,sha256=o3GyO7bpR0dujPCjsvvZMPv4pXNJlFwD49_pA1r5FOA,102
18
- reposnap-0.6.3.dist-info/licenses/LICENSE,sha256=Aj7WCYBXi98pvi723HPn4GDRyjxToNWb3PC6j1_lnPk,1069
19
- reposnap-0.6.3.dist-info/RECORD,,