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
Commit
4cf64702dd2452dff65f755c496297bf51ac989385514db5ceaa454e95b7a273
Parent
80abf6da1603974…
7 files changed
+82
+2
+69
+11
+1
+37
+28
+82
| --- fossil/reader.py | ||
| +++ fossil/reader.py | ||
| @@ -652,10 +652,92 @@ | ||
| 652 | 652 | # Recursively resolve the source blob |
| 653 | 653 | source = self._resolve_blob(delta_row["srcid"], by_rid=True) |
| 654 | 654 | return _apply_fossil_delta(source, data) |
| 655 | 655 | |
| 656 | 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 | |
| 657 | 739 | |
| 658 | 740 | # --- Tickets --- |
| 659 | 741 | |
| 660 | 742 | def get_tickets(self, status: str | None = None, limit: int = 50) -> list[TicketEntry]: |
| 661 | 743 | sql = "SELECT tkt_uuid, title, status, type, tkt_ctime, subsystem, priority FROM ticket" |
| 662 | 744 |
| --- 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 |
+2
| --- fossil/urls.py | ||
| +++ fossil/urls.py | ||
| @@ -19,8 +19,10 @@ | ||
| 19 | 19 | path("tickets/create/", views.ticket_create, name="ticket_create"), |
| 20 | 20 | path("forum/", views.forum_list, name="forum"), |
| 21 | 21 | path("forum/<str:thread_uuid>/", views.forum_thread, name="forum_thread"), |
| 22 | 22 | path("user/<str:username>/", views.user_activity, name="user_activity"), |
| 23 | 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"), | |
| 24 | 26 | path("docs/", views.fossil_docs, name="docs"), |
| 25 | 27 | path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"), |
| 26 | 28 | ] |
| 27 | 29 |
| --- 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 |
+69
| --- fossil/views.py | ||
| +++ fossil/views.py | ||
| @@ -290,10 +290,29 @@ | ||
| 290 | 290 | metadata = reader.get_metadata() |
| 291 | 291 | latest_commit = reader.get_timeline(limit=1, event_type="ci") |
| 292 | 292 | |
| 293 | 293 | # Build directory listing for the current path |
| 294 | 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 | |
| 295 | 314 | |
| 296 | 315 | # Build breadcrumbs |
| 297 | 316 | breadcrumbs = [] |
| 298 | 317 | if dirpath: |
| 299 | 318 | parts = dirpath.strip("/").split("/") |
| @@ -313,10 +332,11 @@ | ||
| 313 | 332 | "current_dir": dirpath, |
| 314 | 333 | "breadcrumbs": breadcrumbs, |
| 315 | 334 | "checkin_uuid": checkin_uuid, |
| 316 | 335 | "metadata": metadata, |
| 317 | 336 | "latest_commit": latest_commit[0] if latest_commit else None, |
| 337 | + "readme_html": readme_html, | |
| 318 | 338 | "active_tab": "code", |
| 319 | 339 | }, |
| 320 | 340 | ) |
| 321 | 341 | |
| 322 | 342 | |
| @@ -841,10 +861,59 @@ | ||
| 841 | 861 | "activity": activity, |
| 842 | 862 | "active_tab": "timeline", |
| 843 | 863 | }, |
| 844 | 864 | ) |
| 845 | 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 | + | |
| 846 | 915 | |
| 847 | 916 | # --- Branches --- |
| 848 | 917 | |
| 849 | 918 | |
| 850 | 919 | @login_required |
| 851 | 920 |
| --- 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 @@ | ||
| 44 | 44 | </div> |
| 45 | 45 | |
| 46 | 46 | <!-- File table --> |
| 47 | 47 | {% include "fossil/partials/file_tree.html" %} |
| 48 | 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 %} | |
| 49 | 60 | {% endblock %} |
| 50 | 61 |
| --- 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 @@ | ||
| 62 | 62 | <div class="flex items-center gap-1 text-xs"> |
| 63 | 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 | 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 | 65 | </div> |
| 66 | 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> | |
| 67 | 68 | {% if not is_binary %} |
| 68 | 69 | <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span> |
| 69 | 70 | {% endif %} |
| 70 | 71 | </div> |
| 71 | 72 | </div> |
| 72 | 73 | |
| 73 | 74 | ADDED templates/fossil/file_history.html |
| 74 | 75 | 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">← 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">← 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 |