Skip to content

Commit 50d5b0f

Browse files
committed
feat: move contextv1 to its own file
1 parent 3b3d3d8 commit 50d5b0f

File tree

8 files changed

+159
-121
lines changed

8 files changed

+159
-121
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ repos:
8484
rev: v1.36.4
8585
hooks:
8686
- id: djlint-reformat-jinja
87+
exclude: ^src/gitingest/format/
8788

8889
- repo: https://github.com/igorshubovych/markdownlint-cli
8990
rev: v0.45.0
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Generated using https://gitingest.com/{{ context.query.user_name }}/{{ context.query.repo_name }}
1+
# Generated using https://gitingest.com/{{ context.query.user_name }}/{{ context.query.repo_name }}{{ context.query.subpath }}
22

33
Sources used:
4-
{%- for source in context.sources %}
4+
{%- for source in context %}
55
- {{ source.name }}: {{ source.__class__.__name__ }}
66
{% endfor %}
77

88
{%- for source in context.sources %}
99
{{ formatter.format(source, context.query) }}
1010
{%- endfor %}
11-
# End of generated content
11+
# End of https://gitingest.com/{{ context.query.user_name }}/{{ context.query.repo_name }}{{ context.query.subpath }}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
Repository: {{ context.query.user_name }}/{{ context.query.repo_name }}
22
Commit: {{ context.query.commit }}
3-
Files analyzed: {{ context.sources[0].file_count }}
3+
Files analyzed: {{ context.file_count }}

src/gitingest/output_formatter.py

Lines changed: 46 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
import requests.exceptions
1111
import tiktoken
12-
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
12+
from jinja2 import Environment, FileSystemLoader, Template, TemplateNotFound
1313

14-
from gitingest.schemas import FileSystemDirectory, FileSystemFile, FileSystemNode, FileSystemSymlink, Source
15-
from gitingest.schemas.filesystem import SEPARATOR, ContextV1, FileSystemNodeType, GitRepository
14+
from gitingest.schemas import ContextV1, FileSystemNode, Source
15+
from gitingest.schemas.filesystem import SEPARATOR, FileSystemNodeType
1616
from gitingest.utils.compat_func import readlink
1717
from gitingest.utils.logging_config import get_logger
1818

@@ -28,49 +28,6 @@
2828
]
2929

3030

31-
# Backward compatibility
32-
33-
34-
def _create_summary_prefix(query: IngestionQuery, *, single_file: bool = False) -> str:
35-
"""Create a prefix string for summarizing a repository or local directory.
36-
37-
Includes repository name (if provided), commit/branch details, and subpath if relevant.
38-
39-
Parameters
40-
----------
41-
query : IngestionQuery
42-
The parsed query object containing information about the repository and query parameters.
43-
single_file : bool
44-
A flag indicating whether the summary is for a single file (default: ``False``).
45-
46-
Returns
47-
-------
48-
str
49-
A summary prefix string containing repository, commit, branch, and subpath details.
50-
51-
"""
52-
parts = []
53-
54-
if query.user_name:
55-
parts.append(f"Repository: {query.user_name}/{query.repo_name}")
56-
else:
57-
# Local scenario
58-
parts.append(f"Directory: {query.slug}")
59-
60-
if query.tag:
61-
parts.append(f"Tag: {query.tag}")
62-
elif query.branch and query.branch not in ("main", "master"):
63-
parts.append(f"Branch: {query.branch}")
64-
65-
if query.commit:
66-
parts.append(f"Commit: {query.commit}")
67-
68-
if query.subpath != "/" and not single_file:
69-
parts.append(f"Subpath: {query.subpath}")
70-
71-
return "\n".join(parts) + "\n"
72-
73-
7431
def _gather_file_contents(node: FileSystemNode) -> str:
7532
"""Recursively gather contents of all files under the given node.
7633
@@ -181,71 +138,76 @@ def _format_token_count(text: str) -> str | None:
181138

182139
def generate_digest(context: ContextV1) -> str:
183140
"""Generate a digest string from a ContextV1 object.
184-
141+
185142
This is a convenience function that uses the DefaultFormatter to format a ContextV1.
186-
143+
187144
Parameters
188145
----------
189146
context : ContextV1
190147
The ContextV1 object containing sources and query information.
191-
148+
192149
Returns
193150
-------
194151
str
195152
The formatted digest string.
153+
196154
"""
197155
formatter = DefaultFormatter()
198156
return formatter.format(context, context.query)
199157

200158

201159
class DefaultFormatter:
202-
def __init__(self):
160+
"""Default formatter for rendering filesystem nodes using Jinja2 templates."""
161+
162+
def __init__(self) -> None:
203163
self.separator = SEPARATOR
204164
template_dir = Path(__file__).parent / "format" / "DefaultFormatter"
205-
self.env = Environment(loader=FileSystemLoader(template_dir))
206-
207-
def _get_template_for_node(self, node):
165+
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
166+
167+
def _get_template_for_node(self, node: Source) -> Template:
208168
"""Get template based on node class name."""
209169
template_name = f"{node.__class__.__name__}.j2"
210170
return self.env.get_template(template_name)
211171

212172
@singledispatchmethod
213-
def format(self, node: Source, query):
173+
def format(self, node: Source, query: IngestionQuery) -> str:
214174
"""Dynamically format any node type based on available templates."""
215175
try:
216176
template = self._get_template_for_node(node)
217177
# Provide common template variables
218178
context_vars = {
219-
'node': node,
220-
'query': query,
221-
'formatter': self,
222-
'SEPARATOR': SEPARATOR
179+
"node": node,
180+
"query": query,
181+
"formatter": self,
182+
"SEPARATOR": SEPARATOR,
223183
}
224184
# Special handling for ContextV1 objects
225185
if isinstance(node, ContextV1):
226-
context_vars['context'] = node
186+
context_vars["context"] = node
227187
# Use ContextV1 for backward compatibility
228188
template = self.env.get_template("ContextV1.j2")
229-
189+
230190
return template.render(**context_vars)
231191
except TemplateNotFound:
232192
# Fallback: return content if available, otherwise empty string
233193
return f"{getattr(node, 'content', '')}"
234194

235195

236196
class DebugFormatter:
237-
def __init__(self):
197+
"""Debug formatter that shows detailed information about filesystem nodes."""
198+
199+
def __init__(self) -> None:
238200
self.separator = SEPARATOR
239201
template_dir = Path(__file__).parent / "format" / "DebugFormatter"
240-
self.env = Environment(loader=FileSystemLoader(template_dir))
241-
242-
def _get_template_for_node(self, node):
202+
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
203+
204+
def _get_template_for_node(self, node: Source) -> Template:
243205
"""Get template based on node class name."""
244206
template_name = f"{node.__class__.__name__}.j2"
245207
return self.env.get_template(template_name)
246208

247209
@singledispatchmethod
248-
def format(self, node: Source, query):
210+
def format(self, node: Source, query: IngestionQuery) -> str:
249211
"""Dynamically format any node type with debug information."""
250212
try:
251213
# Get the actual class name
@@ -255,11 +217,15 @@ def format(self, node: Source, query):
255217
field_names = []
256218

257219
# Try to get dataclass fields first
220+
def _raise_no_dataclass_fields() -> None:
221+
msg = "No dataclass fields found"
222+
raise AttributeError(msg)
223+
258224
try:
259225
if hasattr(node, "__dataclass_fields__") and hasattr(node.__dataclass_fields__, "keys"):
260226
field_names.extend(node.__dataclass_fields__.keys())
261227
else:
262-
raise AttributeError # Fall through to backup method
228+
_raise_no_dataclass_fields() # Fall through to backup method
263229
except (AttributeError, TypeError):
264230
# Fall back to getting all non-private attributes
265231
field_names = [
@@ -268,20 +234,20 @@ def format(self, node: Source, query):
268234

269235
# Format the debug output
270236
fields_str = ", ".join(field_names)
271-
237+
272238
# Try to get specific template, fallback to Source.j2
273239
try:
274240
template = self._get_template_for_node(node)
275241
except TemplateNotFound:
276242
template = self.env.get_template("Source.j2")
277-
243+
278244
return template.render(
279245
SEPARATOR=SEPARATOR,
280246
class_name=class_name,
281247
fields_str=fields_str,
282248
node=node,
283249
query=query,
284-
formatter=self
250+
formatter=self,
285251
)
286252
except TemplateNotFound:
287253
# Ultimate fallback
@@ -291,34 +257,34 @@ def format(self, node: Source, query):
291257
class SummaryFormatter:
292258
"""Dedicated formatter for generating summaries of filesystem nodes."""
293259

294-
def __init__(self):
260+
def __init__(self) -> None:
295261
template_dir = Path(__file__).parent / "format" / "SummaryFormatter"
296-
self.env = Environment(loader=FileSystemLoader(template_dir))
297-
298-
def _get_template_for_node(self, node):
262+
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
263+
264+
def _get_template_for_node(self, node: Source) -> Template:
299265
"""Get template based on node class name."""
300266
template_name = f"{node.__class__.__name__}.j2"
301267
return self.env.get_template(template_name)
302268

303269
@singledispatchmethod
304-
def summary(self, node: Source, query):
270+
def summary(self, node: Source, query: IngestionQuery) -> str:
305271
"""Dynamically generate summary for any node type based on available templates."""
306272
try:
307273
# Provide common template variables
308274
context_vars = {
309-
'node': node,
310-
'query': query,
311-
'formatter': self
275+
"node": node,
276+
"query": query,
277+
"formatter": self,
312278
}
313-
279+
314280
# Special handling for ContextV1 objects
315281
if isinstance(node, ContextV1):
316-
context_vars['context'] = node
282+
context_vars["context"] = node
317283
# Use ContextV1 for backward compatibility
318284
template = self.env.get_template("ContextV1.j2")
319285
else:
320286
template = self._get_template_for_node(node)
321-
287+
322288
return template.render(**context_vars)
323289
except TemplateNotFound:
324290
# Fallback: return name if available

src/gitingest/schemas/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Module containing the schemas for the Gitingest package."""
22

33
from gitingest.schemas.cloning import CloneConfig
4+
from gitingest.schemas.contextv1 import ContextV1
45
from gitingest.schemas.filesystem import (
5-
ContextV1,
66
FileSystemDirectory,
77
FileSystemFile,
88
FileSystemNode,
@@ -23,4 +23,5 @@
2323
"FileSystemSymlink",
2424
"GitRepository",
2525
"IngestionQuery",
26+
"Source",
2627
]

src/gitingest/schemas/contextv1.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Schema for ContextV1 objects used in formatting."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from typing import TYPE_CHECKING, Iterator
7+
8+
from gitingest.schemas.filesystem import FileSystemDirectory, FileSystemNode, Source
9+
10+
if TYPE_CHECKING:
11+
from gitingest.schemas import IngestionQuery
12+
13+
14+
@dataclass
15+
class ContextV1:
16+
"""The ContextV1 object is an object that contains all information needed to produce a formatted output.
17+
18+
This object contains all information needed to produce a formatted output
19+
similar to the "legacy" output.
20+
21+
Attributes
22+
----------
23+
sources : list[Source]
24+
List of source objects (files, directories, etc.)
25+
query : IngestionQuery
26+
The query context.
27+
28+
"""
29+
30+
sources: list[Source]
31+
query: IngestionQuery
32+
33+
@property
34+
def sources_by_type(self) -> dict[str, list[Source]]:
35+
"""Return sources grouped by their class name."""
36+
result = {}
37+
for source in self.sources:
38+
class_name = source.__class__.__name__
39+
if class_name not in result:
40+
result[class_name] = []
41+
result[class_name].append(source)
42+
return result
43+
44+
def __getitem__(self, key: str) -> list[Source]:
45+
"""Allow dict-like access to sources by type name."""
46+
sources_dict = self.sources_by_type
47+
if key not in sources_dict:
48+
error_msg = f"No sources of type '{key}' found"
49+
raise KeyError(error_msg)
50+
return sources_dict[key]
51+
52+
def __iter__(self) -> Iterator[Source]:
53+
"""Allow iteration over all sources."""
54+
return iter(self.sources)
55+
56+
@property
57+
def file_count(self) -> int:
58+
"""Calculate total file count based on sources."""
59+
# No need to iterate on children, directories are already aware of their
60+
# file count
61+
total = 0
62+
for source in self.sources:
63+
if isinstance(source, FileSystemDirectory):
64+
# For directories, add their file_count
65+
total += source.file_count
66+
elif isinstance(source, FileSystemNode):
67+
# For individual files/nodes, increment by 1
68+
total += 1
69+
return total

0 commit comments

Comments
 (0)