difflicious 0.1.0__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.
- difflicious/__init__.py +6 -0
- difflicious/app.py +505 -0
- difflicious/cli.py +77 -0
- difflicious/diff_parser.py +525 -0
- difflicious/dummy_data.json +44 -0
- difflicious/git_operations.py +1005 -0
- difflicious/services/__init__.py +1 -0
- difflicious/services/base_service.py +32 -0
- difflicious/services/diff_service.py +403 -0
- difflicious/services/exceptions.py +19 -0
- difflicious/services/git_service.py +135 -0
- difflicious/services/syntax_service.py +162 -0
- difflicious/services/template_service.py +382 -0
- difflicious/static/css/styles.css +885 -0
- difflicious/static/css/tailwind.css +1 -0
- difflicious/static/css/tailwind.input.css +5 -0
- difflicious/static/js/app.js +1002 -0
- difflicious/static/js/diff-interactions.js +1617 -0
- difflicious/templates/base.html +54 -0
- difflicious/templates/diff_file.html +90 -0
- difflicious/templates/diff_groups.html +29 -0
- difflicious/templates/diff_hunk.html +170 -0
- difflicious/templates/index.html +54 -0
- difflicious/templates/partials/empty_state.html +29 -0
- difflicious/templates/partials/global_controls.html +23 -0
- difflicious/templates/partials/loading_state.html +7 -0
- difflicious/templates/partials/toolbar.html +165 -0
- difflicious-0.1.0.dist-info/METADATA +190 -0
- difflicious-0.1.0.dist-info/RECORD +32 -0
- difflicious-0.1.0.dist-info/WHEEL +4 -0
- difflicious-0.1.0.dist-info/entry_points.txt +2 -0
- difflicious-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""Git diff parser for converting unified diff format to side-by-side structure."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from unidiff import Hunk, PatchedFile, PatchSet
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_file_line_count(file_path: str) -> Optional[int]:
|
|
14
|
+
"""Get the number of lines in a file using wc -l.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
file_path: Path to the file to count lines in
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Number of lines in the file, or None if file doesn't exist or count fails
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
if not file_path or not os.path.isfile(file_path):
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
result = subprocess.run(
|
|
27
|
+
["wc", "-l", file_path], capture_output=True, text=True, timeout=5
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if result.returncode == 0:
|
|
31
|
+
# wc -l output format: " 123 filename"
|
|
32
|
+
line_count = int(result.stdout.strip().split()[0])
|
|
33
|
+
return line_count
|
|
34
|
+
else:
|
|
35
|
+
logger.warning(f"wc command failed for {file_path}: {result.stderr}")
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
except (subprocess.TimeoutExpired, ValueError, IndexError, OSError) as e:
|
|
39
|
+
logger.warning(f"Failed to count lines in {file_path}: {e}")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DiffParseError(Exception):
|
|
44
|
+
"""Exception raised when diff parsing fails."""
|
|
45
|
+
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_git_diff(diff_text: str) -> list[dict[str, Any]]:
|
|
50
|
+
"""Parse git diff output into structured side-by-side format.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
diff_text: Raw git diff output in unified format
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of file dictionaries with structured diff data
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
DiffParseError: If parsing fails
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
if not diff_text.strip():
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
# Parse using unidiff library
|
|
66
|
+
patch_set = PatchSet(diff_text)
|
|
67
|
+
|
|
68
|
+
files = []
|
|
69
|
+
for patched_file in patch_set:
|
|
70
|
+
file_data = _parse_file(patched_file)
|
|
71
|
+
files.append(file_data)
|
|
72
|
+
|
|
73
|
+
return files
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Failed to parse diff: {e}")
|
|
77
|
+
raise DiffParseError(f"Diff parsing failed: {e}") from e
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _parse_file(patched_file: PatchedFile) -> dict[str, Any]:
|
|
81
|
+
"""Parse a single file's diff data.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
patched_file: PatchedFile object from unidiff
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Dictionary containing file metadata and hunks
|
|
88
|
+
"""
|
|
89
|
+
# Determine file status
|
|
90
|
+
if patched_file.is_added_file:
|
|
91
|
+
status = "added"
|
|
92
|
+
elif patched_file.is_removed_file:
|
|
93
|
+
status = "deleted"
|
|
94
|
+
elif patched_file.is_modified_file:
|
|
95
|
+
status = "modified"
|
|
96
|
+
elif patched_file.source_file != patched_file.target_file:
|
|
97
|
+
status = "renamed"
|
|
98
|
+
else:
|
|
99
|
+
status = "modified"
|
|
100
|
+
|
|
101
|
+
# Calculate total additions and deletions
|
|
102
|
+
total_additions = 0
|
|
103
|
+
total_deletions = 0
|
|
104
|
+
for hunk in patched_file:
|
|
105
|
+
total_additions += hunk.added
|
|
106
|
+
total_deletions += hunk.removed
|
|
107
|
+
|
|
108
|
+
# Clean up file paths by removing a/ and b/ prefixes
|
|
109
|
+
target_path = patched_file.target_file or patched_file.source_file
|
|
110
|
+
source_path = patched_file.source_file
|
|
111
|
+
|
|
112
|
+
# Remove a/ and b/ prefixes commonly found in git diffs
|
|
113
|
+
if target_path and target_path.startswith(("a/", "b/")):
|
|
114
|
+
target_path = target_path[2:]
|
|
115
|
+
if source_path and source_path.startswith(("a/", "b/")):
|
|
116
|
+
source_path = source_path[2:]
|
|
117
|
+
|
|
118
|
+
# Get line count for the current file (target_path)
|
|
119
|
+
line_count = _get_file_line_count(target_path) if target_path else None
|
|
120
|
+
|
|
121
|
+
file_data: dict[str, Any] = {
|
|
122
|
+
"path": target_path,
|
|
123
|
+
"old_path": source_path,
|
|
124
|
+
"status": status,
|
|
125
|
+
"additions": total_additions,
|
|
126
|
+
"deletions": total_deletions,
|
|
127
|
+
"changes": total_additions + total_deletions,
|
|
128
|
+
"line_count": line_count,
|
|
129
|
+
"hunks": [],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Parse each hunk
|
|
133
|
+
for hunk in patched_file:
|
|
134
|
+
hunk_data = _parse_hunk(hunk)
|
|
135
|
+
file_data["hunks"].append(hunk_data)
|
|
136
|
+
|
|
137
|
+
return file_data
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_hunk(hunk: Hunk) -> dict[str, Any]:
|
|
141
|
+
"""Parse a single hunk into side-by-side structure.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
hunk: Hunk object from unidiff
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Dictionary containing hunk metadata and side-by-side lines
|
|
148
|
+
"""
|
|
149
|
+
hunk_data: dict[str, Any] = {
|
|
150
|
+
"old_start": hunk.source_start,
|
|
151
|
+
"old_count": hunk.source_length,
|
|
152
|
+
"new_start": hunk.target_start,
|
|
153
|
+
"new_count": hunk.target_length,
|
|
154
|
+
"section_header": hunk.section_header or "",
|
|
155
|
+
"lines": [],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Convert linear hunk into side-by-side structure
|
|
159
|
+
old_line_num = hunk.source_start
|
|
160
|
+
new_line_num = hunk.target_start
|
|
161
|
+
|
|
162
|
+
hunk_lines = list(hunk) # Convert to list for lookahead
|
|
163
|
+
i = 0
|
|
164
|
+
while i < len(hunk_lines):
|
|
165
|
+
line = hunk_lines[i]
|
|
166
|
+
|
|
167
|
+
# Check if next line is a "no newline" marker
|
|
168
|
+
next_line_is_no_newline = (
|
|
169
|
+
i + 1 < len(hunk_lines) and hunk_lines[i + 1].line_type == "\\"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Parse the current line (skip "no newline" markers)
|
|
173
|
+
if line.line_type != "\\":
|
|
174
|
+
line_data = _parse_line(
|
|
175
|
+
line, old_line_num, new_line_num, next_line_is_no_newline
|
|
176
|
+
)
|
|
177
|
+
hunk_data["lines"].append(line_data)
|
|
178
|
+
|
|
179
|
+
# Update line numbers based on line type
|
|
180
|
+
if line.line_type == " ": # Context line
|
|
181
|
+
old_line_num += 1
|
|
182
|
+
new_line_num += 1
|
|
183
|
+
elif line.line_type == "-": # Deletion
|
|
184
|
+
old_line_num += 1
|
|
185
|
+
elif line.line_type == "+": # Addition
|
|
186
|
+
new_line_num += 1
|
|
187
|
+
|
|
188
|
+
i += 1
|
|
189
|
+
|
|
190
|
+
return hunk_data
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _parse_line(
|
|
194
|
+
line: Any, old_line_num: int, new_line_num: int, missing_newline: bool = False
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""Parse a single diff line.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
line: Line object from unidiff
|
|
200
|
+
old_line_num: Current old file line number
|
|
201
|
+
new_line_num: Current new file line number
|
|
202
|
+
missing_newline: Whether this line is missing a newline at end
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Dictionary containing line data for side-by-side view
|
|
206
|
+
"""
|
|
207
|
+
# Determine line type
|
|
208
|
+
if line.line_type == " ":
|
|
209
|
+
line_type = "context"
|
|
210
|
+
old_num = old_line_num
|
|
211
|
+
new_num = new_line_num
|
|
212
|
+
elif line.line_type == "-":
|
|
213
|
+
line_type = "deletion"
|
|
214
|
+
old_num = old_line_num
|
|
215
|
+
new_num = None
|
|
216
|
+
elif line.line_type == "+":
|
|
217
|
+
line_type = "addition"
|
|
218
|
+
old_num = None
|
|
219
|
+
new_num = new_line_num
|
|
220
|
+
else:
|
|
221
|
+
line_type = "context"
|
|
222
|
+
old_num = old_line_num
|
|
223
|
+
new_num = new_line_num
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"type": line_type,
|
|
227
|
+
"old_line_num": old_num,
|
|
228
|
+
"new_line_num": new_num,
|
|
229
|
+
# Preserve leading whitespace; remove only a single trailing newline/carriage return
|
|
230
|
+
"content": line.value.rstrip("\r\n"),
|
|
231
|
+
"missing_newline": missing_newline,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def create_side_by_side_lines(hunks: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
236
|
+
"""Convert hunks into side-by-side line pairs for rendering.
|
|
237
|
+
|
|
238
|
+
This function takes the parsed hunks and creates a structure optimized
|
|
239
|
+
for side-by-side rendering, handling cases where additions and deletions
|
|
240
|
+
don't match up 1:1.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
hunks: List of parsed hunk dictionaries
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of line pair dictionaries for side-by-side rendering
|
|
247
|
+
"""
|
|
248
|
+
side_by_side_lines = []
|
|
249
|
+
|
|
250
|
+
for hunk in hunks:
|
|
251
|
+
# Always add hunk header to ensure proper separation
|
|
252
|
+
side_by_side_lines.append(
|
|
253
|
+
{
|
|
254
|
+
"type": "hunk_header",
|
|
255
|
+
"content": hunk.get("section_header", ""),
|
|
256
|
+
"old_start": hunk["old_start"],
|
|
257
|
+
"new_start": hunk["new_start"],
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Group consecutive additions and deletions for better alignment
|
|
262
|
+
i = 0
|
|
263
|
+
while i < len(hunk["lines"]):
|
|
264
|
+
line = hunk["lines"][i]
|
|
265
|
+
|
|
266
|
+
if line["type"] == "context":
|
|
267
|
+
# Context lines appear on both sides
|
|
268
|
+
side_by_side_lines.append(
|
|
269
|
+
{
|
|
270
|
+
"type": "context",
|
|
271
|
+
"left": {
|
|
272
|
+
"line_num": line["old_line_num"],
|
|
273
|
+
"content": line["content"],
|
|
274
|
+
"missing_newline": line.get("missing_newline", False),
|
|
275
|
+
},
|
|
276
|
+
"right": {
|
|
277
|
+
"line_num": line["new_line_num"],
|
|
278
|
+
"content": line["content"],
|
|
279
|
+
"missing_newline": line.get("missing_newline", False),
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
i += 1
|
|
284
|
+
|
|
285
|
+
elif line["type"] == "deletion":
|
|
286
|
+
# Look ahead for corresponding additions
|
|
287
|
+
deletions = []
|
|
288
|
+
additions = []
|
|
289
|
+
|
|
290
|
+
# Collect consecutive deletions
|
|
291
|
+
while i < len(hunk["lines"]) and hunk["lines"][i]["type"] == "deletion":
|
|
292
|
+
deletions.append(hunk["lines"][i])
|
|
293
|
+
i += 1
|
|
294
|
+
|
|
295
|
+
# Collect consecutive additions
|
|
296
|
+
while i < len(hunk["lines"]) and hunk["lines"][i]["type"] == "addition":
|
|
297
|
+
additions.append(hunk["lines"][i])
|
|
298
|
+
i += 1
|
|
299
|
+
|
|
300
|
+
# Create side-by-side pairs
|
|
301
|
+
max_lines = max(len(deletions), len(additions))
|
|
302
|
+
for j in range(max_lines):
|
|
303
|
+
left_line = deletions[j] if j < len(deletions) else None
|
|
304
|
+
right_line = additions[j] if j < len(additions) else None
|
|
305
|
+
|
|
306
|
+
side_by_side_lines.append(
|
|
307
|
+
{
|
|
308
|
+
"type": "change",
|
|
309
|
+
"left": {
|
|
310
|
+
"line_num": (
|
|
311
|
+
left_line["old_line_num"] if left_line else None
|
|
312
|
+
),
|
|
313
|
+
"content": left_line["content"] if left_line else "",
|
|
314
|
+
"type": "deletion" if left_line else "empty",
|
|
315
|
+
"missing_newline": (
|
|
316
|
+
left_line.get("missing_newline", False)
|
|
317
|
+
if left_line
|
|
318
|
+
else False
|
|
319
|
+
),
|
|
320
|
+
},
|
|
321
|
+
"right": {
|
|
322
|
+
"line_num": (
|
|
323
|
+
right_line["new_line_num"] if right_line else None
|
|
324
|
+
),
|
|
325
|
+
"content": right_line["content"] if right_line else "",
|
|
326
|
+
"type": "addition" if right_line else "empty",
|
|
327
|
+
"missing_newline": (
|
|
328
|
+
right_line.get("missing_newline", False)
|
|
329
|
+
if right_line
|
|
330
|
+
else False
|
|
331
|
+
),
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
elif line["type"] == "addition":
|
|
337
|
+
# Handle standalone addition (no preceding deletion)
|
|
338
|
+
side_by_side_lines.append(
|
|
339
|
+
{
|
|
340
|
+
"type": "change",
|
|
341
|
+
"left": {
|
|
342
|
+
"line_num": None,
|
|
343
|
+
"content": "",
|
|
344
|
+
"type": "empty",
|
|
345
|
+
"missing_newline": False,
|
|
346
|
+
},
|
|
347
|
+
"right": {
|
|
348
|
+
"line_num": line["new_line_num"],
|
|
349
|
+
"content": line["content"],
|
|
350
|
+
"type": "addition",
|
|
351
|
+
"missing_newline": line.get("missing_newline", False),
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
i += 1
|
|
356
|
+
else:
|
|
357
|
+
i += 1
|
|
358
|
+
|
|
359
|
+
return side_by_side_lines
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def parse_git_diff_for_rendering(diff_text: str) -> list[dict[str, Any]]:
|
|
363
|
+
"""Parse git diff output into side-by-side structure optimized for rendering.
|
|
364
|
+
|
|
365
|
+
This is the main method to use for converting git diff output into the final
|
|
366
|
+
structure needed for frontend rendering. Each file contains hunks, and each
|
|
367
|
+
hunk contains side-by-side line pairs ready for display.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
diff_text: Raw git diff output in unified format
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
List of files with side-by-side structure:
|
|
374
|
+
[
|
|
375
|
+
{
|
|
376
|
+
"path": "file.js",
|
|
377
|
+
"old_path": "file.js",
|
|
378
|
+
"status": "modified",
|
|
379
|
+
"additions": 5,
|
|
380
|
+
"deletions": 2,
|
|
381
|
+
"changes": 7,
|
|
382
|
+
"line_count": 150,
|
|
383
|
+
"hunks": [
|
|
384
|
+
{
|
|
385
|
+
"old_start": 10, "old_count": 5,
|
|
386
|
+
"new_start": 10, "new_count": 6,
|
|
387
|
+
"section_header": "function example()",
|
|
388
|
+
"lines": [
|
|
389
|
+
{
|
|
390
|
+
"type": "context|change|hunk_header",
|
|
391
|
+
"left": {"line_num": 10, "content": "...", "type": "context"},
|
|
392
|
+
"right": {"line_num": 10, "content": "...", "type": "context"}
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
DiffParseError: If parsing fails
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
# First parse into intermediate structure
|
|
405
|
+
parsed_files = parse_git_diff(diff_text)
|
|
406
|
+
|
|
407
|
+
# Convert each file to side-by-side structure
|
|
408
|
+
rendered_files = []
|
|
409
|
+
for file_data in parsed_files:
|
|
410
|
+
# Convert hunks to side-by-side lines
|
|
411
|
+
side_by_side_lines = create_side_by_side_lines(file_data["hunks"])
|
|
412
|
+
|
|
413
|
+
# Group side-by-side lines back into hunks for rendering
|
|
414
|
+
rendered_hunks = _group_lines_into_hunks(
|
|
415
|
+
side_by_side_lines, file_data["hunks"]
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
rendered_file = {
|
|
419
|
+
"path": file_data["path"],
|
|
420
|
+
"old_path": file_data["old_path"],
|
|
421
|
+
"status": file_data["status"],
|
|
422
|
+
"additions": file_data["additions"],
|
|
423
|
+
"deletions": file_data["deletions"],
|
|
424
|
+
"changes": file_data["changes"],
|
|
425
|
+
"line_count": file_data["line_count"],
|
|
426
|
+
"hunks": rendered_hunks,
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
rendered_files.append(rendered_file)
|
|
430
|
+
|
|
431
|
+
return rendered_files
|
|
432
|
+
|
|
433
|
+
except Exception as e:
|
|
434
|
+
logger.error(f"Failed to parse diff for rendering: {e}")
|
|
435
|
+
raise DiffParseError(f"Diff parsing for rendering failed: {e}") from e
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _group_lines_into_hunks(
|
|
439
|
+
side_by_side_lines: list[dict[str, Any]], original_hunks: list[dict[str, Any]]
|
|
440
|
+
) -> list[dict[str, Any]]:
|
|
441
|
+
"""Group side-by-side lines back into hunks for structured rendering.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
side_by_side_lines: List of side-by-side line pairs
|
|
445
|
+
original_hunks: Original hunk metadata for reference
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
List of hunks with side-by-side lines grouped appropriately
|
|
449
|
+
"""
|
|
450
|
+
if not side_by_side_lines:
|
|
451
|
+
return []
|
|
452
|
+
|
|
453
|
+
rendered_hunks = []
|
|
454
|
+
current_hunk = None
|
|
455
|
+
hunk_index = 0
|
|
456
|
+
|
|
457
|
+
for line_pair in side_by_side_lines:
|
|
458
|
+
# Start a new hunk when we encounter a hunk header
|
|
459
|
+
if line_pair["type"] == "hunk_header":
|
|
460
|
+
if current_hunk is not None:
|
|
461
|
+
rendered_hunks.append(current_hunk)
|
|
462
|
+
|
|
463
|
+
# Get original hunk metadata if available
|
|
464
|
+
original_hunk = (
|
|
465
|
+
original_hunks[hunk_index] if hunk_index < len(original_hunks) else {}
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
current_hunk = {
|
|
469
|
+
"old_start": line_pair["old_start"],
|
|
470
|
+
"old_count": original_hunk.get("old_count", 0),
|
|
471
|
+
"new_start": line_pair["new_start"],
|
|
472
|
+
"new_count": original_hunk.get("new_count", 0),
|
|
473
|
+
"section_header": line_pair["content"],
|
|
474
|
+
"lines": [],
|
|
475
|
+
}
|
|
476
|
+
hunk_index += 1
|
|
477
|
+
|
|
478
|
+
else:
|
|
479
|
+
# If we haven't started a hunk yet, create a default one
|
|
480
|
+
if current_hunk is None:
|
|
481
|
+
original_hunk = original_hunks[0] if original_hunks else {}
|
|
482
|
+
current_hunk = {
|
|
483
|
+
"old_start": original_hunk.get("old_start", 1),
|
|
484
|
+
"old_count": original_hunk.get("old_count", 0),
|
|
485
|
+
"new_start": original_hunk.get("new_start", 1),
|
|
486
|
+
"new_count": original_hunk.get("new_count", 0),
|
|
487
|
+
"section_header": "",
|
|
488
|
+
"lines": [],
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
# Add the line pair to current hunk
|
|
492
|
+
current_hunk["lines"].append(line_pair)
|
|
493
|
+
|
|
494
|
+
# Don't forget the last hunk
|
|
495
|
+
if current_hunk is not None:
|
|
496
|
+
rendered_hunks.append(current_hunk)
|
|
497
|
+
|
|
498
|
+
return rendered_hunks
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def get_file_summary(files: list[dict[str, Any]]) -> dict[str, Any]:
|
|
502
|
+
"""Generate summary statistics for a set of parsed diff files.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
files: List of parsed file dictionaries
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Dictionary containing summary statistics
|
|
509
|
+
"""
|
|
510
|
+
total_files = len(files)
|
|
511
|
+
total_additions = sum(file_data["additions"] for file_data in files)
|
|
512
|
+
total_deletions = sum(file_data["deletions"] for file_data in files)
|
|
513
|
+
|
|
514
|
+
files_by_status: dict[str, int] = {}
|
|
515
|
+
for file_data in files:
|
|
516
|
+
status = file_data["status"]
|
|
517
|
+
files_by_status[status] = files_by_status.get(status, 0) + 1
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
"total_files": total_files,
|
|
521
|
+
"total_additions": total_additions,
|
|
522
|
+
"total_deletions": total_deletions,
|
|
523
|
+
"total_changes": total_additions + total_deletions,
|
|
524
|
+
"files_by_status": files_by_status,
|
|
525
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": {
|
|
3
|
+
"status": "ok",
|
|
4
|
+
"git_available": true,
|
|
5
|
+
"current_branch": "feature/new-ui",
|
|
6
|
+
"files_changed": 4,
|
|
7
|
+
"repository_path": "/Users/demo/project",
|
|
8
|
+
"is_clean": false
|
|
9
|
+
},
|
|
10
|
+
"diffs": [
|
|
11
|
+
{
|
|
12
|
+
"file": "src/components/Header.jsx",
|
|
13
|
+
"additions": 12,
|
|
14
|
+
"deletions": 3,
|
|
15
|
+
"changes": 15,
|
|
16
|
+
"status": "modified",
|
|
17
|
+
"content": "@@ -1,8 +1,17 @@\n import React from 'react';\n+import { useState } from 'react';\n \n const Header = () => {\n+ const [isMenuOpen, setIsMenuOpen] = useState(false);\n+\n return (\n- <header className=\"header\">\n- <h1>My App</h1>\n+ <header className=\"bg-white shadow-sm border-b\">\n+ <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n+ <div className=\"flex justify-between items-center h-16\">\n+ <h1 className=\"text-xl font-semibold text-gray-900\">My App</h1>\n+ <nav className=\"flex space-x-4\">\n+ <button onClick={() => setIsMenuOpen(!isMenuOpen)}>Menu</button>\n+ </nav>\n+ </div>\n+ </div>\n </header>\n );\n };"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"file": "src/styles/globals.css",
|
|
21
|
+
"additions": 8,
|
|
22
|
+
"deletions": 2,
|
|
23
|
+
"changes": 10,
|
|
24
|
+
"status": "modified",
|
|
25
|
+
"content": "@@ -1,6 +1,12 @@\n body {\n- margin: 0;\n- font-family: Arial, sans-serif;\n+ margin: 0;\n+ padding: 0;\n+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;\n+ line-height: 1.6;\n+ color: #333;\n }\n \n+* {\n+ box-sizing: border-box;\n+}\n+\n .container {\n max-width: 1200px;\n margin: 0 auto;"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"file": "package.json",
|
|
29
|
+
"additions": 4,
|
|
30
|
+
"deletions": 1,
|
|
31
|
+
"changes": 5,
|
|
32
|
+
"status": "modified",
|
|
33
|
+
"content": "@@ -8,7 +8,10 @@\n },\n \"dependencies\": {\n \"react\": \"^18.2.0\",\n- \"react-dom\": \"^18.2.0\"\n+ \"react-dom\": \"^18.2.0\",\n+ \"tailwindcss\": \"^3.3.0\",\n+ \"autoprefixer\": \"^10.4.14\",\n+ \"postcss\": \"^8.4.24\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.2.15\","
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"file": "README.md",
|
|
37
|
+
"additions": 15,
|
|
38
|
+
"deletions": 5,
|
|
39
|
+
"changes": 20,
|
|
40
|
+
"status": "modified",
|
|
41
|
+
"content": "@@ -1,8 +1,18 @@\n-# My Project\n+# My Awesome Project\n \n-A simple web application.\n+A modern web application built with React and Tailwind CSS.\n \n ## Installation\n \n+```bash\nnpm install\n```\n+\n+## Development\n+\n+```bash\nnpm run dev\n```\n+\n ## Usage\n \n-Run the application with `npm start`.\n+Open your browser and navigate to `http://localhost:3000`."
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|