|
1
|
{% extends "base.html" %} |
|
2
|
{% load fossil_filters %} |
|
3
|
{% block title %}Compare — {{ project.name }} — Fossilrepo{% endblock %} |
|
4
|
|
|
5
|
{% block extra_head %} |
|
6
|
<style> |
|
7
|
.diff-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; } |
|
8
|
.diff-table td { padding: 0 8px; white-space: pre; vertical-align: top; line-height: 1.4rem; } |
|
9
|
.diff-line-add { background: rgba(34, 197, 94, 0.1); } |
|
10
|
.diff-line-add td:last-child { color: #86efac; } |
|
11
|
.diff-line-del { background: rgba(239, 68, 68, 0.1); } |
|
12
|
.diff-line-del td:last-child { color: #fca5a5; } |
|
13
|
.diff-line-hunk { background: rgba(96, 165, 250, 0.08); } |
|
14
|
.diff-line-hunk td { color: #93c5fd; } |
|
15
|
.diff-line-header td { color: #6b7280; } |
|
16
|
.diff-gutter { width: 1%; user-select: none; color: #4b5563; text-align: right; padding: 0 6px; border-right: 1px solid #374151; white-space: nowrap; } |
|
17
|
/* Split diff view */ |
|
18
|
.split-diff { display: grid; grid-template-columns: 1fr; } |
|
19
|
@media (min-width: 768px) { .split-diff { grid-template-columns: 1fr 1fr; } } |
|
20
|
.split-diff-side { overflow-x: auto; } |
|
21
|
@media (min-width: 768px) { .split-diff-side:first-child { border-right: 1px solid #374151; } } |
|
22
|
@media (max-width: 767px) { .split-diff-side:first-child { border-bottom: 1px solid #374151; } } |
|
23
|
.split-diff-side .diff-table td:last-child { width: 100%; } |
|
24
|
.split-line-add { background: rgba(34, 197, 94, 0.1); } |
|
25
|
.split-line-add td:last-child { color: #86efac; } |
|
26
|
.split-line-del { background: rgba(239, 68, 68, 0.1); } |
|
27
|
.split-line-del td:last-child { color: #fca5a5; } |
|
28
|
.split-line-empty { background: rgba(107, 114, 128, 0.05); } |
|
29
|
.split-line-empty td:last-child { color: transparent; } |
|
30
|
/* Syntax highlighting: preserve diff bg colors over hljs */ |
|
31
|
.diff-code .hljs { background: transparent !important; padding: 0 !important; } |
|
32
|
.diff-code { display: inline; } |
|
33
|
</style> |
|
34
|
{% endblock %} |
|
35
|
|
|
36
|
{% block content %} |
|
37
|
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
|
38
|
{% include "fossil/_project_nav.html" %} |
|
39
|
|
|
40
|
<div class="mb-6"> |
|
41
|
<h2 class="text-lg font-semibold text-gray-200 mb-3">Compare Checkins</h2> |
|
42
|
<form method="get" class="flex flex-col sm:flex-row sm:items-end gap-3"> |
|
43
|
<div class="flex-1"> |
|
44
|
<label class="block text-xs text-gray-500 mb-1">From (older)</label> |
|
45
|
<input type="text" name="from" value="{{ from_uuid }}" placeholder="Checkin hash..." |
|
46
|
aria-label="From checkin hash" |
|
47
|
class="w-full rounded-md border-gray-700 bg-gray-800 text-gray-100 text-sm px-3 py-2 font-mono focus:border-brand focus:ring-brand"> |
|
48
|
</div> |
|
49
|
<div class="flex-1"> |
|
50
|
<label class="block text-xs text-gray-500 mb-1">To (newer)</label> |
|
51
|
<input type="text" name="to" value="{{ to_uuid }}" placeholder="Checkin hash..." |
|
52
|
aria-label="To checkin hash" |
|
53
|
class="w-full rounded-md border-gray-700 bg-gray-800 text-gray-100 text-sm px-3 py-2 font-mono focus:border-brand focus:ring-brand"> |
|
54
|
</div> |
|
55
|
<button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white hover:bg-brand-hover">Compare</button> |
|
56
|
</form> |
|
57
|
</div> |
|
58
|
|
|
59
|
{% if from_detail and to_detail %} |
|
60
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6"> |
|
61
|
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
|
62
|
<div class="text-xs text-gray-500 mb-1">From</div> |
|
63
|
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=from_detail.uuid %}" class="text-sm text-brand-light hover:text-brand">{{ from_detail.comment|truncatechars:60 }}</a> |
|
64
|
<div class="mt-1 text-xs text-gray-500">{{ from_detail.user|display_user }} · {{ from_detail.timestamp|date:"Y-m-d H:i" }}</div> |
|
65
|
</div> |
|
66
|
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
|
67
|
<div class="text-xs text-gray-500 mb-1">To</div> |
|
68
|
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=to_detail.uuid %}" class="text-sm text-brand-light hover:text-brand">{{ to_detail.comment|truncatechars:60 }}</a> |
|
69
|
<div class="mt-1 text-xs text-gray-500">{{ to_detail.user|display_user }} · {{ to_detail.timestamp|date:"Y-m-d H:i" }}</div> |
|
70
|
</div> |
|
71
|
</div> |
|
72
|
|
|
73
|
{% if file_diffs %} |
|
74
|
<div x-data="{ mode: localStorage.getItem('diff-mode') || 'unified' }" x-init="$watch('mode', val => localStorage.setItem('diff-mode', val))" x-ref="diffToggle"> |
|
75
|
<div class="flex items-center gap-2 mb-3"> |
|
76
|
<button @click="mode = 'unified'" :class="mode === 'unified' ? 'bg-gray-700 text-gray-100' : 'text-gray-400 hover:text-gray-200'" class="px-3 py-1 text-xs rounded-md">Unified</button> |
|
77
|
<button @click="mode = 'split'" :class="mode === 'split' ? 'bg-gray-700 text-gray-100' : 'text-gray-400 hover:text-gray-200'" class="px-3 py-1 text-xs rounded-md">Split</button> |
|
78
|
</div> |
|
79
|
|
|
80
|
<div class="space-y-4"> |
|
81
|
{% for fd in file_diffs %} |
|
82
|
<div class="rounded-lg bg-gray-800 border border-gray-700 overflow-hidden" data-filename="{{ fd.name }}"> |
|
83
|
<div class="px-4 py-2.5 border-b border-gray-700 bg-gray-900/50 flex items-center justify-between"> |
|
84
|
<span class="text-sm font-mono text-gray-300">{{ fd.name }}</span> |
|
85
|
<div class="flex items-center gap-2 text-xs"> |
|
86
|
{% if fd.additions %}<span class="text-green-400">+{{ fd.additions }}</span>{% endif %} |
|
87
|
{% if fd.deletions %}<span class="text-red-400">-{{ fd.deletions }}</span>{% endif %} |
|
88
|
</div> |
|
89
|
</div> |
|
90
|
|
|
91
|
<!-- Unified view --> |
|
92
|
<div class="overflow-x-auto" x-show="mode === 'unified'"> |
|
93
|
<table class="diff-table"> |
|
94
|
<tbody> |
|
95
|
{% for dl in fd.diff_lines %} |
|
96
|
<tr class="diff-line-{{ dl.type }}"> |
|
97
|
<td class="diff-gutter">{{ dl.old_num }}</td> |
|
98
|
<td class="diff-gutter">{{ dl.new_num }}</td> |
|
99
|
<td><span class="diff-prefix">{{ dl.prefix }}</span><span class="diff-code">{{ dl.code }}</span></td> |
|
100
|
</tr> |
|
101
|
{% endfor %} |
|
102
|
</tbody> |
|
103
|
</table> |
|
104
|
</div> |
|
105
|
|
|
106
|
<!-- Split view --> |
|
107
|
<div class="split-diff" x-show="mode === 'split'" x-cloak> |
|
108
|
<div class="split-diff-side"> |
|
109
|
<table class="diff-table"> |
|
110
|
<tbody> |
|
111
|
{% for dl in fd.split_left %} |
|
112
|
<tr class="{% if dl.type == 'del' %}split-line-del{% elif dl.type == 'hunk' %}diff-line-hunk{% elif dl.type == 'header' %}diff-line-header{% elif dl.type == 'empty' %}split-line-empty{% endif %}"> |
|
113
|
<td class="diff-gutter">{{ dl.old_num }}</td> |
|
114
|
<td>{% if dl.type == 'empty' %} {% elif dl.type == 'hunk' or dl.type == 'header' %}{{ dl.text }}{% else %}<span class="diff-code">{{ dl.code }}</span>{% endif %}</td> |
|
115
|
</tr> |
|
116
|
{% endfor %} |
|
117
|
</tbody> |
|
118
|
</table> |
|
119
|
</div> |
|
120
|
<div class="split-diff-side"> |
|
121
|
<table class="diff-table"> |
|
122
|
<tbody> |
|
123
|
{% for dl in fd.split_right %} |
|
124
|
<tr class="{% if dl.type == 'add' %}split-line-add{% elif dl.type == 'hunk' %}diff-line-hunk{% elif dl.type == 'header' %}diff-line-header{% elif dl.type == 'empty' %}split-line-empty{% endif %}"> |
|
125
|
<td class="diff-gutter">{{ dl.new_num }}</td> |
|
126
|
<td>{% if dl.type == 'empty' %} {% elif dl.type == 'hunk' or dl.type == 'header' %}{{ dl.text }}{% else %}<span class="diff-code">{{ dl.code }}</span>{% endif %}</td> |
|
127
|
</tr> |
|
128
|
{% endfor %} |
|
129
|
</tbody> |
|
130
|
</table> |
|
131
|
</div> |
|
132
|
</div> |
|
133
|
</div> |
|
134
|
{% endfor %} |
|
135
|
</div> |
|
136
|
</div> |
|
137
|
{% else %} |
|
138
|
<p class="text-sm text-gray-500 text-center py-8">No differences found between these checkins.</p> |
|
139
|
{% endif %} |
|
140
|
|
|
141
|
{% elif from_uuid and to_uuid %} |
|
142
|
<p class="text-sm text-gray-500 text-center py-8">One or both checkins not found.</p> |
|
143
|
{% endif %} |
|
144
|
<script> |
|
145
|
(function() { |
|
146
|
var langMap = { |
|
147
|
'py': 'python', 'js': 'javascript', 'ts': 'typescript', |
|
148
|
'html': 'html', 'css': 'css', 'json': 'json', 'yaml': 'yaml', |
|
149
|
'yml': 'yaml', 'md': 'markdown', 'sh': 'bash', 'sql': 'sql', |
|
150
|
'rs': 'rust', 'go': 'go', 'rb': 'ruby', 'java': 'java', |
|
151
|
'toml': 'toml', 'xml': 'xml', 'jsx': 'javascript', 'tsx': 'typescript', |
|
152
|
'c': 'c', 'h': 'c', 'cpp': 'cpp', 'hpp': 'cpp', |
|
153
|
}; |
|
154
|
function highlightDiffCode() { |
|
155
|
document.querySelectorAll('[data-filename]').forEach(function(container) { |
|
156
|
var filename = container.dataset.filename || ''; |
|
157
|
var ext = filename.split('.').pop(); |
|
158
|
var lang = langMap[ext] || ''; |
|
159
|
if (lang && window.hljs) { |
|
160
|
container.querySelectorAll('.diff-code').forEach(function(el) { |
|
161
|
if (el.dataset.highlighted) return; |
|
162
|
var text = el.textContent; |
|
163
|
if (!text.trim()) return; |
|
164
|
try { |
|
165
|
var result = hljs.highlight(text, { language: lang, ignoreIllegals: true }); |
|
166
|
el.innerHTML = result.value; |
|
167
|
el.dataset.highlighted = '1'; |
|
168
|
} catch(e) {} |
|
169
|
}); |
|
170
|
} |
|
171
|
}); |
|
172
|
} |
|
173
|
if (document.readyState === 'loading') { |
|
174
|
document.addEventListener('DOMContentLoaded', highlightDiffCode); |
|
175
|
} else { |
|
176
|
highlightDiffCode(); |
|
177
|
} |
|
178
|
document.addEventListener('click', function(e) { |
|
179
|
if (e.target.closest('[x-ref="diffToggle"]')) { |
|
180
|
setTimeout(highlightDiffCode, 50); |
|
181
|
} |
|
182
|
}); |
|
183
|
})(); |
|
184
|
</script> |
|
185
|
{% endblock %} |
|
186
|
|