reposnap 0.6.3__py3-none-any.whl → 0.6.5__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/controllers/project_controller.py +74 -26
- reposnap/core/git_repo.py +0 -1
- reposnap/core/markdown_generator.py +66 -54
- reposnap/interfaces/cli.py +33 -14
- reposnap/interfaces/gui.py +29 -26
- reposnap/models/file_tree.py +21 -9
- reposnap/utils/path_utils.py +5 -3
- {reposnap-0.6.3.dist-info → reposnap-0.6.5.dist-info}/METADATA +1 -1
- reposnap-0.6.5.dist-info/RECORD +19 -0
- reposnap-0.6.3.dist-info/RECORD +0 -19
- {reposnap-0.6.3.dist-info → reposnap-0.6.5.dist-info}/WHEEL +0 -0
- {reposnap-0.6.3.dist-info → reposnap-0.6.5.dist-info}/entry_points.txt +0 -0
- {reposnap-0.6.3.dist-info → reposnap-0.6.5.dist-info}/licenses/LICENSE +0 -0
@@ -5,6 +5,7 @@ from reposnap.models.file_tree import FileTree
|
|
5
5
|
import pathspec
|
6
6
|
from typing import List, Optional
|
7
7
|
|
8
|
+
|
8
9
|
class ProjectController:
|
9
10
|
def __init__(self, args: Optional[object] = None):
|
10
11
|
self.logger = logging.getLogger(__name__)
|
@@ -13,27 +14,43 @@ class ProjectController:
|
|
13
14
|
if args:
|
14
15
|
self.args = args
|
15
16
|
# Treat positional arguments as literal file/directory names.
|
16
|
-
input_paths = [
|
17
|
+
input_paths = [
|
18
|
+
Path(p) for p in (args.paths if hasattr(args, "paths") else [args.path])
|
19
|
+
]
|
17
20
|
self.input_paths = []
|
18
21
|
for p in input_paths:
|
19
22
|
candidate = (self.root_dir / p).resolve()
|
20
23
|
if candidate.exists():
|
21
24
|
try:
|
22
25
|
rel = candidate.relative_to(self.root_dir)
|
23
|
-
if rel != Path(
|
26
|
+
if rel != Path("."):
|
24
27
|
self.input_paths.append(rel)
|
25
28
|
except ValueError:
|
26
|
-
self.logger.warning(
|
29
|
+
self.logger.warning(
|
30
|
+
f"Path {p} is not under repository root {self.root_dir}. Ignoring."
|
31
|
+
)
|
27
32
|
else:
|
28
|
-
self.logger.warning(
|
29
|
-
|
30
|
-
|
31
|
-
self.
|
32
|
-
|
33
|
+
self.logger.warning(
|
34
|
+
f"Path {p} does not exist relative to repository root {self.root_dir}."
|
35
|
+
)
|
36
|
+
self.output_file: Path = (
|
37
|
+
Path(args.output).resolve()
|
38
|
+
if args.output
|
39
|
+
else self.root_dir / "output.md"
|
40
|
+
)
|
41
|
+
self.structure_only: bool = (
|
42
|
+
args.structure_only if hasattr(args, "structure_only") else False
|
43
|
+
)
|
44
|
+
self.include_patterns: List[str] = (
|
45
|
+
args.include if hasattr(args, "include") else []
|
46
|
+
)
|
47
|
+
self.exclude_patterns: List[str] = (
|
48
|
+
args.exclude if hasattr(args, "exclude") else []
|
49
|
+
)
|
33
50
|
else:
|
34
51
|
self.args = None
|
35
52
|
self.input_paths = []
|
36
|
-
self.output_file = self.root_dir /
|
53
|
+
self.output_file = self.root_dir / "output.md"
|
37
54
|
self.structure_only = False
|
38
55
|
self.include_patterns = []
|
39
56
|
self.exclude_patterns = []
|
@@ -48,11 +65,14 @@ class ProjectController:
|
|
48
65
|
otherwise use the current directory.
|
49
66
|
"""
|
50
67
|
from git import Repo, InvalidGitRepositoryError
|
68
|
+
|
51
69
|
try:
|
52
70
|
repo = Repo(Path.cwd(), search_parent_directories=True)
|
53
71
|
return Path(repo.working_tree_dir).resolve()
|
54
72
|
except InvalidGitRepositoryError:
|
55
|
-
self.logger.warning(
|
73
|
+
self.logger.warning(
|
74
|
+
"Not a git repository. Using current directory as root."
|
75
|
+
)
|
56
76
|
return Path.cwd().resolve()
|
57
77
|
|
58
78
|
def set_root_dir(self, root_dir: Path) -> None:
|
@@ -64,21 +84,27 @@ class ProjectController:
|
|
64
84
|
|
65
85
|
def _apply_include_exclude(self, files: List[Path]) -> List[Path]:
|
66
86
|
"""Filter a list of file paths using include and exclude patterns."""
|
87
|
+
|
67
88
|
def adjust_patterns(patterns):
|
68
89
|
adjusted = []
|
69
90
|
for p in patterns:
|
70
|
-
if any(ch in p for ch in [
|
91
|
+
if any(ch in p for ch in ["*", "?", "["]):
|
71
92
|
adjusted.append(p)
|
72
93
|
else:
|
73
|
-
adjusted.append(f
|
94
|
+
adjusted.append(f"*{p}*")
|
74
95
|
return adjusted
|
96
|
+
|
75
97
|
if self.include_patterns:
|
76
98
|
inc = adjust_patterns(self.include_patterns)
|
77
|
-
spec_inc = pathspec.PathSpec.from_lines(
|
99
|
+
spec_inc = pathspec.PathSpec.from_lines(
|
100
|
+
pathspec.patterns.GitWildMatchPattern, inc
|
101
|
+
)
|
78
102
|
files = [f for f in files if spec_inc.match_file(f.as_posix())]
|
79
103
|
if self.exclude_patterns:
|
80
104
|
exc = adjust_patterns(self.exclude_patterns)
|
81
|
-
spec_exc = pathspec.PathSpec.from_lines(
|
105
|
+
spec_exc = pathspec.PathSpec.from_lines(
|
106
|
+
pathspec.patterns.GitWildMatchPattern, exc
|
107
|
+
)
|
82
108
|
files = [f for f in files if not spec_exc.match_file(f.as_posix())]
|
83
109
|
return files
|
84
110
|
|
@@ -86,6 +112,7 @@ class ProjectController:
|
|
86
112
|
self.logger.info("Collecting files from Git tracked files if available.")
|
87
113
|
try:
|
88
114
|
from reposnap.core.git_repo import GitRepo
|
115
|
+
|
89
116
|
git_repo = GitRepo(self.root_dir)
|
90
117
|
all_files = git_repo.get_git_files()
|
91
118
|
self.logger.debug(f"Git tracked files: {all_files}")
|
@@ -96,7 +123,9 @@ class ProjectController:
|
|
96
123
|
if not all_files:
|
97
124
|
file_list = [p for p in self.root_dir.rglob("*") if p.is_file()]
|
98
125
|
if file_list:
|
99
|
-
self.logger.info(
|
126
|
+
self.logger.info(
|
127
|
+
"Git tracked files empty, using filesystem scan fallback."
|
128
|
+
)
|
100
129
|
all_files = []
|
101
130
|
for path in file_list:
|
102
131
|
try:
|
@@ -109,7 +138,12 @@ class ProjectController:
|
|
109
138
|
if self.input_paths:
|
110
139
|
trees = []
|
111
140
|
for input_path in self.input_paths:
|
112
|
-
subset = [
|
141
|
+
subset = [
|
142
|
+
f
|
143
|
+
for f in all_files
|
144
|
+
if f == input_path
|
145
|
+
or list(f.parts[: len(input_path.parts)]) == list(input_path.parts)
|
146
|
+
]
|
113
147
|
self.logger.debug(f"Files for input path '{input_path}': {subset}")
|
114
148
|
if subset:
|
115
149
|
tree = FileSystem(self.root_dir).build_tree_structure(subset)
|
@@ -145,32 +179,40 @@ class ProjectController:
|
|
145
179
|
|
146
180
|
def apply_filters(self) -> None:
|
147
181
|
self.logger.info("Applying .gitignore filters to the merged tree.")
|
148
|
-
spec = pathspec.PathSpec.from_lines(
|
182
|
+
spec = pathspec.PathSpec.from_lines(
|
183
|
+
pathspec.patterns.GitWildMatchPattern, self.gitignore_patterns
|
184
|
+
)
|
149
185
|
self.logger.debug(f".gitignore patterns: {self.gitignore_patterns}")
|
150
186
|
self.file_tree.filter_files(spec)
|
151
187
|
|
152
188
|
def generate_output(self) -> None:
|
153
189
|
self.logger.info("Starting Markdown generation.")
|
154
190
|
from reposnap.core.markdown_generator import MarkdownGenerator
|
191
|
+
|
155
192
|
markdown_generator = MarkdownGenerator(
|
156
193
|
root_dir=self.root_dir,
|
157
194
|
output_file=self.output_file,
|
158
|
-
structure_only=self.structure_only
|
195
|
+
structure_only=self.structure_only,
|
196
|
+
)
|
197
|
+
markdown_generator.generate_markdown(
|
198
|
+
self.file_tree.structure, self.file_tree.get_all_files()
|
159
199
|
)
|
160
|
-
markdown_generator.generate_markdown(self.file_tree.structure, self.file_tree.get_all_files())
|
161
200
|
self.logger.info(f"Markdown generated at {self.output_file}.")
|
162
201
|
|
163
202
|
def generate_output_from_selected(self, selected_files: set) -> None:
|
164
203
|
self.logger.info("Generating Markdown from selected files.")
|
165
204
|
pruned_tree = self.file_tree.prune_tree(selected_files)
|
166
205
|
from reposnap.core.markdown_generator import MarkdownGenerator
|
206
|
+
|
167
207
|
markdown_generator = MarkdownGenerator(
|
168
208
|
root_dir=self.root_dir,
|
169
209
|
output_file=self.output_file,
|
170
210
|
structure_only=False,
|
171
|
-
hide_untoggled=True
|
211
|
+
hide_untoggled=True,
|
212
|
+
)
|
213
|
+
markdown_generator.generate_markdown(
|
214
|
+
pruned_tree, [Path(f) for f in selected_files]
|
172
215
|
)
|
173
|
-
markdown_generator.generate_markdown(pruned_tree, [Path(f) for f in selected_files])
|
174
216
|
self.logger.info(f"Markdown generated at {self.output_file}.")
|
175
217
|
|
176
218
|
def run(self) -> None:
|
@@ -180,19 +222,25 @@ class ProjectController:
|
|
180
222
|
self.generate_output()
|
181
223
|
|
182
224
|
def _load_gitignore_patterns(self) -> List[str]:
|
183
|
-
gitignore_path = self.root_dir /
|
225
|
+
gitignore_path = self.root_dir / ".gitignore"
|
184
226
|
if not gitignore_path.exists():
|
185
227
|
for parent in self.root_dir.parents:
|
186
|
-
candidate = parent /
|
228
|
+
candidate = parent / ".gitignore"
|
187
229
|
if candidate.exists():
|
188
230
|
gitignore_path = candidate
|
189
231
|
break
|
190
232
|
else:
|
191
233
|
gitignore_path = None
|
192
234
|
if gitignore_path and gitignore_path.exists():
|
193
|
-
with gitignore_path.open(
|
194
|
-
patterns = [
|
195
|
-
|
235
|
+
with gitignore_path.open("r") as gitignore:
|
236
|
+
patterns = [
|
237
|
+
line.strip()
|
238
|
+
for line in gitignore
|
239
|
+
if line.strip() and not line.strip().startswith("#")
|
240
|
+
]
|
241
|
+
self.logger.debug(
|
242
|
+
f"Loaded .gitignore patterns from {gitignore_path.parent}: {patterns}"
|
243
|
+
)
|
196
244
|
return patterns
|
197
245
|
else:
|
198
246
|
self.logger.debug(f"No .gitignore found starting from {self.root_dir}.")
|
reposnap/core/git_repo.py
CHANGED
@@ -1,79 +1,91 @@
|
|
1
|
-
# src/reposnap/core/markdown_generator.py
|
2
|
-
|
1
|
+
# src/reposnap/core/markdown_generator.py ★ fully-rewritten file
|
3
2
|
import logging
|
4
3
|
from pathlib import Path
|
4
|
+
from typing import Dict, List, Any
|
5
|
+
|
5
6
|
from reposnap.utils.path_utils import format_tree
|
6
|
-
from typing import List, Dict, Any
|
7
7
|
|
8
8
|
|
9
9
|
class MarkdownGenerator:
|
10
|
-
|
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__)
|
10
|
+
"""Render the collected file-tree into a single Markdown document."""
|
16
11
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
12
|
+
def __init__(
|
13
|
+
self,
|
14
|
+
root_dir: Path,
|
15
|
+
output_file: Path,
|
16
|
+
structure_only: bool = False,
|
17
|
+
hide_untoggled: bool = False,
|
18
|
+
):
|
19
|
+
self.root_dir = root_dir.resolve()
|
20
|
+
self.output_file = output_file.resolve()
|
21
|
+
self.structure_only = structure_only
|
22
|
+
self.hide_untoggled = hide_untoggled
|
23
|
+
self.logger = logging.getLogger(__name__)
|
20
24
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
+
# --------------------------------------------------------------
|
26
|
+
# public API
|
27
|
+
# --------------------------------------------------------------
|
28
|
+
def generate_markdown(
|
29
|
+
self, tree_structure: Dict[str, Any], files: List[Path]
|
30
|
+
) -> None:
|
31
|
+
"""Write header (tree) and, unless *structure_only*, every file body."""
|
25
32
|
self._write_header(tree_structure)
|
26
33
|
if not self.structure_only:
|
27
34
|
self._write_file_contents(files)
|
28
35
|
|
36
|
+
# --------------------------------------------------------------
|
37
|
+
# helpers
|
38
|
+
# --------------------------------------------------------------
|
29
39
|
def _write_header(self, tree_structure: Dict[str, Any]) -> None:
|
30
|
-
"""
|
31
|
-
Writes the header and project structure to the Markdown file.
|
32
|
-
"""
|
40
|
+
"""Emit the *Project Structure* section."""
|
33
41
|
self.logger.debug("Writing Markdown header and project structure.")
|
34
42
|
try:
|
35
|
-
with self.output_file.open(
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
except
|
43
|
-
self.logger.error(
|
43
|
+
with self.output_file.open(mode="w", encoding="utf-8") as fh:
|
44
|
+
fh.write("# Project Structure\n\n```\n")
|
45
|
+
for line in format_tree(
|
46
|
+
tree_structure, hide_untoggled=self.hide_untoggled
|
47
|
+
):
|
48
|
+
fh.write(line)
|
49
|
+
fh.write("```\n\n")
|
50
|
+
except OSError as exc:
|
51
|
+
self.logger.error("Failed to write header: %s", exc)
|
44
52
|
raise
|
45
53
|
|
46
54
|
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
|
-
"""
|
55
|
+
"""Append every file in *files* under its own fenced section."""
|
53
56
|
self.logger.debug("Writing file contents to Markdown.")
|
54
|
-
for
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
self.logger.debug(f"File not found: {file_path}. Skipping.")
|
57
|
+
for rel_path in files:
|
58
|
+
abs_path = self.root_dir / rel_path
|
59
|
+
if not abs_path.exists(): # git had stale entry
|
60
|
+
self.logger.debug("File not found: %s -- skipping.", abs_path)
|
59
61
|
continue
|
60
|
-
|
61
62
|
try:
|
62
|
-
self.
|
63
|
-
except UnicodeDecodeError as
|
64
|
-
self.logger.error(
|
65
|
-
|
63
|
+
self._write_single_file(abs_path, rel_path.as_posix())
|
64
|
+
except UnicodeDecodeError as exc:
|
65
|
+
self.logger.error("Unicode error for %s: %s", abs_path, exc)
|
66
66
|
|
67
|
-
|
67
|
+
# --------------------------------------------------------------
|
68
|
+
# single-file writer
|
69
|
+
# --------------------------------------------------------------
|
70
|
+
def _write_single_file(self, file_path: Path, rel_str: str) -> None:
|
68
71
|
"""
|
69
|
-
|
72
|
+
Append one file.
|
73
|
+
|
74
|
+
We guarantee **one and only one** newline between the last character
|
75
|
+
of *content* and the closing code-fence so the output is stable and
|
76
|
+
deterministic (important for tests and downstream diff-tools).
|
70
77
|
"""
|
71
78
|
try:
|
72
|
-
with file_path.open(
|
73
|
-
content
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
79
|
+
with file_path.open(encoding="utf-8") as src:
|
80
|
+
content = src.read()
|
81
|
+
|
82
|
+
with self.output_file.open(mode="a", encoding="utf-8") as dst:
|
83
|
+
dst.write(f"## {rel_str}\n\n")
|
84
|
+
dst.write("```python\n" if file_path.suffix == ".py" else "```\n")
|
85
|
+
|
86
|
+
# normalise trailing EOL → exactly one '\n'
|
87
|
+
dst.write(content if content.endswith("\n") else f"{content}\n")
|
88
|
+
|
89
|
+
dst.write("```\n\n")
|
90
|
+
except OSError as exc:
|
91
|
+
self.logger.error("Error processing %s: %s", file_path, exc)
|
reposnap/interfaces/cli.py
CHANGED
@@ -7,27 +7,46 @@ from reposnap.controllers.project_controller import ProjectController
|
|
7
7
|
|
8
8
|
def main():
|
9
9
|
parser = argparse.ArgumentParser(
|
10
|
-
description=
|
10
|
+
description="Generate a Markdown representation of a Git repository."
|
11
11
|
)
|
12
12
|
# Changed positional argument to allow one or more paths.
|
13
13
|
parser.add_argument(
|
14
|
-
|
15
|
-
nargs=
|
16
|
-
help=
|
17
|
-
)
|
18
|
-
parser.add_argument(
|
19
|
-
|
20
|
-
|
21
|
-
parser.add_argument(
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
14
|
+
"paths",
|
15
|
+
nargs="+",
|
16
|
+
help="One or more paths (files or directories) to include in the Markdown output.",
|
17
|
+
)
|
18
|
+
parser.add_argument(
|
19
|
+
"-o", "--output", help="Output Markdown file", default="output.md"
|
20
|
+
)
|
21
|
+
parser.add_argument(
|
22
|
+
"--structure-only",
|
23
|
+
action="store_true",
|
24
|
+
help="Only include the file structure without content.",
|
25
|
+
)
|
26
|
+
parser.add_argument(
|
27
|
+
"--debug", action="store_true", help="Enable debug-level logging."
|
28
|
+
)
|
29
|
+
parser.add_argument(
|
30
|
+
"-i",
|
31
|
+
"--include",
|
32
|
+
nargs="*",
|
33
|
+
default=[],
|
34
|
+
help="File/folder patterns to include.",
|
35
|
+
)
|
36
|
+
parser.add_argument(
|
37
|
+
"-e",
|
38
|
+
"--exclude",
|
39
|
+
nargs="*",
|
40
|
+
default=[],
|
41
|
+
help="File/folder patterns to exclude.",
|
42
|
+
)
|
26
43
|
|
27
44
|
args = parser.parse_args()
|
28
45
|
|
29
46
|
log_level = logging.DEBUG if args.debug else logging.INFO
|
30
|
-
logging.basicConfig(
|
47
|
+
logging.basicConfig(
|
48
|
+
level=log_level, format="%(asctime)s - %(levelname)s - %(message)s"
|
49
|
+
)
|
31
50
|
|
32
51
|
controller = ProjectController(args)
|
33
52
|
controller.run()
|
reposnap/interfaces/gui.py
CHANGED
@@ -14,7 +14,7 @@ class MyCheckBox(urwid.CheckBox):
|
|
14
14
|
class RepoSnapGUI:
|
15
15
|
def __init__(self):
|
16
16
|
self.controller = ProjectController()
|
17
|
-
self.root_dir = Path(
|
17
|
+
self.root_dir = Path(".").resolve()
|
18
18
|
self.file_tree = None
|
19
19
|
self.selected_files = set()
|
20
20
|
|
@@ -22,23 +22,22 @@ class RepoSnapGUI:
|
|
22
22
|
self.build_main_menu()
|
23
23
|
|
24
24
|
def build_main_menu(self):
|
25
|
-
self.root_dir_edit = urwid.Edit(
|
25
|
+
self.root_dir_edit = urwid.Edit(
|
26
|
+
("bold", "Root Directory: "), str(self.root_dir)
|
27
|
+
)
|
26
28
|
scan_button = urwid.Button("Scan", on_press=self.on_scan)
|
27
29
|
|
28
30
|
main_menu = urwid.Frame(
|
29
|
-
header=urwid.Text((
|
31
|
+
header=urwid.Text(("bold", "RepoSnap - Main Menu")),
|
30
32
|
body=urwid.Padding(
|
31
33
|
urwid.LineBox(
|
32
|
-
urwid.ListBox(
|
33
|
-
|
34
|
-
self.root_dir_edit
|
35
|
-
])
|
36
|
-
),
|
37
|
-
title="Enter Root Directory"
|
34
|
+
urwid.ListBox(urwid.SimpleFocusListWalker([self.root_dir_edit])),
|
35
|
+
title="Enter Root Directory",
|
38
36
|
),
|
39
|
-
left=2,
|
37
|
+
left=2,
|
38
|
+
right=2,
|
40
39
|
),
|
41
|
-
footer=urwid.Padding(scan_button, align=
|
40
|
+
footer=urwid.Padding(scan_button, align="center"),
|
42
41
|
)
|
43
42
|
|
44
43
|
self.main_widget = main_menu
|
@@ -56,9 +55,9 @@ class RepoSnapGUI:
|
|
56
55
|
render_button = urwid.Button("Render", on_press=self.on_render)
|
57
56
|
|
58
57
|
tree_menu = urwid.Frame(
|
59
|
-
header=urwid.Text((
|
58
|
+
header=urwid.Text(("bold", f"File Tree of {self.root_dir}")),
|
60
59
|
body=urwid.LineBox(tree_listbox),
|
61
|
-
footer=urwid.Padding(render_button, align=
|
60
|
+
footer=urwid.Padding(render_button, align="center"),
|
62
61
|
)
|
63
62
|
|
64
63
|
self.main_widget = tree_menu
|
@@ -67,23 +66,25 @@ class RepoSnapGUI:
|
|
67
66
|
def build_tree_widget(self, tree_structure, parent_path="", level=0):
|
68
67
|
widgets = []
|
69
68
|
for key, value in sorted(tree_structure.items()):
|
70
|
-
node_path = f"{parent_path}/{key}".lstrip(
|
69
|
+
node_path = f"{parent_path}/{key}".lstrip("/")
|
71
70
|
checkbox = MyCheckBox(
|
72
71
|
key,
|
73
|
-
user_data={
|
72
|
+
user_data={"path": node_path, "level": level},
|
74
73
|
state=False,
|
75
|
-
on_state_change=self.on_checkbox_change
|
74
|
+
on_state_change=self.on_checkbox_change,
|
76
75
|
)
|
77
|
-
indented_checkbox = urwid.Padding(checkbox, left=4*level)
|
76
|
+
indented_checkbox = urwid.Padding(checkbox, left=4 * level)
|
78
77
|
widgets.append(indented_checkbox)
|
79
78
|
if isinstance(value, dict):
|
80
|
-
widgets.extend(
|
79
|
+
widgets.extend(
|
80
|
+
self.build_tree_widget(value, node_path, level=level + 1)
|
81
|
+
)
|
81
82
|
return widgets
|
82
83
|
|
83
84
|
def on_checkbox_change(self, checkbox, state):
|
84
85
|
user_data = checkbox.user_data
|
85
|
-
node_path = user_data[
|
86
|
-
level = user_data[
|
86
|
+
node_path = user_data["path"]
|
87
|
+
level = user_data["level"]
|
87
88
|
if state:
|
88
89
|
self.selected_files.add(node_path)
|
89
90
|
else:
|
@@ -108,10 +109,10 @@ class RepoSnapGUI:
|
|
108
109
|
if isinstance(widget, urwid.Padding):
|
109
110
|
checkbox_widget = widget.original_widget
|
110
111
|
widget_user_data = checkbox_widget.user_data
|
111
|
-
widget_level = widget_user_data[
|
112
|
+
widget_level = widget_user_data["level"]
|
112
113
|
if widget_level > level:
|
113
114
|
checkbox_widget.set_state(state, do_callback=False)
|
114
|
-
node_path = widget_user_data[
|
115
|
+
node_path = widget_user_data["path"]
|
115
116
|
if state:
|
116
117
|
self.selected_files.add(node_path)
|
117
118
|
else:
|
@@ -124,12 +125,14 @@ class RepoSnapGUI:
|
|
124
125
|
|
125
126
|
def on_render(self, button):
|
126
127
|
self.controller.generate_output_from_selected(self.selected_files)
|
127
|
-
message = urwid.Text(
|
128
|
+
message = urwid.Text(
|
129
|
+
("bold", f"Markdown generated at {self.controller.output_file}")
|
130
|
+
)
|
128
131
|
exit_button = urwid.Button("Exit", on_press=self.exit_program)
|
129
132
|
result_menu = urwid.Frame(
|
130
|
-
header=urwid.Text((
|
131
|
-
body=urwid.Filler(message, valign=
|
132
|
-
footer=urwid.Padding(exit_button, align=
|
133
|
+
header=urwid.Text(("bold", "Success")),
|
134
|
+
body=urwid.Filler(message, valign="middle"),
|
135
|
+
footer=urwid.Padding(exit_button, align="center"),
|
133
136
|
)
|
134
137
|
self.main_widget = result_menu
|
135
138
|
self.refresh()
|
reposnap/models/file_tree.py
CHANGED
@@ -19,10 +19,12 @@ class FileTree:
|
|
19
19
|
"""
|
20
20
|
return self._extract_files(self.structure)
|
21
21
|
|
22
|
-
def _extract_files(
|
22
|
+
def _extract_files(
|
23
|
+
self, subtree: Dict[str, Any], path_prefix: str = ""
|
24
|
+
) -> List[Path]:
|
23
25
|
files: List[Path] = []
|
24
26
|
for key, value in subtree.items():
|
25
|
-
current_path: str = f"{path_prefix}/{key}".lstrip(
|
27
|
+
current_path: str = f"{path_prefix}/{key}".lstrip("/")
|
26
28
|
if isinstance(value, dict):
|
27
29
|
files.extend(self._extract_files(value, current_path))
|
28
30
|
else:
|
@@ -39,17 +41,23 @@ class FileTree:
|
|
39
41
|
self.logger.debug("Filtering files in the file tree.")
|
40
42
|
self.structure = self._filter_tree(self.structure, spec)
|
41
43
|
|
42
|
-
def _filter_tree(
|
44
|
+
def _filter_tree(
|
45
|
+
self, subtree: Dict[str, Any], spec: pathspec.PathSpec, path_prefix: str = ""
|
46
|
+
) -> Dict[str, Any]:
|
43
47
|
filtered_subtree: Dict[str, Any] = {}
|
44
48
|
for key, value in subtree.items():
|
45
|
-
current_path: str = f"{path_prefix}/{key}".lstrip(
|
49
|
+
current_path: str = f"{path_prefix}/{key}".lstrip("/")
|
46
50
|
if isinstance(value, dict):
|
47
|
-
filtered_value: Dict[str, Any] = self._filter_tree(
|
51
|
+
filtered_value: Dict[str, Any] = self._filter_tree(
|
52
|
+
value, spec, current_path
|
53
|
+
)
|
48
54
|
if filtered_value:
|
49
55
|
filtered_subtree[key] = filtered_value
|
50
56
|
else:
|
51
57
|
# 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(
|
58
|
+
if not spec.match_file(current_path) and not spec.match_file(
|
59
|
+
Path(current_path).name
|
60
|
+
):
|
53
61
|
filtered_subtree[key] = value
|
54
62
|
return filtered_subtree
|
55
63
|
|
@@ -65,12 +73,16 @@ class FileTree:
|
|
65
73
|
"""
|
66
74
|
return self._prune_tree(self.structure, selected_files)
|
67
75
|
|
68
|
-
def _prune_tree(
|
76
|
+
def _prune_tree(
|
77
|
+
self, subtree: Dict[str, Any], selected_files: set, path_prefix: str = ""
|
78
|
+
) -> Dict[str, Any]:
|
69
79
|
pruned_subtree: Dict[str, Any] = {}
|
70
80
|
for key, value in subtree.items():
|
71
|
-
current_path: str = f"{path_prefix}/{key}".lstrip(
|
81
|
+
current_path: str = f"{path_prefix}/{key}".lstrip("/")
|
72
82
|
if isinstance(value, dict):
|
73
|
-
pruned_value: Dict[str, Any] = self._prune_tree(
|
83
|
+
pruned_value: Dict[str, Any] = self._prune_tree(
|
84
|
+
value, selected_files, current_path
|
85
|
+
)
|
74
86
|
if pruned_value:
|
75
87
|
pruned_subtree[key] = pruned_value
|
76
88
|
else:
|
reposnap/utils/path_utils.py
CHANGED
@@ -2,12 +2,14 @@
|
|
2
2
|
from typing import Dict, Generator, Any
|
3
3
|
|
4
4
|
|
5
|
-
def format_tree(
|
5
|
+
def format_tree(
|
6
|
+
tree: Dict[str, Any], indent: str = "", hide_untoggled: bool = False
|
7
|
+
) -> Generator[str, None, None]:
|
6
8
|
for key, value in tree.items():
|
7
|
-
if value ==
|
9
|
+
if value == "<hidden>":
|
8
10
|
yield f"{indent}<...>\n"
|
9
11
|
elif isinstance(value, dict):
|
10
12
|
yield f"{indent}{key}/\n"
|
11
|
-
yield from format_tree(value, indent +
|
13
|
+
yield from format_tree(value, indent + " ", hide_untoggled)
|
12
14
|
else:
|
13
15
|
yield f"{indent}{key}\n"
|
@@ -0,0 +1,19 @@
|
|
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=XranKRoWd1cRZ4cEXmN_YddO-GHREz9t2J0TUJ4mtgs,10053
|
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=I0AhB6XbABJ-oVGOSkVhSjFjSFfcm6f1VFHGTeuM4gE,1255
|
7
|
+
reposnap/core/markdown_generator.py,sha256=V6uEbxVSbCbxKN9ysTDKsIDvEGBxFutpOpyaZRXZUGw,3747
|
8
|
+
reposnap/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
reposnap/interfaces/cli.py,sha256=qkbYAlgZzlxgW8ePIUwd8JSbvnBVSvde-VlVqzMDh7g,1470
|
10
|
+
reposnap/interfaces/gui.py,sha256=sTuQxjD1nPa9FpgfzOwi6VDO5QMMtDX-5CiEhbJJcs4,5429
|
11
|
+
reposnap/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
+
reposnap/models/file_tree.py,sha256=jGo_SizdFcOiDC1OOMz-tiijRN3iSD7ENh6Xw8S6OL0,3362
|
13
|
+
reposnap/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
+
reposnap/utils/path_utils.py,sha256=UrMe5cjspTf-4gjg2lzv6BgLwZ7S_1lLECQvDMDZO9Y,507
|
15
|
+
reposnap-0.6.5.dist-info/METADATA,sha256=V85e8nN5tNH_U-W_tludr_nG8CNvhGEDAX34Tz1r8JM,5348
|
16
|
+
reposnap-0.6.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
17
|
+
reposnap-0.6.5.dist-info/entry_points.txt,sha256=o3GyO7bpR0dujPCjsvvZMPv4pXNJlFwD49_pA1r5FOA,102
|
18
|
+
reposnap-0.6.5.dist-info/licenses/LICENSE,sha256=Aj7WCYBXi98pvi723HPn4GDRyjxToNWb3PC6j1_lnPk,1069
|
19
|
+
reposnap-0.6.5.dist-info/RECORD,,
|
reposnap-0.6.3.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|