FossilRepo

fossilrepo / templates / fossil / compare.html
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 }} &middot; {{ 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 }} &middot; {{ 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' %}&nbsp;{% 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' %}&nbsp;{% 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button