Skip to content

Commit c56253a

Browse files
committed
Enh(viewer): Browse diff by filename
1 parent f13b796 commit c56253a

3 files changed

Lines changed: 122 additions & 10 deletions

File tree

codeclash/tournaments/utils/git_utils.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,60 @@ def extract_modified_code_file_paths_from_diff(diff: str) -> list[str]:
104104
file_paths.append(file_path)
105105

106106
return file_paths
107+
108+
109+
def split_git_diff_by_files(diff: str) -> dict[str, str]:
110+
"""Split a git diff into separate diffs for each file.
111+
112+
Args:
113+
diff: Git diff text containing potentially multiple files
114+
115+
Returns:
116+
Dictionary mapping file paths to their individual diff content
117+
"""
118+
if not diff or not diff.strip():
119+
return {}
120+
121+
lines = diff.splitlines(keepends=True)
122+
files_diffs = {}
123+
current_file = None
124+
current_block = []
125+
126+
# Store any prelude (content before first diff --git line)
127+
prelude = []
128+
found_first_diff = False
129+
130+
for line in lines:
131+
if line.startswith("diff --git "):
132+
# Save previous file's diff if we have one
133+
if current_file and current_block:
134+
files_diffs[current_file] = "".join(prelude + current_block)
135+
current_block = []
136+
137+
# Extract file path from the diff line
138+
# Format: "diff --git a/path/to/file b/path/to/file"
139+
match = re.match(r"diff --git a/(.+) b/(.+)", line)
140+
if match:
141+
current_file = match.group(2) # Use the "b/" path (after changes)
142+
else:
143+
# Fallback parsing
144+
parts = line.strip().split()
145+
if len(parts) >= 4:
146+
current_file = parts[3][2:] if parts[3].startswith("b/") else parts[3]
147+
else:
148+
current_file = "unknown_file"
149+
150+
current_block.append(line)
151+
found_first_diff = True
152+
else:
153+
if found_first_diff and current_file:
154+
current_block.append(line)
155+
else:
156+
# This is prelude content before any diff
157+
prelude.append(line)
158+
159+
# Handle the last file
160+
if current_file and current_block:
161+
files_diffs[current_file] = "".join(prelude + current_block)
162+
163+
return files_diffs

codeclash/viewer/app.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
from flask import Flask, jsonify, redirect, render_template, request, url_for
1414

15+
from codeclash.tournaments.utils.git_utils import filter_git_diff, split_git_diff_by_files
16+
1517
# Global variable to store the directory to search for logs
1618
LOG_BASE_DIR = Path.cwd() / "logs"
1719

@@ -240,6 +242,8 @@ class TrajectoryInfo:
240242
incremental_diff: str | None = None
241243
modified_files: dict[str, str] | None = None
242244
trajectory_file_path: str | None = None
245+
diff_by_files: dict[str, str] | None = None
246+
incremental_diff_by_files: dict[str, str] | None = None
243247

244248

245249
class LogParser:
@@ -333,13 +337,23 @@ def parse_trajectory(self, player_id: int, round_num: int) -> TrajectoryInfo | N
333337
diff = None
334338
incremental_diff = None
335339
modified_files = None
340+
diff_by_files = {}
341+
incremental_diff_by_files = {}
336342

337343
if player_name in self._player_metadata:
338344
player_meta = self._player_metadata[player_name]
339345
diff = player_meta.get("diff", {}).get(str(round_num), "")
340346
incremental_diff = player_meta.get("incremental_diff", {}).get(str(round_num), "")
341347
modified_files = player_meta.get("modified_files", {}).get(str(round_num), {})
342348

349+
# Filter and split diffs by files
350+
filtered_diff = filter_git_diff(diff) if diff else ""
351+
filtered_incremental_diff = filter_git_diff(incremental_diff) if incremental_diff else ""
352+
diff_by_files = split_git_diff_by_files(filtered_diff) if filtered_diff else {}
353+
incremental_diff_by_files = (
354+
split_git_diff_by_files(filtered_incremental_diff) if filtered_incremental_diff else {}
355+
)
356+
343357
return TrajectoryInfo(
344358
player_id=player_id,
345359
round_num=round_num,
@@ -353,6 +367,8 @@ def parse_trajectory(self, player_id: int, round_num: int) -> TrajectoryInfo | N
353367
incremental_diff=incremental_diff,
354368
modified_files=modified_files,
355369
trajectory_file_path=str(traj_file),
370+
diff_by_files=diff_by_files,
371+
incremental_diff_by_files=incremental_diff_by_files,
356372
)
357373
except (json.JSONDecodeError, KeyError) as e:
358374
print(f"Error parsing {traj_file}: {e}")

codeclash/viewer/templates/index.html

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,23 @@ <h3>
330330
</details>
331331

332332
<!-- Diff Foldout inside the same container -->
333-
{% if trajectory.diff and trajectory.diff.strip() %}
333+
{% if trajectory.diff_by_files and trajectory.diff_by_files %}
334+
<details class="trajectory-messages-foldout">
335+
<summary>📝 Diff (Full) ({{ trajectory.diff_by_files|length }} files)</summary>
336+
<div class="trajectory-content">
337+
<div class="log-content">
338+
{% for file_path, file_diff in trajectory.diff_by_files.items() %}
339+
<details class="foldout">
340+
<summary>{{ file_path }}</summary>
341+
<div class="log-content">
342+
<pre><code>{{ file_diff }}</code></pre>
343+
</div>
344+
</details>
345+
{% endfor %}
346+
</div>
347+
</div>
348+
</details>
349+
{% elif trajectory.diff and trajectory.diff.strip() %}
334350
<details class="trajectory-messages-foldout">
335351
<summary>📝 Diff (Full)</summary>
336352
<div class="trajectory-content">
@@ -342,7 +358,23 @@ <h3>
342358
{% endif %}
343359

344360
<!-- Incremental Diff Foldout inside the same container -->
345-
{% if trajectory.incremental_diff and trajectory.incremental_diff.strip() %}
361+
{% if trajectory.incremental_diff_by_files and trajectory.incremental_diff_by_files %}
362+
<details class="trajectory-messages-foldout">
363+
<summary>🔄 Incremental Diff ({{ trajectory.incremental_diff_by_files|length }} files)</summary>
364+
<div class="trajectory-content">
365+
<div class="log-content">
366+
{% for file_path, file_diff in trajectory.incremental_diff_by_files.items() %}
367+
<details class="foldout">
368+
<summary>{{ file_path }}</summary>
369+
<div class="log-content">
370+
<pre><code>{{ file_diff }}</code></pre>
371+
</div>
372+
</details>
373+
{% endfor %}
374+
</div>
375+
</div>
376+
</details>
377+
{% elif trajectory.incremental_diff and trajectory.incremental_diff.strip() %}
346378
<details class="trajectory-messages-foldout">
347379
<summary>🔄 Incremental Diff</summary>
348380
<div class="trajectory-content">
@@ -423,14 +455,21 @@ <h3>
423455
<details class="foldout round-foldout">
424456
<summary>
425457
🏆 Round {{ round_num }} Results
426-
{% if round_data.results.winner %}
427-
- Winner: {{ round_data.results.winner }}
428-
{% if round_data.results.winner_percentage %}
429-
with {{ round_data.results.winner_percentage }}%
430-
{% endif %}
431-
{% if round_data.results.sorted_scores %}
432-
(
433-
{%- for player, score in round_data.results.sorted_scores -%}
458+
{% if round_data.results.winner and round_data.results.scores %}
459+
{%- set total_games = round_data.results.scores.values() | sum -%}
460+
{%- set winner = round_data.results.winner -%}
461+
{%- if winner != 'Tie' -%}
462+
{%- set winner_wins = round_data.results.scores.get(winner, 0) -%}
463+
{%- set ties = round_data.results.scores.get('Tie', 0) -%}
464+
{%- set win_percentage = ((winner_wins + 0.5 * ties) / total_games * 100) | round(1) -%}
465+
- Winner: {{ winner }} with {{ win_percentage }}% (
466+
{%- for player, score in round_data.results.scores.items() -%}
467+
{{- score -}}
468+
{%- if not loop.last -%}-{%- endif -%}
469+
{%- endfor -%})
470+
{%- else -%}
471+
- Winner: Tie (
472+
{%- for player, score in round_data.results.scores.items() -%}
434473
{{- score -}}
435474
{%- if not loop.last -%}-{%- endif -%}
436475
{%- endfor -%})

0 commit comments

Comments
 (0)