FossilRepo

Add line numbers with permalinks, show full commit hash - Code file viewer: clickable line numbers (#L1, #L42, etc.) - Line permalink anchors — click line number to update URL hash - Highlighted target line (scroll + brand-colored highlight) - Line count shown in header - Checkin detail: show full commit hash instead of truncated - Per-line syntax highlighting via highlight.js

lmata 2026-04-06 13:40 trunk
Commit fc1b99b27f00c222778e9ab3582cd11c4e41ee37b674734113407ab5a9ecb760
--- fossil/views.py
+++ fossil/views.py
@@ -101,19 +101,26 @@
101101
parts = filepath.split("/")
102102
file_breadcrumbs = []
103103
for i, part in enumerate(parts):
104104
file_breadcrumbs.append({"name": part, "path": "/".join(parts[: i + 1])})
105105
106
+ # Split into lines for line-number display
107
+ lines = content.split("\n") if not is_binary else []
108
+ # Enumerate for template (1-indexed)
109
+ numbered_lines = [{"num": i + 1, "text": line} for i, line in enumerate(lines)]
110
+
106111
return render(
107112
request,
108113
"fossil/code_file.html",
109114
{
110115
"project": project,
111116
"fossil_repo": fossil_repo,
112117
"filepath": filepath,
113118
"file_breadcrumbs": file_breadcrumbs,
114119
"content": content,
120
+ "lines": numbered_lines,
121
+ "line_count": len(lines),
115122
"is_binary": is_binary,
116123
"language": ext,
117124
"active_tab": "code",
118125
},
119126
)
120127
--- fossil/views.py
+++ fossil/views.py
@@ -101,19 +101,26 @@
101 parts = filepath.split("/")
102 file_breadcrumbs = []
103 for i, part in enumerate(parts):
104 file_breadcrumbs.append({"name": part, "path": "/".join(parts[: i + 1])})
105
 
 
 
 
 
106 return render(
107 request,
108 "fossil/code_file.html",
109 {
110 "project": project,
111 "fossil_repo": fossil_repo,
112 "filepath": filepath,
113 "file_breadcrumbs": file_breadcrumbs,
114 "content": content,
 
 
115 "is_binary": is_binary,
116 "language": ext,
117 "active_tab": "code",
118 },
119 )
120
--- fossil/views.py
+++ fossil/views.py
@@ -101,19 +101,26 @@
101 parts = filepath.split("/")
102 file_breadcrumbs = []
103 for i, part in enumerate(parts):
104 file_breadcrumbs.append({"name": part, "path": "/".join(parts[: i + 1])})
105
106 # Split into lines for line-number display
107 lines = content.split("\n") if not is_binary else []
108 # Enumerate for template (1-indexed)
109 numbered_lines = [{"num": i + 1, "text": line} for i, line in enumerate(lines)]
110
111 return render(
112 request,
113 "fossil/code_file.html",
114 {
115 "project": project,
116 "fossil_repo": fossil_repo,
117 "filepath": filepath,
118 "file_breadcrumbs": file_breadcrumbs,
119 "content": content,
120 "lines": numbered_lines,
121 "line_count": len(lines),
122 "is_binary": is_binary,
123 "language": ext,
124 "active_tab": "code",
125 },
126 )
127
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -24,17 +24,17 @@
2424
</div>
2525
</div>
2626
<div class="px-6 py-3 border-t border-gray-700 bg-gray-800/50 flex items-center gap-6 text-xs">
2727
<div class="flex items-center gap-2">
2828
<span class="text-gray-500">Commit</span>
29
- <code class="font-mono text-gray-300">{{ checkin.uuid|truncatechars:16 }}</code>
29
+ <code class="font-mono text-gray-300 text-xs break-all">{{ checkin.uuid }}</code>
3030
</div>
3131
{% if checkin.parent_uuid %}
3232
<div class="flex items-center gap-2">
3333
<span class="text-gray-500">Parent</span>
3434
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=checkin.parent_uuid %}"
35
- class="font-mono text-brand-light hover:text-brand">{{ checkin.parent_uuid|truncatechars:12 }}</a>
35
+ class="font-mono text-brand-light hover:text-brand text-xs">{{ checkin.parent_uuid|truncatechars:16 }}</a>
3636
</div>
3737
{% endif %}
3838
<div class="flex items-center gap-2">
3939
<span class="text-gray-500">Files changed</span>
4040
<span class="text-gray-300">{{ checkin.files_changed|length }}</span>
4141
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -24,17 +24,17 @@
24 </div>
25 </div>
26 <div class="px-6 py-3 border-t border-gray-700 bg-gray-800/50 flex items-center gap-6 text-xs">
27 <div class="flex items-center gap-2">
28 <span class="text-gray-500">Commit</span>
29 <code class="font-mono text-gray-300">{{ checkin.uuid|truncatechars:16 }}</code>
30 </div>
31 {% if checkin.parent_uuid %}
32 <div class="flex items-center gap-2">
33 <span class="text-gray-500">Parent</span>
34 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=checkin.parent_uuid %}"
35 class="font-mono text-brand-light hover:text-brand">{{ checkin.parent_uuid|truncatechars:12 }}</a>
36 </div>
37 {% endif %}
38 <div class="flex items-center gap-2">
39 <span class="text-gray-500">Files changed</span>
40 <span class="text-gray-300">{{ checkin.files_changed|length }}</span>
41
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -24,17 +24,17 @@
24 </div>
25 </div>
26 <div class="px-6 py-3 border-t border-gray-700 bg-gray-800/50 flex items-center gap-6 text-xs">
27 <div class="flex items-center gap-2">
28 <span class="text-gray-500">Commit</span>
29 <code class="font-mono text-gray-300 text-xs break-all">{{ checkin.uuid }}</code>
30 </div>
31 {% if checkin.parent_uuid %}
32 <div class="flex items-center gap-2">
33 <span class="text-gray-500">Parent</span>
34 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=checkin.parent_uuid %}"
35 class="font-mono text-brand-light hover:text-brand text-xs">{{ checkin.parent_uuid|truncatechars:16 }}</a>
36 </div>
37 {% endif %}
38 <div class="flex items-center gap-2">
39 <span class="text-gray-500">Files changed</span>
40 <span class="text-gray-300">{{ checkin.files_changed|length }}</span>
41
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -2,19 +2,37 @@
22
{% block title %}{{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
33
44
{% block extra_head %}
55
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
66
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
7
+<style>
8
+ .code-table { border-collapse: collapse; width: 100%; }
9
+ .code-table td { padding: 0; vertical-align: top; }
10
+ .line-num {
11
+ width: 1%; white-space: nowrap; padding: 0 12px;
12
+ text-align: right; user-select: none; cursor: pointer;
13
+ color: #4b5563; font-size: 0.75rem; line-height: 1.5rem;
14
+ }
15
+ .line-num:hover { color: #DC394C; }
16
+ .line-num a { color: inherit; text-decoration: none; display: block; }
17
+ .line-code {
18
+ white-space: pre; padding: 0 16px 0 0;
19
+ font-size: 0.8125rem; line-height: 1.5rem;
20
+ }
21
+ .line-row:hover { background: rgba(220, 57, 76, 0.05); }
22
+ .line-row:target { background: rgba(220, 57, 76, 0.12); }
23
+ .line-row:target .line-num { color: #DC394C; font-weight: 600; }
24
+</style>
725
{% endblock %}
826
927
{% block content %}
1028
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
1129
{% include "fossil/_project_nav.html" %}
1230
1331
<div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
14
- <!-- File path breadcrumb -->
15
- <div class="px-4 py-3 border-b border-gray-700">
32
+ <!-- File path breadcrumb + stats -->
33
+ <div class="px-4 py-3 border-b border-gray-700 flex items-center justify-between">
1634
<div class="flex items-center gap-1 text-sm font-mono">
1735
<a href="{% url 'fossil:code' slug=project.slug %}" class="text-brand-light hover:text-brand">{{ project.slug }}</a>
1836
{% for crumb in file_breadcrumbs %}
1937
<span class="text-gray-600">/</span>
2038
{% if forloop.last %}
@@ -22,19 +40,39 @@
2240
{% else %}
2341
<a href="{% url 'fossil:code_dir' slug=project.slug dirpath=crumb.path %}" class="text-brand-light hover:text-brand">{{ crumb.name }}</a>
2442
{% endif %}
2543
{% endfor %}
2644
</div>
45
+ {% if not is_binary %}
46
+ <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
47
+ {% endif %}
2748
</div>
2849
<div class="overflow-x-auto">
2950
{% if is_binary %}
3051
<p class="p-4 text-sm text-gray-500">{{ content }}</p>
3152
{% else %}
32
- <pre class="p-0 m-0"><code class="language-{{ language }} text-sm leading-relaxed">{{ content }}</code></pre>
53
+ <table class="code-table">
54
+ <tbody>
55
+ {% for line in lines %}
56
+ <tr class="line-row" id="L{{ line.num }}">
57
+ <td class="line-num"><a href="#L{{ line.num }}">{{ line.num }}</a></td>
58
+ <td class="line-code"><code class="language-{{ language }}">{{ line.text }}</code></td>
59
+ </tr>
60
+ {% endfor %}
61
+ </tbody>
62
+ </table>
3363
{% endif %}
3464
</div>
3565
</div>
3666
3767
{% if not is_binary %}
38
-<script>hljs.highlightAll();</script>
68
+<script>
69
+ // Highlight each code cell
70
+ document.querySelectorAll('.line-code code').forEach(el => hljs.highlightElement(el));
71
+ // Scroll to line if hash present
72
+ if (window.location.hash) {
73
+ const el = document.querySelector(window.location.hash);
74
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
75
+ }
76
+</script>
3977
{% endif %}
4078
{% endblock %}
4179
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -2,19 +2,37 @@
2 {% block title %}{{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block extra_head %}
5 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
6 <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7 {% endblock %}
8
9 {% block content %}
10 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
11 {% include "fossil/_project_nav.html" %}
12
13 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
14 <!-- File path breadcrumb -->
15 <div class="px-4 py-3 border-b border-gray-700">
16 <div class="flex items-center gap-1 text-sm font-mono">
17 <a href="{% url 'fossil:code' slug=project.slug %}" class="text-brand-light hover:text-brand">{{ project.slug }}</a>
18 {% for crumb in file_breadcrumbs %}
19 <span class="text-gray-600">/</span>
20 {% if forloop.last %}
@@ -22,19 +40,39 @@
22 {% else %}
23 <a href="{% url 'fossil:code_dir' slug=project.slug dirpath=crumb.path %}" class="text-brand-light hover:text-brand">{{ crumb.name }}</a>
24 {% endif %}
25 {% endfor %}
26 </div>
 
 
 
27 </div>
28 <div class="overflow-x-auto">
29 {% if is_binary %}
30 <p class="p-4 text-sm text-gray-500">{{ content }}</p>
31 {% else %}
32 <pre class="p-0 m-0"><code class="language-{{ language }} text-sm leading-relaxed">{{ content }}</code></pre>
 
 
 
 
 
 
 
 
 
33 {% endif %}
34 </div>
35 </div>
36
37 {% if not is_binary %}
38 <script>hljs.highlightAll();</script>
 
 
 
 
 
 
 
 
39 {% endif %}
40 {% endblock %}
41
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -2,19 +2,37 @@
2 {% block title %}{{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block extra_head %}
5 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
6 <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
7 <style>
8 .code-table { border-collapse: collapse; width: 100%; }
9 .code-table td { padding: 0; vertical-align: top; }
10 .line-num {
11 width: 1%; white-space: nowrap; padding: 0 12px;
12 text-align: right; user-select: none; cursor: pointer;
13 color: #4b5563; font-size: 0.75rem; line-height: 1.5rem;
14 }
15 .line-num:hover { color: #DC394C; }
16 .line-num a { color: inherit; text-decoration: none; display: block; }
17 .line-code {
18 white-space: pre; padding: 0 16px 0 0;
19 font-size: 0.8125rem; line-height: 1.5rem;
20 }
21 .line-row:hover { background: rgba(220, 57, 76, 0.05); }
22 .line-row:target { background: rgba(220, 57, 76, 0.12); }
23 .line-row:target .line-num { color: #DC394C; font-weight: 600; }
24 </style>
25 {% endblock %}
26
27 {% block content %}
28 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
29 {% include "fossil/_project_nav.html" %}
30
31 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
32 <!-- File path breadcrumb + stats -->
33 <div class="px-4 py-3 border-b border-gray-700 flex items-center justify-between">
34 <div class="flex items-center gap-1 text-sm font-mono">
35 <a href="{% url 'fossil:code' slug=project.slug %}" class="text-brand-light hover:text-brand">{{ project.slug }}</a>
36 {% for crumb in file_breadcrumbs %}
37 <span class="text-gray-600">/</span>
38 {% if forloop.last %}
@@ -22,19 +40,39 @@
40 {% else %}
41 <a href="{% url 'fossil:code_dir' slug=project.slug dirpath=crumb.path %}" class="text-brand-light hover:text-brand">{{ crumb.name }}</a>
42 {% endif %}
43 {% endfor %}
44 </div>
45 {% if not is_binary %}
46 <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
47 {% endif %}
48 </div>
49 <div class="overflow-x-auto">
50 {% if is_binary %}
51 <p class="p-4 text-sm text-gray-500">{{ content }}</p>
52 {% else %}
53 <table class="code-table">
54 <tbody>
55 {% for line in lines %}
56 <tr class="line-row" id="L{{ line.num }}">
57 <td class="line-num"><a href="#L{{ line.num }}">{{ line.num }}</a></td>
58 <td class="line-code"><code class="language-{{ language }}">{{ line.text }}</code></td>
59 </tr>
60 {% endfor %}
61 </tbody>
62 </table>
63 {% endif %}
64 </div>
65 </div>
66
67 {% if not is_binary %}
68 <script>
69 // Highlight each code cell
70 document.querySelectorAll('.line-code code').forEach(el => hljs.highlightElement(el));
71 // Scroll to line if hash present
72 if (window.location.hash) {
73 const el = document.querySelector(window.location.hash);
74 if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
75 }
76 </script>
77 {% endif %}
78 {% endblock %}
79

Keyboard Shortcuts

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