FossilRepo

Add search, file history, README rendering Search: - New /fossil/search/ view with search across checkins, tickets, wiki - Full-text search on commit messages, ticket titles, wiki page names - Grouped results with clickable links File history: - New /fossil/code/history/<path> view showing all commits that touched a file - "History" button on file viewer header - Chronological commit list with hashes, users, timestamps README rendering: - Code browser auto-detects README.md/README/README.txt/README.wiki in the current directory - Renders below the file table like GitHub - Content processed through _render_fossil_content Code file viewer: - "History" link in the header bar

lmata 2026-04-07 00:18 trunk
Commit 4cf64702dd2452dff65f755c496297bf51ac989385514db5ceaa454e95b7a273
--- fossil/reader.py
+++ fossil/reader.py
@@ -652,10 +652,92 @@
652652
# Recursively resolve the source blob
653653
source = self._resolve_blob(delta_row["srcid"], by_rid=True)
654654
return _apply_fossil_delta(source, data)
655655
656656
return data
657
+
658
+ def get_file_history(self, filename: str, limit: int = 50) -> list[dict]:
659
+ """Get commit history for a specific file."""
660
+ history = []
661
+ try:
662
+ rows = self.conn.execute(
663
+ """
664
+ SELECT blob.uuid, event.mtime, event.user, event.comment
665
+ FROM mlink ml
666
+ JOIN filename fn ON ml.fnid = fn.fnid
667
+ JOIN event ON ml.mid = event.objid
668
+ JOIN blob ON event.objid = blob.rid
669
+ WHERE fn.name = ? AND event.type = 'ci'
670
+ ORDER BY event.mtime DESC
671
+ LIMIT ?
672
+ """,
673
+ (filename, limit),
674
+ ).fetchall()
675
+ for r in rows:
676
+ history.append(
677
+ {
678
+ "uuid": r["uuid"],
679
+ "timestamp": _julian_to_datetime(r["mtime"]),
680
+ "user": r["user"] or "",
681
+ "comment": r["comment"] or "",
682
+ }
683
+ )
684
+ except sqlite3.OperationalError:
685
+ pass
686
+ return history
687
+
688
+ def search(self, query: str, limit: int = 50) -> dict:
689
+ """Search across checkins, tickets, and wiki pages."""
690
+ results = {"checkins": [], "tickets": [], "wiki": []}
691
+ q = f"%{query}%"
692
+ try:
693
+ # Search checkin comments
694
+ rows = self.conn.execute(
695
+ "SELECT blob.uuid, event.mtime, event.user, event.comment FROM event "
696
+ "JOIN blob ON event.objid=blob.rid WHERE event.type='ci' AND event.comment LIKE ? "
697
+ "ORDER BY event.mtime DESC LIMIT ?",
698
+ (q, limit),
699
+ ).fetchall()
700
+ for r in rows:
701
+ results["checkins"].append(
702
+ {
703
+ "uuid": r["uuid"],
704
+ "timestamp": _julian_to_datetime(r["mtime"]),
705
+ "user": r["user"] or "",
706
+ "comment": r["comment"] or "",
707
+ }
708
+ )
709
+ except sqlite3.OperationalError:
710
+ pass
711
+ try:
712
+ # Search ticket titles
713
+ rows = self.conn.execute(
714
+ "SELECT tkt_uuid, title, status, tkt_ctime FROM ticket WHERE title LIKE ? ORDER BY tkt_ctime DESC LIMIT ?",
715
+ (q, limit),
716
+ ).fetchall()
717
+ for r in rows:
718
+ results["tickets"].append(
719
+ {
720
+ "uuid": r["tkt_uuid"],
721
+ "title": r["title"] or "",
722
+ "status": r["status"] or "",
723
+ "created": _julian_to_datetime(r["tkt_ctime"]) if r["tkt_ctime"] else None,
724
+ }
725
+ )
726
+ except sqlite3.OperationalError:
727
+ pass
728
+ try:
729
+ # Search wiki page names
730
+ rows = self.conn.execute(
731
+ "SELECT DISTINCT substr(tagname, 6) as name FROM tag WHERE tagname LIKE ? ORDER BY name LIMIT ?",
732
+ (f"wiki-%{query}%", limit),
733
+ ).fetchall()
734
+ for r in rows:
735
+ results["wiki"].append({"name": r["name"]})
736
+ except sqlite3.OperationalError:
737
+ pass
738
+ return results
657739
658740
# --- Tickets ---
659741
660742
def get_tickets(self, status: str | None = None, limit: int = 50) -> list[TicketEntry]:
661743
sql = "SELECT tkt_uuid, title, status, type, tkt_ctime, subsystem, priority FROM ticket"
662744
--- fossil/reader.py
+++ fossil/reader.py
@@ -652,10 +652,92 @@
652 # Recursively resolve the source blob
653 source = self._resolve_blob(delta_row["srcid"], by_rid=True)
654 return _apply_fossil_delta(source, data)
655
656 return data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
658 # --- Tickets ---
659
660 def get_tickets(self, status: str | None = None, limit: int = 50) -> list[TicketEntry]:
661 sql = "SELECT tkt_uuid, title, status, type, tkt_ctime, subsystem, priority FROM ticket"
662
--- fossil/reader.py
+++ fossil/reader.py
@@ -652,10 +652,92 @@
652 # Recursively resolve the source blob
653 source = self._resolve_blob(delta_row["srcid"], by_rid=True)
654 return _apply_fossil_delta(source, data)
655
656 return data
657
658 def get_file_history(self, filename: str, limit: int = 50) -> list[dict]:
659 """Get commit history for a specific file."""
660 history = []
661 try:
662 rows = self.conn.execute(
663 """
664 SELECT blob.uuid, event.mtime, event.user, event.comment
665 FROM mlink ml
666 JOIN filename fn ON ml.fnid = fn.fnid
667 JOIN event ON ml.mid = event.objid
668 JOIN blob ON event.objid = blob.rid
669 WHERE fn.name = ? AND event.type = 'ci'
670 ORDER BY event.mtime DESC
671 LIMIT ?
672 """,
673 (filename, limit),
674 ).fetchall()
675 for r in rows:
676 history.append(
677 {
678 "uuid": r["uuid"],
679 "timestamp": _julian_to_datetime(r["mtime"]),
680 "user": r["user"] or "",
681 "comment": r["comment"] or "",
682 }
683 )
684 except sqlite3.OperationalError:
685 pass
686 return history
687
688 def search(self, query: str, limit: int = 50) -> dict:
689 """Search across checkins, tickets, and wiki pages."""
690 results = {"checkins": [], "tickets": [], "wiki": []}
691 q = f"%{query}%"
692 try:
693 # Search checkin comments
694 rows = self.conn.execute(
695 "SELECT blob.uuid, event.mtime, event.user, event.comment FROM event "
696 "JOIN blob ON event.objid=blob.rid WHERE event.type='ci' AND event.comment LIKE ? "
697 "ORDER BY event.mtime DESC LIMIT ?",
698 (q, limit),
699 ).fetchall()
700 for r in rows:
701 results["checkins"].append(
702 {
703 "uuid": r["uuid"],
704 "timestamp": _julian_to_datetime(r["mtime"]),
705 "user": r["user"] or "",
706 "comment": r["comment"] or "",
707 }
708 )
709 except sqlite3.OperationalError:
710 pass
711 try:
712 # Search ticket titles
713 rows = self.conn.execute(
714 "SELECT tkt_uuid, title, status, tkt_ctime FROM ticket WHERE title LIKE ? ORDER BY tkt_ctime DESC LIMIT ?",
715 (q, limit),
716 ).fetchall()
717 for r in rows:
718 results["tickets"].append(
719 {
720 "uuid": r["tkt_uuid"],
721 "title": r["title"] or "",
722 "status": r["status"] or "",
723 "created": _julian_to_datetime(r["tkt_ctime"]) if r["tkt_ctime"] else None,
724 }
725 )
726 except sqlite3.OperationalError:
727 pass
728 try:
729 # Search wiki page names
730 rows = self.conn.execute(
731 "SELECT DISTINCT substr(tagname, 6) as name FROM tag WHERE tagname LIKE ? ORDER BY name LIMIT ?",
732 (f"wiki-%{query}%", limit),
733 ).fetchall()
734 for r in rows:
735 results["wiki"].append({"name": r["name"]})
736 except sqlite3.OperationalError:
737 pass
738 return results
739
740 # --- Tickets ---
741
742 def get_tickets(self, status: str | None = None, limit: int = 50) -> list[TicketEntry]:
743 sql = "SELECT tkt_uuid, title, status, type, tkt_ctime, subsystem, priority FROM ticket"
744
--- fossil/urls.py
+++ fossil/urls.py
@@ -19,8 +19,10 @@
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("search/", views.search, name="search"),
25
+ path("code/history/<path:filepath>", views.file_history, name="file_history"),
2426
path("docs/", views.fossil_docs, name="docs"),
2527
path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
2628
]
2729
--- fossil/urls.py
+++ fossil/urls.py
@@ -19,8 +19,10 @@
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
--- fossil/urls.py
+++ fossil/urls.py
@@ -19,8 +19,10 @@
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/views.py
+++ fossil/views.py
@@ -290,10 +290,29 @@
290290
metadata = reader.get_metadata()
291291
latest_commit = reader.get_timeline(limit=1, event_type="ci")
292292
293293
# Build directory listing for the current path
294294
tree = _build_file_tree(files, current_dir=dirpath)
295
+
296
+ # Check for README in current directory
297
+ readme_html = ""
298
+ prefix = (dirpath.strip("/") + "/") if dirpath else ""
299
+ for readme_name in ["README.md", "README", "README.txt", "README.wiki"]:
300
+ full_name = prefix + readme_name
301
+ for f in files:
302
+ if f.name == full_name:
303
+ with reader:
304
+ content_bytes = reader.get_file_content(f.uuid)
305
+ try:
306
+ readme_content = content_bytes.decode("utf-8")
307
+ doc_base = prefix if prefix else ""
308
+ readme_html = mark_safe(_render_fossil_content(readme_content, project_slug=slug, base_path=doc_base))
309
+ except (UnicodeDecodeError, Exception):
310
+ pass
311
+ break
312
+ if readme_html:
313
+ break
295314
296315
# Build breadcrumbs
297316
breadcrumbs = []
298317
if dirpath:
299318
parts = dirpath.strip("/").split("/")
@@ -313,10 +332,11 @@
313332
"current_dir": dirpath,
314333
"breadcrumbs": breadcrumbs,
315334
"checkin_uuid": checkin_uuid,
316335
"metadata": metadata,
317336
"latest_commit": latest_commit[0] if latest_commit else None,
337
+ "readme_html": readme_html,
318338
"active_tab": "code",
319339
},
320340
)
321341
322342
@@ -841,10 +861,59 @@
841861
"activity": activity,
842862
"active_tab": "timeline",
843863
},
844864
)
845865
866
+
867
+# --- Search ---
868
+
869
+
870
+@login_required
871
+def search(request, slug):
872
+ P.PROJECT_VIEW.check(request.user)
873
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
874
+
875
+ query = request.GET.get("q", "").strip()
876
+ results = None
877
+ if query:
878
+ with reader:
879
+ results = reader.search(query, limit=20)
880
+
881
+ return render(
882
+ request,
883
+ "fossil/search.html",
884
+ {
885
+ "project": project,
886
+ "query": query,
887
+ "results": results,
888
+ "active_tab": "code",
889
+ },
890
+ )
891
+
892
+
893
+# --- File History ---
894
+
895
+
896
+@login_required
897
+def file_history(request, slug, filepath):
898
+ P.PROJECT_VIEW.check(request.user)
899
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
900
+
901
+ with reader:
902
+ history = reader.get_file_history(filepath)
903
+
904
+ return render(
905
+ request,
906
+ "fossil/file_history.html",
907
+ {
908
+ "project": project,
909
+ "filepath": filepath,
910
+ "history": history,
911
+ "active_tab": "code",
912
+ },
913
+ )
914
+
846915
847916
# --- Branches ---
848917
849918
850919
@login_required
851920
--- fossil/views.py
+++ fossil/views.py
@@ -290,10 +290,29 @@
290 metadata = reader.get_metadata()
291 latest_commit = reader.get_timeline(limit=1, event_type="ci")
292
293 # Build directory listing for the current path
294 tree = _build_file_tree(files, current_dir=dirpath)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
296 # Build breadcrumbs
297 breadcrumbs = []
298 if dirpath:
299 parts = dirpath.strip("/").split("/")
@@ -313,10 +332,11 @@
313 "current_dir": dirpath,
314 "breadcrumbs": breadcrumbs,
315 "checkin_uuid": checkin_uuid,
316 "metadata": metadata,
317 "latest_commit": latest_commit[0] if latest_commit else None,
 
318 "active_tab": "code",
319 },
320 )
321
322
@@ -841,10 +861,59 @@
841 "activity": activity,
842 "active_tab": "timeline",
843 },
844 )
845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
847 # --- Branches ---
848
849
850 @login_required
851
--- fossil/views.py
+++ fossil/views.py
@@ -290,10 +290,29 @@
290 metadata = reader.get_metadata()
291 latest_commit = reader.get_timeline(limit=1, event_type="ci")
292
293 # Build directory listing for the current path
294 tree = _build_file_tree(files, current_dir=dirpath)
295
296 # Check for README in current directory
297 readme_html = ""
298 prefix = (dirpath.strip("/") + "/") if dirpath else ""
299 for readme_name in ["README.md", "README", "README.txt", "README.wiki"]:
300 full_name = prefix + readme_name
301 for f in files:
302 if f.name == full_name:
303 with reader:
304 content_bytes = reader.get_file_content(f.uuid)
305 try:
306 readme_content = content_bytes.decode("utf-8")
307 doc_base = prefix if prefix else ""
308 readme_html = mark_safe(_render_fossil_content(readme_content, project_slug=slug, base_path=doc_base))
309 except (UnicodeDecodeError, Exception):
310 pass
311 break
312 if readme_html:
313 break
314
315 # Build breadcrumbs
316 breadcrumbs = []
317 if dirpath:
318 parts = dirpath.strip("/").split("/")
@@ -313,10 +332,11 @@
332 "current_dir": dirpath,
333 "breadcrumbs": breadcrumbs,
334 "checkin_uuid": checkin_uuid,
335 "metadata": metadata,
336 "latest_commit": latest_commit[0] if latest_commit else None,
337 "readme_html": readme_html,
338 "active_tab": "code",
339 },
340 )
341
342
@@ -841,10 +861,59 @@
861 "activity": activity,
862 "active_tab": "timeline",
863 },
864 )
865
866
867 # --- Search ---
868
869
870 @login_required
871 def search(request, slug):
872 P.PROJECT_VIEW.check(request.user)
873 project, fossil_repo, reader = _get_repo_and_reader(slug)
874
875 query = request.GET.get("q", "").strip()
876 results = None
877 if query:
878 with reader:
879 results = reader.search(query, limit=20)
880
881 return render(
882 request,
883 "fossil/search.html",
884 {
885 "project": project,
886 "query": query,
887 "results": results,
888 "active_tab": "code",
889 },
890 )
891
892
893 # --- File History ---
894
895
896 @login_required
897 def file_history(request, slug, filepath):
898 P.PROJECT_VIEW.check(request.user)
899 project, fossil_repo, reader = _get_repo_and_reader(slug)
900
901 with reader:
902 history = reader.get_file_history(filepath)
903
904 return render(
905 request,
906 "fossil/file_history.html",
907 {
908 "project": project,
909 "filepath": filepath,
910 "history": history,
911 "active_tab": "code",
912 },
913 )
914
915
916 # --- Branches ---
917
918
919 @login_required
920
--- templates/fossil/code_browser.html
+++ templates/fossil/code_browser.html
@@ -44,6 +44,17 @@
4444
</div>
4545
4646
<!-- File table -->
4747
{% include "fossil/partials/file_tree.html" %}
4848
</div>
49
+
50
+{% if readme_html %}
51
+<div class="mt-4 rounded-lg bg-gray-800 border border-gray-700">
52
+ <div class="px-4 py-3 border-b border-gray-700 text-sm font-medium text-gray-300">README</div>
53
+ <div class="px-6 py-5">
54
+ <div class="prose prose-invert prose-gray max-w-none">
55
+ {{ readme_html }}
56
+ </div>
57
+ </div>
58
+</div>
59
+{% endif %}
4960
{% endblock %}
5061
--- templates/fossil/code_browser.html
+++ templates/fossil/code_browser.html
@@ -44,6 +44,17 @@
44 </div>
45
46 <!-- File table -->
47 {% include "fossil/partials/file_tree.html" %}
48 </div>
 
 
 
 
 
 
 
 
 
 
 
49 {% endblock %}
50
--- templates/fossil/code_browser.html
+++ templates/fossil/code_browser.html
@@ -44,6 +44,17 @@
44 </div>
45
46 <!-- File table -->
47 {% include "fossil/partials/file_tree.html" %}
48 </div>
49
50 {% if readme_html %}
51 <div class="mt-4 rounded-lg bg-gray-800 border border-gray-700">
52 <div class="px-4 py-3 border-b border-gray-700 text-sm font-medium text-gray-300">README</div>
53 <div class="px-6 py-5">
54 <div class="prose prose-invert prose-gray max-w-none">
55 {{ readme_html }}
56 </div>
57 </div>
58 </div>
59 {% endif %}
60 {% endblock %}
61
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -62,10 +62,11 @@
6262
<div class="flex items-center gap-1 text-xs">
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 %}
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>
6768
{% if not is_binary %}
6869
<span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
6970
{% endif %}
7071
</div>
7172
</div>
7273
7374
ADDED templates/fossil/file_history.html
7475
ADDED templates/fossil/search.html
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -62,10 +62,11 @@
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
73 DDED templates/fossil/file_history.html
74 DDED templates/fossil/search.html
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -62,10 +62,11 @@
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 <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/file_history.html
75 DDED templates/fossil/search.html
--- a/templates/fossil/file_history.html
+++ b/templates/fossil/file_history.html
@@ -0,0 +1,37 @@
1
+{% extends "base.html" %}
2
+{% load fossil_filters %}
3
+{% block title %}History: {{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
4
+
5
+{% block content %}
6
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7
+{% include "fossil/_project_nav.html" %}
8
+
9
+<div class="mb-4 flex items-center justify-between">
10
+ <div>
11
+ <a href="{% url 'fossil:code_file' slug=project.slug filepath=filepath %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to file</a>
12
+ <h2 class="text-lg font-semibold text-gray-100 mt-1 font-mono">{{ filepath }}</h2>
13
+ </div>
14
+ <span class="text-sm text-gray-500">{{ history|length }} commit{{ history|length|pluralize }}</span>
15
+</div>
16
+
17
+<div class="space-y-2">
18
+ {% for commit in history %}
19
+ <div class="rounded-lg bg-gray-800 border border-gray-700 px-4 py-3 flex items-start gap-3">
20
+ <div class="flex-shrink-0 mt-1">
21
+ <div class="w-2.5 h-2.5 rounded-full bg-brand"></div>
22
+ </div>
23
+ <div class="flex-1 min-w-0">
24
+ <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
25
+ class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:100 }}</a>
26
+ <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
27
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=comm }}</aap-3 text-xs text-gray-500">
28
+ e.html" %}
29
+{% load fossil_filters %{% extends "base.html" %}
30
+{% load fossil_filters %}
31
+{% block title %}History: {{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
32
+
33
+{% block content %}
34
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
35
+{% include "fossil/_project_nav.html" %}
36
+
37
+<d
--- a/templates/fossil/file_history.html
+++ b/templates/fossil/file_history.html
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/file_history.html
+++ b/templates/fossil/file_history.html
@@ -0,0 +1,37 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}History: {{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block content %}
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7 {% include "fossil/_project_nav.html" %}
8
9 <div class="mb-4 flex items-center justify-between">
10 <div>
11 <a href="{% url 'fossil:code_file' slug=project.slug filepath=filepath %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to file</a>
12 <h2 class="text-lg font-semibold text-gray-100 mt-1 font-mono">{{ filepath }}</h2>
13 </div>
14 <span class="text-sm text-gray-500">{{ history|length }} commit{{ history|length|pluralize }}</span>
15 </div>
16
17 <div class="space-y-2">
18 {% for commit in history %}
19 <div class="rounded-lg bg-gray-800 border border-gray-700 px-4 py-3 flex items-start gap-3">
20 <div class="flex-shrink-0 mt-1">
21 <div class="w-2.5 h-2.5 rounded-full bg-brand"></div>
22 </div>
23 <div class="flex-1 min-w-0">
24 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
25 class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:100 }}</a>
26 <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
27 <a href="{% url 'fossil:user_activity' slug=project.slug username=comm }}</aap-3 text-xs text-gray-500">
28 e.html" %}
29 {% load fossil_filters %{% extends "base.html" %}
30 {% load fossil_filters %}
31 {% block title %}History: {{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
32
33 {% block content %}
34 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
35 {% include "fossil/_project_nav.html" %}
36
37 <d
--- a/templates/fossil/search.html
+++ b/templates/fossil/search.html
@@ -0,0 +1,28 @@
1
+{% extends "base.html" %}
2
+{% load fossil_filters %}
3
+{% block title %}Search — {{ project.name }} — Fossilrepo{% endblock %}
4
+
5
+{% block content %}
6
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7
+{% include "fossil/_project_nav.html" %}
8
+
9
+<form method="get" class=gap-2">
10
+ <input type="text" name="q" value="{{ query }}" placeholder="Search checkins, tickets, wiki..."
11
+ rder-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm px-4 py-2"
12
+ autofocus>
13
+ <button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white hover:bg-brand-hover">Search</button>
14
+ </div>
15
+</form>
16
+
17
+{% if results %}
18
+<div class="space-y-6">
19
+ {% if results.checkins %}
20
+ <div>
21
+ <h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-2">Checkins ({{ results.checkins|length }})</h3>
22
+ <div class="rounded-lg bg-gray-800 border border-gray-700 divide-y divide-gray-700">
23
+ {% for c in results.checkins %}
24
+ <div class="px-4 py-3">
25
+ <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=c.uuid %}" class="text-sm text-gray-200 hover:text-brand-light">{{ c.comment|truncatechars:100 }}</a>
26
+ <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
27
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="hover:text-gray-300">{{ c.user|display_user }}</a>
28
+ ap-3 text-xs texheckin_detai
--- a/templates/fossil/search.html
+++ b/templates/fossil/search.html
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/search.html
+++ b/templates/fossil/search.html
@@ -0,0 +1,28 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Search — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block content %}
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7 {% include "fossil/_project_nav.html" %}
8
9 <form method="get" class=gap-2">
10 <input type="text" name="q" value="{{ query }}" placeholder="Search checkins, tickets, wiki..."
11 rder-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm px-4 py-2"
12 autofocus>
13 <button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white hover:bg-brand-hover">Search</button>
14 </div>
15 </form>
16
17 {% if results %}
18 <div class="space-y-6">
19 {% if results.checkins %}
20 <div>
21 <h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-2">Checkins ({{ results.checkins|length }})</h3>
22 <div class="rounded-lg bg-gray-800 border border-gray-700 divide-y divide-gray-700">
23 {% for c in results.checkins %}
24 <div class="px-4 py-3">
25 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=c.uuid %}" class="text-sm text-gray-200 hover:text-brand-light">{{ c.comment|truncatechars:100 }}</a>
26 <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
27 <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="hover:text-gray-300">{{ c.user|display_user }}</a>
28 ap-3 text-xs texheckin_detai

Keyboard Shortcuts

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