FossilRepo

Add tags, raw download, repo statistics, fix external forum links Tags: - New /fossil/tags/ view listing all version tags with checkin links Raw file download: - /fossil/code/raw/<path> serves file as octet-stream attachment - "Raw" button on code file viewer header Repository statistics: - /fossil/stats/ with comprehensive stats dashboard - Stats cards: checkins, contributors, artifacts, repo size - 52-week activity chart (Chart.js) - Event breakdown: checkins, wiki, tickets, forum counts - Top contributors with progress bars - First/last checkin dates Link fixes: - Only rewrite fossil-scm.org/home links (source repo), not /forum - External forum links stay external (separate Fossil instance)

lmata 2026-04-07 00:21 trunk
Commit 6140f5bde8f73752d64bbbaad647a7a51afcc89218ea39128c5ec705e834b5d1
--- fossil/reader.py
+++ fossil/reader.py
@@ -402,10 +402,70 @@
402402
}
403403
)
404404
except sqlite3.OperationalError:
405405
pass
406406
return branches
407
+
408
+ def get_tags(self) -> list[dict]:
409
+ """Get all tags (non-branch sym- tags that mark specific checkins)."""
410
+ tags = []
411
+ try:
412
+ rows = self.conn.execute(
413
+ """
414
+ SELECT tag.tagname, event.mtime, event.user, blob.uuid
415
+ FROM tag
416
+ JOIN tagxref ON tag.tagid = tagxref.tagid AND tagxref.value > 0
417
+ JOIN event ON tagxref.rid = event.objid
418
+ JOIN blob ON event.objid = blob.rid
419
+ WHERE tag.tagname LIKE 'sym-%'
420
+ AND tag.tagname NOT IN (SELECT tagname FROM tag JOIN tagxref ON tag.tagid=tagxref.tagid GROUP BY tagname HAVING count(*) > 5)
421
+ ORDER BY event.mtime DESC
422
+ LIMIT 100
423
+ """,
424
+ ).fetchall()
425
+ for r in rows:
426
+ tags.append(
427
+ {
428
+ "name": r["tagname"].replace("sym-", "", 1),
429
+ "timestamp": _julian_to_datetime(r["mtime"]),
430
+ "user": r["user"] or "",
431
+ "uuid": r["uuid"],
432
+ }
433
+ )
434
+ except sqlite3.OperationalError:
435
+ pass
436
+ return tags
437
+
438
+ def get_repo_statistics(self) -> dict:
439
+ """Get comprehensive repository statistics."""
440
+ stats = {}
441
+ try:
442
+ stats["total_artifacts"] = self.conn.execute("SELECT count(*) FROM blob").fetchone()[0]
443
+ stats["total_events"] = self.conn.execute("SELECT count(*) FROM event").fetchone()[0]
444
+ stats["checkin_count"] = self.conn.execute("SELECT count(*) FROM event WHERE type='ci'").fetchone()[0]
445
+ stats["wiki_events"] = self.conn.execute("SELECT count(*) FROM event WHERE type='w'").fetchone()[0]
446
+ stats["ticket_events"] = self.conn.execute("SELECT count(*) FROM event WHERE type='t'").fetchone()[0]
447
+ stats["forum_events"] = self.conn.execute("SELECT count(*) FROM event WHERE type='f'").fetchone()[0]
448
+
449
+ # First and last checkin dates
450
+ first = self.conn.execute("SELECT min(mtime) FROM event WHERE type='ci'").fetchone()
451
+ last = self.conn.execute("SELECT max(mtime) FROM event WHERE type='ci'").fetchone()
452
+ if first and first[0]:
453
+ stats["first_checkin"] = _julian_to_datetime(first[0])
454
+ if last and last[0]:
455
+ stats["last_checkin"] = _julian_to_datetime(last[0])
456
+
457
+ # Unique contributors
458
+ stats["contributors"] = self.conn.execute("SELECT count(DISTINCT user) FROM event WHERE type='ci'").fetchone()[0]
459
+
460
+ # DB size
461
+ stats["db_pages"] = self.conn.execute("PRAGMA page_count").fetchone()[0]
462
+ stats["page_size"] = self.conn.execute("PRAGMA page_size").fetchone()[0]
463
+ stats["db_size_mb"] = round((stats["db_pages"] * stats["page_size"]) / (1024 * 1024), 1)
464
+ except sqlite3.OperationalError:
465
+ pass
466
+ return stats
407467
408468
# --- Timeline ---
409469
410470
def get_timeline(self, limit: int = 50, offset: int = 0, event_type: str | None = None) -> list[TimelineEntry]:
411471
sql = """
412472
--- fossil/reader.py
+++ fossil/reader.py
@@ -402,10 +402,70 @@
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/reader.py
+++ fossil/reader.py
@@ -402,10 +402,70 @@
402 }
403 )
404 except sqlite3.OperationalError:
405 pass
406 return branches
407
408 def get_tags(self) -> list[dict]:
409 """Get all tags (non-branch sym- tags that mark specific checkins)."""
410 tags = []
411 try:
412 rows = self.conn.execute(
413 """
414 SELECT tag.tagname, event.mtime, event.user, blob.uuid
415 FROM tag
416 JOIN tagxref ON tag.tagid = tagxref.tagid AND tagxref.value > 0
417 JOIN event ON tagxref.rid = event.objid
418 JOIN blob ON event.objid = blob.rid
419 WHERE tag.tagname LIKE 'sym-%'
420 AND tag.tagname NOT IN (SELECT tagname FROM tag JOIN tagxref ON tag.tagid=tagxref.tagid GROUP BY tagname HAVING count(*) > 5)
421 ORDER BY event.mtime DESC
422 LIMIT 100
423 """,
424 ).fetchall()
425 for r in rows:
426 tags.append(
427 {
428 "name": r["tagname"].replace("sym-", "", 1),
429 "timestamp": _julian_to_datetime(r["mtime"]),
430 "user": r["user"] or "",
431 "uuid": r["uuid"],
432 }
433 )
434 except sqlite3.OperationalError:
435 pass
436 return tags
437
438 def get_repo_statistics(self) -> dict:
439 """Get comprehensive repository statistics."""
440 stats = {}
441 try:
442 stats["total_artifacts"] = self.conn.execute("SELECT count(*) FROM blob").fetchone()[0]
443 stats["total_events"] = self.conn.execute("SELECT count(*) FROM event").fetchone()[0]
444 stats["checkin_count"] = self.conn.execute("SELECT count(*) FROM event WHERE type='ci'").fetchone()[0]
445 stats["wiki_events"] = self.conn.execute("SELECT count(*) FROM event WHERE type='w'").fetchone()[0]
446 stats["ticket_events"] = self.conn.execute("SELECT count(*) FROM event WHERE type='t'").fetchone()[0]
447 stats["forum_events"] = self.conn.execute("SELECT count(*) FROM event WHERE type='f'").fetchone()[0]
448
449 # First and last checkin dates
450 first = self.conn.execute("SELECT min(mtime) FROM event WHERE type='ci'").fetchone()
451 last = self.conn.execute("SELECT max(mtime) FROM event WHERE type='ci'").fetchone()
452 if first and first[0]:
453 stats["first_checkin"] = _julian_to_datetime(first[0])
454 if last and last[0]:
455 stats["last_checkin"] = _julian_to_datetime(last[0])
456
457 # Unique contributors
458 stats["contributors"] = self.conn.execute("SELECT count(DISTINCT user) FROM event WHERE type='ci'").fetchone()[0]
459
460 # DB size
461 stats["db_pages"] = self.conn.execute("PRAGMA page_count").fetchone()[0]
462 stats["page_size"] = self.conn.execute("PRAGMA page_size").fetchone()[0]
463 stats["db_size_mb"] = round((stats["db_pages"] * stats["page_size"]) / (1024 * 1024), 1)
464 except sqlite3.OperationalError:
465 pass
466 return stats
467
468 # --- Timeline ---
469
470 def get_timeline(self, limit: int = 50, offset: int = 0, event_type: str | None = None) -> list[TimelineEntry]:
471 sql = """
472
--- fossil/urls.py
+++ fossil/urls.py
@@ -19,10 +19,13 @@
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"),
2323
path("branches/", views.branch_list, name="branches"),
24
+ path("tags/", views.tag_list, name="tags"),
2425
path("search/", views.search, name="search"),
26
+ path("stats/", views.repo_stats, name="stats"),
27
+ path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
2528
path("code/history/<path:filepath>", views.file_history, name="file_history"),
2629
path("docs/", views.fossil_docs, name="docs"),
2730
path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
2831
]
2932
--- fossil/urls.py
+++ fossil/urls.py
@@ -19,10 +19,13 @@
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("search/", views.search, name="search"),
 
 
25 path("code/history/<path:filepath>", views.file_history, name="file_history"),
26 path("docs/", views.fossil_docs, name="docs"),
27 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
28 ]
29
--- fossil/urls.py
+++ fossil/urls.py
@@ -19,10 +19,13 @@
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("tags/", views.tag_list, name="tags"),
25 path("search/", views.search, name="search"),
26 path("stats/", views.repo_stats, name="stats"),
27 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
28 path("code/history/<path:filepath>", views.file_history, name="file_history"),
29 path("docs/", views.fossil_docs, name="docs"),
30 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
31 ]
32
+81 -6
--- fossil/views.py
+++ fossil/views.py
@@ -243,28 +243,29 @@
243243
# Rewrite href="/..." links (internal Fossil paths)
244244
html = re.sub(r'href="(/[^"]*)"', replace_link, html)
245245
# Rewrite Fossil URI schemes: forum:/..., info:..., wiki:...
246246
html = re.sub(r'href="(forum|info|wiki):([^"]*)"', replace_scheme_link, html)
247247
248
- # Rewrite external fossil-scm.org links to local views
248
+ # Rewrite external fossil-scm.org/home links (source repo) to local views
249
+ # Do NOT rewrite fossil-scm.org/forum links — those are a separate repo/instance
249250
def replace_external_fossil(match):
250251
path = match.group(1)
251
- # /forum/forumpost/HASH
252
- m = re.match(r"/forum/forumpost/([0-9a-f]+)", path)
253
- if m:
254
- return f'href="{base}/forum/{m.group(1)}/"'
255252
# /info/HASH
256253
m = re.match(r"/info/([0-9a-f]+)", path)
257254
if m:
258255
return f'href="{base}/checkin/{m.group(1)}/"'
259256
# /wiki/PageName
260257
m = re.match(r"/wiki/(.+)", path)
261258
if m:
262259
return f'href="{base}/wiki/page/{m.group(1)}"'
260
+ # /doc/trunk/www/file -> docs
261
+ m = re.match(r"/doc/(?:trunk|tip|[^/]+)/(.+)", path)
262
+ if m:
263
+ return f'href="{base}/docs/{m.group(1)}"'
263264
return match.group(0)
264265
265
- html = re.sub(r'href="https?://(?:www\.)?fossil-scm\.org(?:/home)?(/[^"]*)"', replace_external_fossil, html)
266
+ html = re.sub(r'href="https?://(?:www\.)?fossil-scm\.org/home(/[^"]*)"', replace_external_fossil, html)
266267
return html
267268
268269
269270
def _get_repo_and_reader(slug):
270271
"""Return (project, fossil_repo, reader) or raise 404."""
@@ -933,10 +934,84 @@
933934
"branches": branches,
934935
"active_tab": "code",
935936
},
936937
)
937938
939
+
940
+# --- Tags ---
941
+
942
+
943
+@login_required
944
+def tag_list(request, slug):
945
+ P.PROJECT_VIEW.check(request.user)
946
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
947
+
948
+ with reader:
949
+ tags = reader.get_tags()
950
+
951
+ return render(
952
+ request,
953
+ "fossil/tag_list.html",
954
+ {"project": project, "tags": tags, "active_tab": "code"},
955
+ )
956
+
957
+
958
+# --- Raw File Download ---
959
+
960
+
961
+@login_required
962
+def code_raw(request, slug, filepath):
963
+ P.PROJECT_VIEW.check(request.user)
964
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
965
+
966
+ with reader:
967
+ checkin_uuid = reader.get_latest_checkin_uuid()
968
+ files = reader.get_files_at_checkin(checkin_uuid) if checkin_uuid else []
969
+ target = None
970
+ for f in files:
971
+ if f.name == filepath:
972
+ target = f
973
+ break
974
+ if not target:
975
+ raise Http404(f"File not found: {filepath}")
976
+ content_bytes = reader.get_file_content(target.uuid)
977
+
978
+ from django.http import HttpResponse as DjHttpResponse
979
+
980
+ filename = filepath.split("/")[-1]
981
+ response = DjHttpResponse(content_bytes, content_type="application/octet-stream")
982
+ response["Content-Disposition"] = f'attachment; filename="{filename}"'
983
+ return response
984
+
985
+
986
+# --- Repository Statistics ---
987
+
988
+
989
+@login_required
990
+def repo_stats(request, slug):
991
+ P.PROJECT_VIEW.check(request.user)
992
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
993
+
994
+ with reader:
995
+ stats = reader.get_repo_statistics()
996
+ top_contributors = reader.get_top_contributors(limit=15)
997
+ activity = reader.get_commit_activity(weeks=52)
998
+
999
+ import json
1000
+
1001
+ return render(
1002
+ request,
1003
+ "fossil/repo_stats.html",
1004
+ {
1005
+ "project": project,
1006
+ "stats": stats,
1007
+ "top_contributors": top_contributors,
1008
+ "activity_json": json.dumps([c["count"] for c in activity]),
1009
+ "active_tab": "code",
1010
+ },
1011
+ )
1012
+
9381013
9391014
# --- Fossil Docs ---
9401015
9411016
FOSSIL_SCM_SLUG = "fossil-scm"
9421017
9431018
--- fossil/views.py
+++ fossil/views.py
@@ -243,28 +243,29 @@
243 # Rewrite href="/..." links (internal Fossil paths)
244 html = re.sub(r'href="(/[^"]*)"', replace_link, html)
245 # Rewrite Fossil URI schemes: forum:/..., info:..., wiki:...
246 html = re.sub(r'href="(forum|info|wiki):([^"]*)"', replace_scheme_link, html)
247
248 # Rewrite external fossil-scm.org links to local views
 
249 def replace_external_fossil(match):
250 path = match.group(1)
251 # /forum/forumpost/HASH
252 m = re.match(r"/forum/forumpost/([0-9a-f]+)", path)
253 if m:
254 return f'href="{base}/forum/{m.group(1)}/"'
255 # /info/HASH
256 m = re.match(r"/info/([0-9a-f]+)", path)
257 if m:
258 return f'href="{base}/checkin/{m.group(1)}/"'
259 # /wiki/PageName
260 m = re.match(r"/wiki/(.+)", path)
261 if m:
262 return f'href="{base}/wiki/page/{m.group(1)}"'
 
 
 
 
263 return match.group(0)
264
265 html = re.sub(r'href="https?://(?:www\.)?fossil-scm\.org(?:/home)?(/[^"]*)"', replace_external_fossil, html)
266 return html
267
268
269 def _get_repo_and_reader(slug):
270 """Return (project, fossil_repo, reader) or raise 404."""
@@ -933,10 +934,84 @@
933 "branches": branches,
934 "active_tab": "code",
935 },
936 )
937
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938
939 # --- Fossil Docs ---
940
941 FOSSIL_SCM_SLUG = "fossil-scm"
942
943
--- fossil/views.py
+++ fossil/views.py
@@ -243,28 +243,29 @@
243 # Rewrite href="/..." links (internal Fossil paths)
244 html = re.sub(r'href="(/[^"]*)"', replace_link, html)
245 # Rewrite Fossil URI schemes: forum:/..., info:..., wiki:...
246 html = re.sub(r'href="(forum|info|wiki):([^"]*)"', replace_scheme_link, html)
247
248 # Rewrite external fossil-scm.org/home links (source repo) to local views
249 # Do NOT rewrite fossil-scm.org/forum links — those are a separate repo/instance
250 def replace_external_fossil(match):
251 path = match.group(1)
 
 
 
 
252 # /info/HASH
253 m = re.match(r"/info/([0-9a-f]+)", path)
254 if m:
255 return f'href="{base}/checkin/{m.group(1)}/"'
256 # /wiki/PageName
257 m = re.match(r"/wiki/(.+)", path)
258 if m:
259 return f'href="{base}/wiki/page/{m.group(1)}"'
260 # /doc/trunk/www/file -> docs
261 m = re.match(r"/doc/(?:trunk|tip|[^/]+)/(.+)", path)
262 if m:
263 return f'href="{base}/docs/{m.group(1)}"'
264 return match.group(0)
265
266 html = re.sub(r'href="https?://(?:www\.)?fossil-scm\.org/home(/[^"]*)"', replace_external_fossil, html)
267 return html
268
269
270 def _get_repo_and_reader(slug):
271 """Return (project, fossil_repo, reader) or raise 404."""
@@ -933,10 +934,84 @@
934 "branches": branches,
935 "active_tab": "code",
936 },
937 )
938
939
940 # --- Tags ---
941
942
943 @login_required
944 def tag_list(request, slug):
945 P.PROJECT_VIEW.check(request.user)
946 project, fossil_repo, reader = _get_repo_and_reader(slug)
947
948 with reader:
949 tags = reader.get_tags()
950
951 return render(
952 request,
953 "fossil/tag_list.html",
954 {"project": project, "tags": tags, "active_tab": "code"},
955 )
956
957
958 # --- Raw File Download ---
959
960
961 @login_required
962 def code_raw(request, slug, filepath):
963 P.PROJECT_VIEW.check(request.user)
964 project, fossil_repo, reader = _get_repo_and_reader(slug)
965
966 with reader:
967 checkin_uuid = reader.get_latest_checkin_uuid()
968 files = reader.get_files_at_checkin(checkin_uuid) if checkin_uuid else []
969 target = None
970 for f in files:
971 if f.name == filepath:
972 target = f
973 break
974 if not target:
975 raise Http404(f"File not found: {filepath}")
976 content_bytes = reader.get_file_content(target.uuid)
977
978 from django.http import HttpResponse as DjHttpResponse
979
980 filename = filepath.split("/")[-1]
981 response = DjHttpResponse(content_bytes, content_type="application/octet-stream")
982 response["Content-Disposition"] = f'attachment; filename="{filename}"'
983 return response
984
985
986 # --- Repository Statistics ---
987
988
989 @login_required
990 def repo_stats(request, slug):
991 P.PROJECT_VIEW.check(request.user)
992 project, fossil_repo, reader = _get_repo_and_reader(slug)
993
994 with reader:
995 stats = reader.get_repo_statistics()
996 top_contributors = reader.get_top_contributors(limit=15)
997 activity = reader.get_commit_activity(weeks=52)
998
999 import json
1000
1001 return render(
1002 request,
1003 "fossil/repo_stats.html",
1004 {
1005 "project": project,
1006 "stats": stats,
1007 "top_contributors": top_contributors,
1008 "activity_json": json.dumps([c["count"] for c in activity]),
1009 "active_tab": "code",
1010 },
1011 )
1012
1013
1014 # --- Fossil Docs ---
1015
1016 FOSSIL_SCM_SLUG = "fossil-scm"
1017
1018
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -63,10 +63,11 @@
6363
<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>
6464
<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>
6565
</div>
6666
{% endif %}
6767
<a href="{% url 'fossil:file_history' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">History</a>
68
+ <a href="{% url 'fossil:code_raw' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">Raw</a>
6869
{% if not is_binary %}
6970
<span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
7071
{% endif %}
7172
</div>
7273
</div>
7374
7475
ADDED templates/fossil/repo_stats.html
7576
ADDED templates/fossil/tag_list.html
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -63,10 +63,11 @@
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 <a href="{% url 'fossil:file_history' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">History</a>
 
68 {% if not is_binary %}
69 <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
70 {% endif %}
71 </div>
72 </div>
73
74 DDED templates/fossil/repo_stats.html
75 DDED templates/fossil/tag_list.html
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -63,10 +63,11 @@
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 <a href="{% url 'fossil:file_history' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">History</a>
68 <a href="{% url 'fossil:code_raw' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">Raw</a>
69 {% if not is_binary %}
70 <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
71 {% endif %}
72 </div>
73 </div>
74
75 DDED templates/fossil/repo_stats.html
76 DDED templates/fossil/tag_list.html
--- a/templates/fossil/repo_stats.html
+++ b/templates/fossil/repo_stats.html
@@ -0,0 +1,76 @@
1
+{% extends "base.html" %}
2
+{% load fossil_filters %}
3
+{% block title %}Statistics — {{ project.name }} — Fossilrepo{% endblock %}
4
+
5
+{% block extra_head %}
6
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.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="grid grid-cols-2 gap-4 sm:grid-cols-4 mb-6">
14
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
15
+ <div class="text-2xl font-bold text-gray-100">{{ stats.checkin_count|default:"0" }}</div>
16
+ <div class="text-xs text-gray-500 mt-1">Checkins</div>
17
+ </div>
18
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
19
+ <div class="text-2xl font-bold text-gray-100">{{ stats.contributors|default:"0" }}</div>
20
+ <div class="text-xs text-gray-500 mt-1">Contributors</div>
21
+ </div>
22
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
23
+ <div class="text-2xl font-bold text-gray-100">{{ stats.total_artifacts|default:"0" }}</div>
24
+ <div class="text-xs text-gray-500 mt-1">Artifacts</div>
25
+ </div>
26
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
27
+ <div class="text-2xl font-bold text-gray-100">{{ stats.db_size_mb|default:"0" }} MB</div>
28
+ <div class="text-xs text-gray-500 mt-1">Repository Size</div>
29
+ </div>
30
+</div>
31
+
32
+{% if activity_json %}
33
+<div class="rounded-lg bg-gray-800 border border-gray-700 p-4 mb-6">
34
+ <h3 class="text-sm font-medium text-gray-300 mb-3">Commit Activity (52 weeks)</h3>
35
+ <div style="height: 160px;">
36
+ <canvas id="statsChart"></canvas>
37
+ </div>
38
+</div>
39
+{% endif %}
40
+
41
+<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
42
+ <!-- Event breakdown -->
43
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
44
+ <h3 class="text-sm font-medium text-gray-300 mb-3">Event Breakdown</h3>
45
+ <div class="space-y-2">
46
+ <div class="flex items-center justify-between">
47
+ <span class="text-sm text-gray-400">Checkins</span>
48
+ <span class="text-sm font-medium text-gray-200">{{ stats.checkin_count|def class="text-xs t>
49
+ <span class="text-sm text-gray-400">Wiki edits</span>
50
+ <span class="text-sm font-medium text-gray-200">{{ stats.wiki_events|default:"0" }}</span>
51
+ </div>
52
+ <div class="flex items-center justify-between">
53
+ <span class="text-sm text-gray-400">Ticket changes</span>
54
+ <span class="text-sm font-medium text-gray-200">{{ stats.ticket_events|default:"0" }}</span>
55
+ </div>
56
+ <div class="flex items-center justify-between">
57
+ <span class="text-sm text-gray-400">Forum posts</span>
58
+ <span class="text-sm font-medium text-gray-200">{{ stats.forum_events|default:"0" }}</span>
59
+ </div>
60
+ {% if stats.first_checkin %}
61
+ <div class="flex items-center justify-between pt-2 border-t border-gray-700">
62
+ <span class="text-sm text-gray-500">First checkin</span>
63
+ <span class="text-sm text-gray-400">{{ stats.first_checkin|date:"Y-m-d" }}</span>
64
+ </div>
65
+ {% endif %}
66
+ {% if stats.last_checkin %}
67
+ <div class="flex items-center justify-between">
68
+ <span class="text-sm text-gray-500">Last checkin</span>
69
+ <span class="text-sm text-gray-400">{{ stats.last_checkin|date:"Y-m-d" }}</span>
70
+ </div>
71
+ {% endif %}
72
+ </div>
73
+ </div>
74
+
75
+ <!-- Top contributors -->
76
+ <div class="rounded
--- a/templates/fossil/repo_stats.html
+++ b/templates/fossil/repo_stats.html
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/repo_stats.html
+++ b/templates/fossil/repo_stats.html
@@ -0,0 +1,76 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Statistics — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block extra_head %}
6 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.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="grid grid-cols-2 gap-4 sm:grid-cols-4 mb-6">
14 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
15 <div class="text-2xl font-bold text-gray-100">{{ stats.checkin_count|default:"0" }}</div>
16 <div class="text-xs text-gray-500 mt-1">Checkins</div>
17 </div>
18 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
19 <div class="text-2xl font-bold text-gray-100">{{ stats.contributors|default:"0" }}</div>
20 <div class="text-xs text-gray-500 mt-1">Contributors</div>
21 </div>
22 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
23 <div class="text-2xl font-bold text-gray-100">{{ stats.total_artifacts|default:"0" }}</div>
24 <div class="text-xs text-gray-500 mt-1">Artifacts</div>
25 </div>
26 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
27 <div class="text-2xl font-bold text-gray-100">{{ stats.db_size_mb|default:"0" }} MB</div>
28 <div class="text-xs text-gray-500 mt-1">Repository Size</div>
29 </div>
30 </div>
31
32 {% if activity_json %}
33 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4 mb-6">
34 <h3 class="text-sm font-medium text-gray-300 mb-3">Commit Activity (52 weeks)</h3>
35 <div style="height: 160px;">
36 <canvas id="statsChart"></canvas>
37 </div>
38 </div>
39 {% endif %}
40
41 <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
42 <!-- Event breakdown -->
43 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
44 <h3 class="text-sm font-medium text-gray-300 mb-3">Event Breakdown</h3>
45 <div class="space-y-2">
46 <div class="flex items-center justify-between">
47 <span class="text-sm text-gray-400">Checkins</span>
48 <span class="text-sm font-medium text-gray-200">{{ stats.checkin_count|def class="text-xs t>
49 <span class="text-sm text-gray-400">Wiki edits</span>
50 <span class="text-sm font-medium text-gray-200">{{ stats.wiki_events|default:"0" }}</span>
51 </div>
52 <div class="flex items-center justify-between">
53 <span class="text-sm text-gray-400">Ticket changes</span>
54 <span class="text-sm font-medium text-gray-200">{{ stats.ticket_events|default:"0" }}</span>
55 </div>
56 <div class="flex items-center justify-between">
57 <span class="text-sm text-gray-400">Forum posts</span>
58 <span class="text-sm font-medium text-gray-200">{{ stats.forum_events|default:"0" }}</span>
59 </div>
60 {% if stats.first_checkin %}
61 <div class="flex items-center justify-between pt-2 border-t border-gray-700">
62 <span class="text-sm text-gray-500">First checkin</span>
63 <span class="text-sm text-gray-400">{{ stats.first_checkin|date:"Y-m-d" }}</span>
64 </div>
65 {% endif %}
66 {% if stats.last_checkin %}
67 <div class="flex items-center justify-between">
68 <span class="text-sm text-gray-500">Last checkin</span>
69 <span class="text-sm text-gray-400">{{ stats.last_checkin|date:"Y-m-d" }}</span>
70 </div>
71 {% endif %}
72 </div>
73 </div>
74
75 <!-- Top contributors -->
76 <div class="rounded
--- a/templates/fossil/tag_list.html
+++ b/templates/fossil/tag_list.html
@@ -0,0 +1,13 @@
1
+{% extends "base.html" %}
2
+{% load fossil_filters %}
3
+{% block title %}Tags — {{ project.name }} — Fossilrepo{% endblock %}
4
+
5
+{% block content %}
6
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7
+>
8
+ </div>
9
+</div>
10
+
11
+<div id="tag-content">
12
+<divNo tags.</tdcontent"
13
+ hxendblock %}
--- a/templates/fossil/tag_list.html
+++ b/templates/fossil/tag_list.html
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/tag_list.html
+++ b/templates/fossil/tag_list.html
@@ -0,0 +1,13 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Tags — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block content %}
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7 >
8 </div>
9 </div>
10
11 <div id="tag-content">
12 <divNo tags.</tdcontent"
13 hxendblock %}

Keyboard Shortcuts

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