FossilRepo

fossilrepo / templates / fossil / checkin_detail.html
1
{% extends "base.html" %}
2
{% load fossil_filters %}
3
{% block title %}{{ checkin.uuid|truncatechars:12 }} — {{ 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; font-weight: 500; }
15
.diff-line-header td { color: #6b7280; font-weight: 600; }
16
.diff-gutter { width: 1%; user-select: none; color: #4b5563; text-align: right; padding: 0 6px; border-right: 1px solid #374151; cursor: pointer; white-space: nowrap; }
17
.diff-gutter:hover { color: #DC394C; }
18
.diff-gutter a { color: inherit; text-decoration: none; display: block; }
19
.line-row:target { background: rgba(220, 57, 76, 0.15) !important; }
20
.line-row:target .diff-gutter { color: #DC394C; font-weight: 600; }
21
.line-row { scroll-margin-top: 40vh; }
22
/* Split diff view */
23
.split-diff { display: grid; grid-template-columns: 1fr; }
24
@media (min-width: 768px) { .split-diff { grid-template-columns: 1fr 1fr; } }
25
.split-diff-side { overflow-x: auto; }
26
@media (min-width: 768px) { .split-diff-side:first-child { border-right: 1px solid #374151; } }
27
@media (max-width: 767px) { .split-diff-side:first-child { border-bottom: 1px solid #374151; } }
28
.split-diff-side .diff-table td:last-child { width: 100%; }
29
.split-line-add { background: rgba(34, 197, 94, 0.1); }
30
.split-line-add td:last-child { color: #86efac; }
31
.split-line-del { background: rgba(239, 68, 68, 0.1); }
32
.split-line-del td:last-child { color: #fca5a5; }
33
.split-line-empty { background: rgba(107, 114, 128, 0.05); }
34
.split-line-empty td:last-child { color: transparent; }
35
/* Syntax highlighting: preserve diff bg colors over hljs */
36
.diff-code .hljs { background: transparent !important; padding: 0 !important; }
37
.diff-code { display: inline; }
38
.line-popover {
39
position: absolute; left: 100%; top: 50%; transform: translateY(-50%);
40
margin-left: 4px; z-index: 20; white-space: nowrap;
41
background: #1f2937; border: 1px solid #374151; border-radius: 6px;
42
box-shadow: 0 4px 12px rgba(0,0,0,0.4); padding: 2px;
43
display: flex; gap: 2px;
44
}
45
.line-popover button {
46
display: flex; align-items: center; gap: 4px; padding: 4px 8px;
47
font-size: 0.7rem; color: #d1d5db; background: transparent;
48
border: none; border-radius: 4px; cursor: pointer;
49
}
50
.line-popover button:hover { background: #374151; color: #fff; }
51
.line-popover button.copied { color: #22c55e; }
52
</style>
53
{% endblock %}
54
55
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
57
{% include "fossil/_project_nav.html" %}
58
59
<div class="space-y-4">
60
<!-- Commit header -->
61
<div class="rounded-lg bg-gray-800 border border-gray-700 shadow-sm">
62
<div class="px-6 py-5">
63
<p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p>
64
<div class="mt-3 flex items-center gap-x-4 gap-y-1 flex-wrap text-sm">
65
<a href="{% url 'fossil:user_activity' slug=project.slug username=checkin.user %}" class="font-medium text-gray-200 hover:text-brand-light">{{ checkin.user|display_user }}</a>
66
<span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }} UTC</span>
67
{% if checkin.branch %}
68
<span class="inline-flex items-center rounded-md bg-brand/10 border border-brand/20 px-2 py-0.5 text-xs text-brand-light">
69
{{ checkin.branch }}
70
</span>
71
{% endif %}
72
{% if checkin.is_merge %}
73
<span class="inline-flex items-center rounded-full bg-purple-900/50 px-2 py-0.5 text-xs text-purple-300">merge</span>
74
{% endif %}
75
</div>
76
</div>
77
{% if status_checks %}
78
<div class="px-6 py-3 border-t border-gray-700 bg-gray-800/50">
79
<div class="flex items-center gap-2 mb-2">
80
<span class="text-xs font-medium text-gray-400">CI Status</span>
81
<img src="{% url 'fossil:status_badge' slug=project.slug checkin_uuid=checkin.uuid %}" alt="CI Status" class="h-5">
82
</div>
83
<div class="flex flex-wrap gap-x-2 gap-y-1.5">
84
{% for check in status_checks %}
85
<a {% if check.target_url and check.target_url|slice:":4" == "http" %}href="{{ check.target_url }}" target="_blank" rel="noopener"{% endif %}
86
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs
87
{% if check.state == 'success' %}border-green-800 bg-green-900/30 text-green-300
88
{% elif check.state == 'failure' %}border-red-800 bg-red-900/30 text-red-300
89
{% elif check.state == 'error' %}border-red-800 bg-red-900/30 text-red-300
90
{% else %}border-yellow-800 bg-yellow-900/30 text-yellow-300{% endif %}"
91
{% if check.description %}title="{{ check.description }}"{% endif %}>
92
{% if check.state == 'success' %}
93
<svg class="h-3.5 w-3.5 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
94
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/>
95
</svg>
96
{% elif check.state == 'failure' or check.state == 'error' %}
97
<svg class="h-3.5 w-3.5 text-red-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
98
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
99
</svg>
100
{% else %}
101
<svg class="h-3.5 w-3.5 text-yellow-400" fill="currentColor" viewBox="0 0 24 24">
102
<circle cx="12" cy="12" r="6"/>
103
</svg>
104
{% endif %}
105
{{ check.context }}
106
</a>
107
{% endfor %}
108
</div>
109
</div>
110
{% endif %}
111
<div class="px-6 py-3 border-t border-gray-700 bg-gray-800/50 flex items-center gap-x-6 gap-y-2 flex-wrap text-xs">
112
<div class="flex items-center gap-2">
113
<span class="text-gray-500">Commit</span>
114
<code class="font-mono text-gray-300 break-all">{{ checkin.uuid }}</code>
115
</div>
116
{% if checkin.parent_uuid %}
117
<div class="flex items-center gap-2">
118
<span class="text-gray-500">Parent</span>
119
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=checkin.parent_uuid %}"
120
class="font-mono text-brand-light hover:text-brand">{{ checkin.parent_uuid|truncatechars:16 }}</a>
121
</div>
122
{% endif %}
123
</div>
124
</div>
125
126
<!-- File summary bar -->
127
{% if file_diffs %}
128
<div class="rounded-lg bg-gray-800 border border-gray-700 px-4 py-3 shadow-sm">
129
<div class="flex items-center gap-4 text-sm">
130
<span class="text-gray-300 font-medium">{{ file_diffs|length }} file{{ file_diffs|length|pluralize }} changed</span>
131
{% with total_add=0 total_del=0 %}
132
{% for fd in file_diffs %}
133
{% if fd.additions %}
134
<span class="text-green-400">+{{ fd.additions }}</span>
135
{% endif %}
136
{% if fd.deletions %}
137
<span class="text-red-400">-{{ fd.deletions }}</span>
138
{% endif %}
139
{% endfor %}
140
{% endwith %}
141
</div>
142
<div class="mt-2 flex flex-wrap gap-x-2 gap-y-1">
143
{% for fd in file_diffs %}
144
<a href="#diff-{{ forloop.counter }}" class="text-xs font-mono text-brand-light hover:text-brand break-all">
145
{% if fd.change_type == "added" %}
146
<span class="text-green-400">+</span>
147
{% elif fd.change_type == "deleted" %}
148
<span class="text-red-400">-</span>
149
{% else %}
150
<span class="text-yellow-400">~</span>
151
{% endif %}
152
{{ fd.name }}
153
</a>
154
{% endfor %}
155
</div>
156
</div>
157
158
<!-- Diff mode toggle -->
159
<div x-data="{ mode: localStorage.getItem('diff-mode') || 'unified' }" x-init="$watch('mode', val => localStorage.setItem('diff-mode', val))" x-ref="diffToggle">
160
<div class="flex items-center gap-1 mb-3">
161
<button @click="mode = 'unified'" :class="mode === 'unified' ? 'bg-gray-700 text-gray-100 shadow-sm' : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800'" class="px-3 py-1 text-xs rounded-md border border-gray-700 transition-colors">Unified</button>
162
<button @click="mode = 'split'" :class="mode === 'split' ? 'bg-gray-700 text-gray-100 shadow-sm' : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800'" class="px-3 py-1 text-xs rounded-md border border-gray-700 transition-colors">Split</button>
163
</div>
164
165
<!-- Diffs -->
166
{% for fd in file_diffs %}
167
<div class="rounded-lg bg-gray-800 border border-gray-700 overflow-hidden mb-4 shadow-sm" id="diff-{{ forloop.counter }}" data-filename="{{ fd.name }}">
168
<div class="px-4 py-2.5 border-b border-gray-700 flex items-center justify-between bg-gray-900/50">
169
<div class="flex items-center gap-3">
170
{% if fd.change_type == "added" %}
171
<span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-green-900/50 text-green-300">A</span>
172
{% elif fd.change_type == "deleted" %}
173
<span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-red-900/50 text-red-300">D</span>
174
{% else %}
175
<span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-yellow-900/50 text-yellow-300">M</span>
176
{% endif %}
177
{% if fd.uuid %}
178
<a href="{% url 'fossil:code_file' slug=project.slug filepath=fd.name %}"
179
class="text-sm font-mono text-brand-light hover:text-brand">{{ fd.name }}</a>
180
{% else %}
181
<span class="text-sm font-mono text-gray-400">{{ fd.name }}</span>
182
{% endif %}
183
</div>
184
<div class="flex items-center gap-2 text-xs">
185
{% if fd.additions %}<span class="text-green-400">+{{ fd.additions }}</span>{% endif %}
186
{% if fd.deletions %}<span class="text-red-400">-{{ fd.deletions }}</span>{% endif %}
187
</div>
188
</div>
189
190
{% if fd.is_binary %}
191
<p class="p-4 text-sm text-gray-500">Binary file</p>
192
{% elif fd.diff_lines %}
193
194
<!-- Unified view -->
195
<div class="overflow-x-auto" x-show="mode === 'unified'">
196
<table class="diff-table">
197
<tbody>
198
{% for dl in fd.diff_lines %}
199
{% if dl.new_num %}<tr class="diff-line-{{ dl.type }} line-row" id="diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}"><td class="diff-gutter">{{ dl.old_num }}</td><td class="diff-gutter" style="position:relative" x-data="{ pop: false, copied: false }" @click.stop="pop = !pop; window.location.hash = 'diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}'" @click.outside="pop = false">{{ dl.new_num }}<div class="line-popover" x-show="pop" x-transition @click.stop><button @click="navigator.clipboard.writeText(window.location.origin + window.location.pathname + '#diff-{{ forloop.parentloop.counter }}-R{{ dl.new_num }}'); copied = true; setTimeout(() => { copied = false; pop = false }, 1000)" :class="copied && 'copied'"><span x-show="!copied">Copy link</span><span x-show="copied">Copied!</span></button></div></td><td><span class="diff-prefix">{{ dl.prefix }}</span><span class="diff-code">{{ dl.code }}</span></td></tr>{% elif dl.old_num %}<tr class="diff-line-{{ dl.type }} line-row" id="diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}"><td class="diff-gutter" style="position:relative" x-data="{ pop: false, copied: false }" @click.stop="pop = !pop; window.location.hash = 'diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}'" @click.outside="pop = false">{{ dl.old_num }}<div class="line-popover" x-show="pop" x-transition @click.stop><button @click="navigator.clipboard.writeText(window.location.origin + window.location.pathname + '#diff-{{ forloop.parentloop.counter }}-L{{ dl.old_num }}'); copied = true; setTimeout(() => { copied = false; pop = false }, 1000)" :class="copied && 'copied'"><span x-show="!copied">Copy link</span><span x-show="copied">Copied!</span></button></div></td><td class="diff-gutter"></td><td><span class="diff-prefix">{{ dl.prefix }}</span><span class="diff-code">{{ dl.code }}</span></td></tr>{% else %}<tr class="diff-line-{{ dl.type }}"><td class="diff-gutter"></td><td class="diff-gutter"></td><td><span class="diff-prefix">{{ dl.prefix }}</span><span class="diff-code">{{ dl.code }}</span></td></tr>{% endif %}
200
{% endfor %}
201
</tbody>
202
</table>
203
</div>
204
205
<!-- Split view -->
206
<div class="split-diff" x-show="mode === 'split'" x-cloak>
207
<div class="split-diff-side">
208
<table class="diff-table">
209
<tbody>
210
{% for dl in fd.split_left %}
211
<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 %}">
212
<td class="diff-gutter">{{ dl.old_num }}</td>
213
<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>
214
</tr>
215
{% endfor %}
216
</tbody>
217
</table>
218
</div>
219
<div class="split-diff-side">
220
<table class="diff-table">
221
<tbody>
222
{% for dl in fd.split_right %}
223
<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 %}">
224
<td class="diff-gutter">{{ dl.new_num }}</td>
225
<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>
226
</tr>
227
{% endfor %}
228
</tbody>
229
</table>
230
</div>
231
</div>
232
233
{% else %}
234
<p class="p-4 text-sm text-gray-500">No diff available</p>
235
{% endif %}
236
</div>
237
{% endfor %}
238
</div>
239
{% endif %}
240
</div>
241
<!-- scroll-margin-top on .line-row handles anchor positioning via CSS -->
242
<script>
243
(function() {
244
var langMap = {
245
'py': 'python', 'js': 'javascript', 'ts': 'typescript',
246
'html': 'html', 'css': 'css', 'json': 'json', 'yaml': 'yaml',
247
'yml': 'yaml', 'md': 'markdown', 'sh': 'bash', 'sql': 'sql',
248
'rs': 'rust', 'go': 'go', 'rb': 'ruby', 'java': 'java',
249
'toml': 'toml', 'xml': 'xml', 'jsx': 'javascript', 'tsx': 'typescript',
250
'c': 'c', 'h': 'c', 'cpp': 'cpp', 'hpp': 'cpp',
251
};
252
function highlightDiffCode() {
253
document.querySelectorAll('[data-filename]').forEach(function(container) {
254
var filename = container.dataset.filename || '';
255
var ext = filename.split('.').pop();
256
var lang = langMap[ext] || '';
257
if (lang && window.hljs) {
258
container.querySelectorAll('.diff-code').forEach(function(el) {
259
if (el.dataset.highlighted) return;
260
var text = el.textContent;
261
if (!text.trim()) return;
262
try {
263
var result = hljs.highlight(text, { language: lang, ignoreIllegals: true });
264
el.innerHTML = result.value;
265
el.dataset.highlighted = '1';
266
} catch(e) {}
267
});
268
}
269
});
270
}
271
// Run after DOM ready and after Alpine toggles
272
if (document.readyState === 'loading') {
273
document.addEventListener('DOMContentLoaded', highlightDiffCode);
274
} else {
275
highlightDiffCode();
276
}
277
// Re-run when split view is toggled (elements become visible)
278
document.addEventListener('click', function(e) {
279
if (e.target.closest('[x-ref="diffToggle"]')) {
280
setTimeout(highlightDiffCode, 50);
281
}
282
});
283
})();
284
</script>
285
{% endblock %}
286

Keyboard Shortcuts

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