FossilRepo

Add branches view, Source/Rendered toggle, Pikchr diagrams Branches: - New /fossil/branches/ view listing all branches - Shows branch name, last checkin hash, user, count, time ago - "Branches" tab in project navigation Code file viewer: - Source/Rendered toggle for .wiki, .md, .html files - "Rendered" mode passes content through _render_fossil_content - Toggle pills in the file header bar - Fixes viewing docs like release-checklist.wiki as rendered markup Pikchr diagrams: - <verbatim type="pikchr"> blocks rendered to SVG via fossil pikchr CLI - ```pikchr fenced code blocks in markdown also rendered - Falls back to <pre><code> display if rendering fails

lmata 2026-04-07 00:08 trunk
Commit b452cbefb7c7ed6b9f8ec6856e1c51e3b09efd10f1291d8313b7fd4b56e02e54
--- fossil/reader.py
+++ fossil/reader.py
@@ -371,10 +371,41 @@
371371
for r in rows:
372372
contributors.append({"user": r["user"], "count": r["cnt"]})
373373
except sqlite3.OperationalError:
374374
pass
375375
return contributors
376
+
377
+ def get_branches(self) -> list[dict]:
378
+ """Get all branches with their latest checkin info."""
379
+ branches = []
380
+ try:
381
+ rows = self.conn.execute(
382
+ """
383
+ SELECT tag.tagname, max(event.mtime) as last_mtime, event.user,
384
+ count(tagxref.rid) as checkin_count, blob.uuid
385
+ FROM tag
386
+ JOIN tagxref ON tag.tagid = tagxref.tagid
387
+ JOIN event ON tagxref.rid = event.objid
388
+ JOIN blob ON event.objid = blob.rid
389
+ WHERE tag.tagname LIKE 'sym-%' AND event.type = 'ci'
390
+ GROUP BY tag.tagname
391
+ ORDER BY last_mtime DESC
392
+ """,
393
+ ).fetchall()
394
+ for r in rows:
395
+ branches.append(
396
+ {
397
+ "name": r["tagname"].replace("sym-", "", 1),
398
+ "last_checkin": _julian_to_datetime(r["last_mtime"]),
399
+ "last_user": r["user"] or "",
400
+ "checkin_count": r["checkin_count"],
401
+ "last_uuid": r["uuid"],
402
+ }
403
+ )
404
+ except sqlite3.OperationalError:
405
+ pass
406
+ return branches
376407
377408
# --- Timeline ---
378409
379410
def get_timeline(self, limit: int = 50, offset: int = 0, event_type: str | None = None) -> list[TimelineEntry]:
380411
sql = """
381412
--- fossil/reader.py
+++ fossil/reader.py
@@ -371,10 +371,41 @@
371 for r in rows:
372 contributors.append({"user": r["user"], "count": r["cnt"]})
373 except sqlite3.OperationalError:
374 pass
375 return contributors
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
377 # --- Timeline ---
378
379 def get_timeline(self, limit: int = 50, offset: int = 0, event_type: str | None = None) -> list[TimelineEntry]:
380 sql = """
381
--- fossil/reader.py
+++ fossil/reader.py
@@ -371,10 +371,41 @@
371 for r in rows:
372 contributors.append({"user": r["user"], "count": r["cnt"]})
373 except sqlite3.OperationalError:
374 pass
375 return contributors
376
377 def get_branches(self) -> list[dict]:
378 """Get all branches with their latest checkin info."""
379 branches = []
380 try:
381 rows = self.conn.execute(
382 """
383 SELECT tag.tagname, max(event.mtime) as last_mtime, event.user,
384 count(tagxref.rid) as checkin_count, blob.uuid
385 FROM tag
386 JOIN tagxref ON tag.tagid = tagxref.tagid
387 JOIN event ON tagxref.rid = event.objid
388 JOIN blob ON event.objid = blob.rid
389 WHERE tag.tagname LIKE 'sym-%' AND event.type = 'ci'
390 GROUP BY tag.tagname
391 ORDER BY last_mtime DESC
392 """,
393 ).fetchall()
394 for r in rows:
395 branches.append(
396 {
397 "name": r["tagname"].replace("sym-", "", 1),
398 "last_checkin": _julian_to_datetime(r["last_mtime"]),
399 "last_user": r["user"] or "",
400 "checkin_count": r["checkin_count"],
401 "last_uuid": r["uuid"],
402 }
403 )
404 except sqlite3.OperationalError:
405 pass
406 return branches
407
408 # --- Timeline ---
409
410 def get_timeline(self, limit: int = 50, offset: int = 0, event_type: str | None = None) -> list[TimelineEntry]:
411 sql = """
412
--- fossil/urls.py
+++ fossil/urls.py
@@ -18,8 +18,9 @@
1818
path("wiki/edit/<path:page_name>", views.wiki_edit, name="wiki_edit"),
1919
path("tickets/create/", views.ticket_create, name="ticket_create"),
2020
path("forum/", views.forum_list, name="forum"),
2121
path("forum/<str:thread_uuid>/", views.forum_thread, name="forum_thread"),
2222
path("user/<str:username>/", views.user_activity, name="user_activity"),
23
+ path("branches/", views.branch_list, name="branches"),
2324
path("docs/", views.fossil_docs, name="docs"),
2425
path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
2526
]
2627
--- fossil/urls.py
+++ fossil/urls.py
@@ -18,8 +18,9 @@
18 path("wiki/edit/<path:page_name>", views.wiki_edit, name="wiki_edit"),
19 path("tickets/create/", views.ticket_create, name="ticket_create"),
20 path("forum/", views.forum_list, name="forum"),
21 path("forum/<str:thread_uuid>/", views.forum_thread, name="forum_thread"),
22 path("user/<str:username>/", views.user_activity, name="user_activity"),
 
23 path("docs/", views.fossil_docs, name="docs"),
24 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
25 ]
26
--- fossil/urls.py
+++ fossil/urls.py
@@ -18,8 +18,9 @@
18 path("wiki/edit/<path:page_name>", views.wiki_edit, name="wiki_edit"),
19 path("tickets/create/", views.ticket_create, name="ticket_create"),
20 path("forum/", views.forum_list, name="forum"),
21 path("forum/<str:thread_uuid>/", views.forum_thread, name="forum_thread"),
22 path("user/<str:username>/", views.user_activity, name="user_activity"),
23 path("branches/", views.branch_list, name="branches"),
24 path("docs/", views.fossil_docs, name="docs"),
25 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
26 ]
27
+36 -1
--- fossil/views.py
+++ fossil/views.py
@@ -339,13 +339,22 @@
339339
for i, part in enumerate(parts):
340340
file_breadcrumbs.append({"name": part, "path": "/".join(parts[: i + 1])})
341341
342342
# Split into lines for line-number display
343343
lines = content.split("\n") if not is_binary else []
344
- # Enumerate for template (1-indexed)
345344
numbered_lines = [{"num": i + 1, "text": line} for i, line in enumerate(lines)]
346345
346
+ # Check if file can be rendered (wiki, markdown, html)
347
+ can_render = ext in ("wiki", "md", "markdown", "html", "htm")
348
+ view_mode = request.GET.get("mode", "source")
349
+ rendered_html = ""
350
+ if can_render and view_mode == "rendered" and not is_binary:
351
+ doc_base = "/".join(filepath.split("/")[:-1])
352
+ if doc_base:
353
+ doc_base += "/"
354
+ rendered_html = mark_safe(_render_fossil_content(content, project_slug=slug, base_path=doc_base))
355
+
347356
return render(
348357
request,
349358
"fossil/code_file.html",
350359
{
351360
"project": project,
@@ -355,10 +364,13 @@
355364
"content": content,
356365
"lines": numbered_lines,
357366
"line_count": len(lines),
358367
"is_binary": is_binary,
359368
"language": ext,
369
+ "can_render": can_render,
370
+ "view_mode": view_mode,
371
+ "rendered_html": rendered_html,
360372
"active_tab": "code",
361373
},
362374
)
363375
364376
@@ -810,10 +822,33 @@
810822
"activity": activity,
811823
"active_tab": "timeline",
812824
},
813825
)
814826
827
+
828
+# --- Branches ---
829
+
830
+
831
+@login_required
832
+def branch_list(request, slug):
833
+ P.PROJECT_VIEW.check(request.user)
834
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
835
+
836
+ with reader:
837
+ branches = reader.get_branches()
838
+
839
+ return render(
840
+ request,
841
+ "fossil/branch_list.html",
842
+ {
843
+ "project": project,
844
+ "fossil_repo": fossil_repo,
845
+ "branches": branches,
846
+ "active_tab": "code",
847
+ },
848
+ )
849
+
815850
816851
# --- Fossil Docs ---
817852
818853
FOSSIL_SCM_SLUG = "fossil-scm"
819854
820855
--- fossil/views.py
+++ fossil/views.py
@@ -339,13 +339,22 @@
339 for i, part in enumerate(parts):
340 file_breadcrumbs.append({"name": part, "path": "/".join(parts[: i + 1])})
341
342 # Split into lines for line-number display
343 lines = content.split("\n") if not is_binary else []
344 # Enumerate for template (1-indexed)
345 numbered_lines = [{"num": i + 1, "text": line} for i, line in enumerate(lines)]
346
 
 
 
 
 
 
 
 
 
 
347 return render(
348 request,
349 "fossil/code_file.html",
350 {
351 "project": project,
@@ -355,10 +364,13 @@
355 "content": content,
356 "lines": numbered_lines,
357 "line_count": len(lines),
358 "is_binary": is_binary,
359 "language": ext,
 
 
 
360 "active_tab": "code",
361 },
362 )
363
364
@@ -810,10 +822,33 @@
810 "activity": activity,
811 "active_tab": "timeline",
812 },
813 )
814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
816 # --- Fossil Docs ---
817
818 FOSSIL_SCM_SLUG = "fossil-scm"
819
820
--- fossil/views.py
+++ fossil/views.py
@@ -339,13 +339,22 @@
339 for i, part in enumerate(parts):
340 file_breadcrumbs.append({"name": part, "path": "/".join(parts[: i + 1])})
341
342 # Split into lines for line-number display
343 lines = content.split("\n") if not is_binary else []
 
344 numbered_lines = [{"num": i + 1, "text": line} for i, line in enumerate(lines)]
345
346 # Check if file can be rendered (wiki, markdown, html)
347 can_render = ext in ("wiki", "md", "markdown", "html", "htm")
348 view_mode = request.GET.get("mode", "source")
349 rendered_html = ""
350 if can_render and view_mode == "rendered" and not is_binary:
351 doc_base = "/".join(filepath.split("/")[:-1])
352 if doc_base:
353 doc_base += "/"
354 rendered_html = mark_safe(_render_fossil_content(content, project_slug=slug, base_path=doc_base))
355
356 return render(
357 request,
358 "fossil/code_file.html",
359 {
360 "project": project,
@@ -355,10 +364,13 @@
364 "content": content,
365 "lines": numbered_lines,
366 "line_count": len(lines),
367 "is_binary": is_binary,
368 "language": ext,
369 "can_render": can_render,
370 "view_mode": view_mode,
371 "rendered_html": rendered_html,
372 "active_tab": "code",
373 },
374 )
375
376
@@ -810,10 +822,33 @@
822 "activity": activity,
823 "active_tab": "timeline",
824 },
825 )
826
827
828 # --- Branches ---
829
830
831 @login_required
832 def branch_list(request, slug):
833 P.PROJECT_VIEW.check(request.user)
834 project, fossil_repo, reader = _get_repo_and_reader(slug)
835
836 with reader:
837 branches = reader.get_branches()
838
839 return render(
840 request,
841 "fossil/branch_list.html",
842 {
843 "project": project,
844 "fossil_repo": fossil_repo,
845 "branches": branches,
846 "active_tab": "code",
847 },
848 )
849
850
851 # --- Fossil Docs ---
852
853 FOSSIL_SCM_SLUG = "fossil-scm"
854
855
--- templates/fossil/_project_nav.html
+++ templates/fossil/_project_nav.html
@@ -4,10 +4,14 @@
44
Overview
55
</a>
66
<a href="{% url 'fossil:code' slug=project.slug %}"
77
class="px-4 py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'code' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50{% endif %}">
88
Code
9
+ </a>
10
+ <a href="{% url 'fossil:branches' slug=project.slug %}"
11
+ class="px-4 py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'branches' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50{% endif %}">
12
+ Branches
913
</a>
1014
<a href="{% url 'fossil:timeline' slug=project.slug %}"
1115
class="px-4 py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'timeline' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50{% endif %}">
1216
Timeline
1317
</a>
1418
1519
ADDED templates/fossil/branch_list.html
--- templates/fossil/_project_nav.html
+++ templates/fossil/_project_nav.html
@@ -4,10 +4,14 @@
4 Overview
5 </a>
6 <a href="{% url 'fossil:code' slug=project.slug %}"
7 class="px-4 py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'code' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50{% endif %}">
8 Code
 
 
 
 
9 </a>
10 <a href="{% url 'fossil:timeline' slug=project.slug %}"
11 class="px-4 py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'timeline' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50{% endif %}">
12 Timeline
13 </a>
14
15 DDED templates/fossil/branch_list.html
--- templates/fossil/_project_nav.html
+++ templates/fossil/_project_nav.html
@@ -4,10 +4,14 @@
4 Overview
5 </a>
6 <a href="{% url 'fossil:code' slug=project.slug %}"
7 class="px-4 py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'code' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50{% endif %}">
8 Code
9 </a>
10 <a href="{% url 'fossil:branches' slug=project.slug %}"
11 class="px-4 py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'branches' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50{% endif %}">
12 Branches
13 </a>
14 <a href="{% url 'fossil:timeline' slug=project.slug %}"
15 class="px-4 py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'timeline' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50{% endif %}">
16 Timeline
17 </a>
18
19 DDED templates/fossil/branch_list.html
--- a/templates/fossil/branch_list.html
+++ b/templates/fossil/branch_list.html
@@ -0,0 +1,14 @@
1
+{% extends "base.html" %}
2
+{% load fossil_filters %}
3
+{% block title %}Branches — {{ project.name }} — Fossilrepo{% endblock %}
4
+
5
+{% block content %}
6
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7
+ble">
8
+ <table class="min-w-full divide-y divide-gray-700">
9
+ thead class="bg-gray-900/80">
10
+ <tr>
11
+ <th class="px-4 py-3 text-left textext-gray-400">Branch</th>
12
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Last Checkin</th>
13
+ <th class="px-4 py-3 text-left text uppercase tracking-wider text-gray-400">By</th>
14
+ <th class="px-4No branches.</tdss="px-4endblock %}
--- a/templates/fossil/branch_list.html
+++ b/templates/fossil/branch_list.html
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/branch_list.html
+++ b/templates/fossil/branch_list.html
@@ -0,0 +1,14 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Branches — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block content %}
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7 ble">
8 <table class="min-w-full divide-y divide-gray-700">
9 thead class="bg-gray-900/80">
10 <tr>
11 <th class="px-4 py-3 text-left textext-gray-400">Branch</th>
12 <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Last Checkin</th>
13 <th class="px-4 py-3 text-left text uppercase tracking-wider text-gray-400">By</th>
14 <th class="px-4No branches.</tdss="px-4endblock %}
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -55,17 +55,31 @@
5555
{% else %}
5656
<a href="{% url 'fossil:code_dir' slug=project.slug dirpath=crumb.path %}" class="text-brand-light hover:text-brand">{{ crumb.name }}</a>
5757
{% endif %}
5858
{% endfor %}
5959
</div>
60
- {% if not is_binary %}
61
- <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
62
- {% endif %}
60
+ <div class="flex items-center gap-3">
61
+ {% if can_render %}
62
+ <div class="flex items-center gap-1 text-xs">
63
+ <a href="?mode=source" class="px-2 py-1 rounded {% if view_mode == 'source' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Source</a>
64
+ <a href="?mode=rendered" class="px-2 py-1 rounded {% if view_mode == 'rendered' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Rendered</a>
65
+ </div>
66
+ {% endif %}
67
+ {% if not is_binary %}
68
+ <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
69
+ {% endif %}
70
+ </div>
6371
</div>
6472
<div class="overflow-x-auto">
6573
{% if is_binary %}
6674
<p class="p-4 text-sm text-gray-500">{{ content }}</p>
75
+ {% elif view_mode == "rendered" and rendered_html %}
76
+ <div class="px-6 py-6">
77
+ <div class="prose prose-invert prose-gray max-w-none">
78
+ {{ rendered_html }}
79
+ </div>
80
+ </div>
6781
{% else %}
6882
<table class="code-table">
6983
<tbody>
7084
{% for line in lines %}
7185
<tr class="line-row" id="L{{ line.num }}">
7286
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -55,17 +55,31 @@
55 {% else %}
56 <a href="{% url 'fossil:code_dir' slug=project.slug dirpath=crumb.path %}" class="text-brand-light hover:text-brand">{{ crumb.name }}</a>
57 {% endif %}
58 {% endfor %}
59 </div>
60 {% if not is_binary %}
61 <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
62 {% endif %}
 
 
 
 
 
 
 
 
63 </div>
64 <div class="overflow-x-auto">
65 {% if is_binary %}
66 <p class="p-4 text-sm text-gray-500">{{ content }}</p>
 
 
 
 
 
 
67 {% else %}
68 <table class="code-table">
69 <tbody>
70 {% for line in lines %}
71 <tr class="line-row" id="L{{ line.num }}">
72
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -55,17 +55,31 @@
55 {% else %}
56 <a href="{% url 'fossil:code_dir' slug=project.slug dirpath=crumb.path %}" class="text-brand-light hover:text-brand">{{ crumb.name }}</a>
57 {% endif %}
58 {% endfor %}
59 </div>
60 <div class="flex items-center gap-3">
61 {% if can_render %}
62 <div class="flex items-center gap-1 text-xs">
63 <a href="?mode=source" class="px-2 py-1 rounded {% if view_mode == 'source' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Source</a>
64 <a href="?mode=rendered" class="px-2 py-1 rounded {% if view_mode == 'rendered' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Rendered</a>
65 </div>
66 {% endif %}
67 {% if not is_binary %}
68 <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
69 {% endif %}
70 </div>
71 </div>
72 <div class="overflow-x-auto">
73 {% if is_binary %}
74 <p class="p-4 text-sm text-gray-500">{{ content }}</p>
75 {% elif view_mode == "rendered" and rendered_html %}
76 <div class="px-6 py-6">
77 <div class="prose prose-invert prose-gray max-w-none">
78 {{ rendered_html }}
79 </div>
80 </div>
81 {% else %}
82 <table class="code-table">
83 <tbody>
84 {% for line in lines %}
85 <tr class="line-row" id="L{{ line.num }}">
86

Keyboard Shortcuts

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