FossilRepo
v0.1.0 feature complete: chat, bundles, wiki links, author attribution
Commit
46f6d5efaa3c9c1881e937dee379111bd0ea41735ba60cb0eda84182d96163d4
Parent
b3f42783ee51faa…
36 files changed
+1
-1
+6
+22
-8
+35
-15
+16
+24
+18
+37
+53
+1
+6
+113
-13
+1
+5
-1
+1
+4
+29
+40
+1
-1
+1
-1
+2
-2
+1
-1
+8
+16
+1
-1
+1
-1
+1
-1
+12
-2
+1
-1
+9
-2
~
LICENSE
~
docs/assets/css/custom.css
~
docs/getting-started/installation.md
~
docs/index.md
+
docs/overrides/partials/source.html
~
fossil/__pycache__/api_views.cpython-314.pyc
~
fossil/__pycache__/chat.cpython-314.pyc
~
fossil/__pycache__/cli.cpython-314.pyc
~
fossil/__pycache__/models.cpython-314.pyc
~
fossil/__pycache__/urls.cpython-314.pyc
~
fossil/__pycache__/views.cpython-314.pyc
~
fossil/api_views.py
+
fossil/chat.py
~
fossil/cli.py
+
fossil/migrations/0014_chat_message.py
~
fossil/models.py
~
fossil/urls.py
~
fossil/views.py
~
install.sh
~
mkdocs.yml
~
pyproject.toml
~
templates/fossil/_project_nav.html
+
templates/fossil/bundle_import.html
+
templates/fossil/chat.html
~
templates/fossil/checkin_detail.html
~
templates/fossil/code_browser.html
~
templates/fossil/compare.html
~
templates/fossil/file_history.html
+
templates/fossil/partials/chat_messages.html
~
templates/fossil/repo_settings.html
~
templates/fossil/technote_detail.html
~
templates/fossil/technote_list.html
~
templates/fossil/ticket_detail.html
~
templates/fossil/ticket_form.html
~
templates/fossil/unversioned_list.html
~
tests/test_views_coverage.py
M
LICENSE
+1
-1
| --- LICENSE | ||
| +++ LICENSE | ||
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | MIT License |
| 2 | 2 | |
| 3 | -Copyright (c) 2026 Conflict LLC | |
| 3 | +Copyright (c) 2026 Leo Mata & CONFLICT LLC | |
| 4 | 4 | |
| 5 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy |
| 6 | 6 | of this software and associated documentation files (the "Software"), to deal |
| 7 | 7 | in the Software without restriction, including without limitation the rights |
| 8 | 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 9 | 9 |
| --- LICENSE | |
| +++ LICENSE | |
| @@ -1,8 +1,8 @@ | |
| 1 | MIT License |
| 2 | |
| 3 | Copyright (c) 2026 Conflict LLC |
| 4 | |
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy |
| 6 | of this software and associated documentation files (the "Software"), to deal |
| 7 | in the Software without restriction, including without limitation the rights |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 9 |
| --- LICENSE | |
| +++ LICENSE | |
| @@ -1,8 +1,8 @@ | |
| 1 | MIT License |
| 2 | |
| 3 | Copyright (c) 2026 Leo Mata & CONFLICT LLC |
| 4 | |
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy |
| 6 | of this software and associated documentation files (the "Software"), to deal |
| 7 | in the Software without restriction, including without limitation the rights |
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 9 |
| --- docs/assets/css/custom.css | ||
| +++ docs/assets/css/custom.css | ||
| @@ -141,10 +141,16 @@ | ||
| 141 | 141 | } |
| 142 | 142 | |
| 143 | 143 | [data-md-color-scheme="slate"] ::-webkit-scrollbar-thumb:hover { |
| 144 | 144 | background: #3a3a3a; |
| 145 | 145 | } |
| 146 | + | |
| 147 | +/* Dual repo links in header */ | |
| 148 | +.md-header__source { | |
| 149 | + display: flex; | |
| 150 | + gap: 0.4rem; | |
| 151 | +} | |
| 146 | 152 | |
| 147 | 153 | /* Content max width for readability */ |
| 148 | 154 | .md-grid { |
| 149 | 155 | max-width: 1220px; |
| 150 | 156 | } |
| 151 | 157 |
| --- docs/assets/css/custom.css | |
| +++ docs/assets/css/custom.css | |
| @@ -141,10 +141,16 @@ | |
| 141 | } |
| 142 | |
| 143 | [data-md-color-scheme="slate"] ::-webkit-scrollbar-thumb:hover { |
| 144 | background: #3a3a3a; |
| 145 | } |
| 146 | |
| 147 | /* Content max width for readability */ |
| 148 | .md-grid { |
| 149 | max-width: 1220px; |
| 150 | } |
| 151 |
| --- docs/assets/css/custom.css | |
| +++ docs/assets/css/custom.css | |
| @@ -141,10 +141,16 @@ | |
| 141 | } |
| 142 | |
| 143 | [data-md-color-scheme="slate"] ::-webkit-scrollbar-thumb:hover { |
| 144 | background: #3a3a3a; |
| 145 | } |
| 146 | |
| 147 | /* Dual repo links in header */ |
| 148 | .md-header__source { |
| 149 | display: flex; |
| 150 | gap: 0.4rem; |
| 151 | } |
| 152 | |
| 153 | /* Content max width for readability */ |
| 154 | .md-grid { |
| 155 | max-width: 1220px; |
| 156 | } |
| 157 |
+22
-8
| --- docs/getting-started/installation.md | ||
| +++ docs/getting-started/installation.md | ||
| @@ -1,17 +1,31 @@ | ||
| 1 | 1 | # Setup Guide |
| 2 | 2 | |
| 3 | 3 | ## Quick Start (Docker) |
| 4 | 4 | |
| 5 | -```bash | |
| 6 | -git clone https://github.com/ConflictHQ/fossilrepo.git | |
| 7 | -cd fossilrepo | |
| 8 | -docker compose up -d --build | |
| 9 | -docker compose exec backend python manage.py migrate | |
| 10 | -docker compose exec backend python manage.py seed | |
| 11 | -docker compose exec backend python manage.py seed_roles | |
| 12 | -``` | |
| 5 | +=== "Fossil" | |
| 6 | + | |
| 7 | + ```bash | |
| 8 | + fossil clone https://fossilrepo.io/projects/fossilrepo/ fossilrepo.fossil | |
| 9 | + fossil open fossilrepo.fossil --workdir fossilrepo | |
| 10 | + cd fossilrepo | |
| 11 | + docker compose up -d --build | |
| 12 | + docker compose exec backend python manage.py migrate | |
| 13 | + docker compose exec backend python manage.py seed | |
| 14 | + docker compose exec backend python manage.py seed_roles | |
| 15 | + ``` | |
| 16 | + | |
| 17 | +=== "Git" | |
| 18 | + | |
| 19 | + ```bash | |
| 20 | + git clone https://github.com/ConflictHQ/fossilrepo.git | |
| 21 | + cd fossilrepo | |
| 22 | + docker compose up -d --build | |
| 23 | + docker compose exec backend python manage.py migrate | |
| 24 | + docker compose exec backend python manage.py seed | |
| 25 | + docker compose exec backend python manage.py seed_roles | |
| 26 | + ``` | |
| 13 | 27 | |
| 14 | 28 | Visit http://localhost:8000. Login: `admin` / `admin`. |
| 15 | 29 | |
| 16 | 30 | ## Default Users |
| 17 | 31 | |
| 18 | 32 |
| --- docs/getting-started/installation.md | |
| +++ docs/getting-started/installation.md | |
| @@ -1,17 +1,31 @@ | |
| 1 | # Setup Guide |
| 2 | |
| 3 | ## Quick Start (Docker) |
| 4 | |
| 5 | ```bash |
| 6 | git clone https://github.com/ConflictHQ/fossilrepo.git |
| 7 | cd fossilrepo |
| 8 | docker compose up -d --build |
| 9 | docker compose exec backend python manage.py migrate |
| 10 | docker compose exec backend python manage.py seed |
| 11 | docker compose exec backend python manage.py seed_roles |
| 12 | ``` |
| 13 | |
| 14 | Visit http://localhost:8000. Login: `admin` / `admin`. |
| 15 | |
| 16 | ## Default Users |
| 17 | |
| 18 |
| --- docs/getting-started/installation.md | |
| +++ docs/getting-started/installation.md | |
| @@ -1,17 +1,31 @@ | |
| 1 | # Setup Guide |
| 2 | |
| 3 | ## Quick Start (Docker) |
| 4 | |
| 5 | === "Fossil" |
| 6 | |
| 7 | ```bash |
| 8 | fossil clone https://fossilrepo.io/projects/fossilrepo/ fossilrepo.fossil |
| 9 | fossil open fossilrepo.fossil --workdir fossilrepo |
| 10 | cd fossilrepo |
| 11 | docker compose up -d --build |
| 12 | docker compose exec backend python manage.py migrate |
| 13 | docker compose exec backend python manage.py seed |
| 14 | docker compose exec backend python manage.py seed_roles |
| 15 | ``` |
| 16 | |
| 17 | === "Git" |
| 18 | |
| 19 | ```bash |
| 20 | git clone https://github.com/ConflictHQ/fossilrepo.git |
| 21 | cd fossilrepo |
| 22 | docker compose up -d --build |
| 23 | docker compose exec backend python manage.py migrate |
| 24 | docker compose exec backend python manage.py seed |
| 25 | docker compose exec backend python manage.py seed_roles |
| 26 | ``` |
| 27 | |
| 28 | Visit http://localhost:8000. Login: `admin` / `admin`. |
| 29 | |
| 30 | ## Default Users |
| 31 | |
| 32 |
+35
-15
| --- docs/index.md | ||
| +++ docs/index.md | ||
| @@ -23,29 +23,49 @@ | ||
| 23 | 23 | |---|---| |
| 24 | 24 | | **Fossil server** | Serves all repos from a single process | |
| 25 | 25 | | **Caddy** | SSL termination, subdomain-per-repo routing | |
| 26 | 26 | | **Litestream** | Continuous SQLite replication to S3/MinIO | |
| 27 | 27 | | **Django management UI** | Repository lifecycle, user management, dashboards | |
| 28 | -| **Sync bridge** | Mirror Fossil repos to GitHub/GitLab (read-only) | | |
| 28 | +| **Sync bridge** | Bidirectional sync between Fossil and GitHub/GitLab | | |
| 29 | 29 | | **Celery workers** | Background sync, scheduled tasks | |
| 30 | 30 | |
| 31 | 31 | ## Quick Start |
| 32 | 32 | |
| 33 | -```bash | |
| 34 | -# Clone the repo | |
| 35 | -git clone https://github.com/ConflictHQ/fossilrepo.git | |
| 36 | -cd fossilrepo | |
| 37 | - | |
| 38 | -# Start the full stack | |
| 39 | -make build | |
| 40 | - | |
| 41 | -# Seed development data | |
| 42 | -make seed | |
| 43 | - | |
| 44 | -# Open the dashboard | |
| 45 | -open http://localhost:8000 | |
| 46 | -``` | |
| 33 | +=== "Fossil" | |
| 34 | + | |
| 35 | + ```bash | |
| 36 | + fossil clone https://fossilrepo.io/projects/fossilrepo/ fossilrepo.fossil | |
| 37 | + fossil open fossilrepo.fossil --workdir fossilrepo | |
| 38 | + cd fossilrepo | |
| 39 | + | |
| 40 | + # Start the full stack | |
| 41 | + make build | |
| 42 | + | |
| 43 | + # Seed development data | |
| 44 | + make seed | |
| 45 | + | |
| 46 | + # Open the dashboard | |
| 47 | + open http://localhost:8000 | |
| 48 | + ``` | |
| 49 | + | |
| 50 | +=== "Git" | |
| 51 | + | |
| 52 | + ```bash | |
| 53 | + git clone https://github.com/ConflictHQ/fossilrepo.git | |
| 54 | + cd fossilrepo | |
| 55 | + | |
| 56 | + # Start the full stack | |
| 57 | + make build | |
| 58 | + | |
| 59 | + # Seed development data | |
| 60 | + make seed | |
| 61 | + | |
| 62 | + # Open the dashboard | |
| 63 | + open http://localhost:8000 | |
| 64 | + ``` | |
| 65 | + | |
| 66 | +Both repositories are kept in sync bidirectionally via the [sync bridge](architecture/sync-bridge.md). | |
| 47 | 67 | |
| 48 | 68 | ## Architecture |
| 49 | 69 | |
| 50 | 70 | ``` |
| 51 | 71 | Caddy (SSL termination, routing, subdomain per repo) |
| 52 | 72 | |
| 53 | 73 | ADDED docs/overrides/partials/source.html |
| --- docs/index.md | |
| +++ docs/index.md | |
| @@ -23,29 +23,49 @@ | |
| 23 | |---|---| |
| 24 | | **Fossil server** | Serves all repos from a single process | |
| 25 | | **Caddy** | SSL termination, subdomain-per-repo routing | |
| 26 | | **Litestream** | Continuous SQLite replication to S3/MinIO | |
| 27 | | **Django management UI** | Repository lifecycle, user management, dashboards | |
| 28 | | **Sync bridge** | Mirror Fossil repos to GitHub/GitLab (read-only) | |
| 29 | | **Celery workers** | Background sync, scheduled tasks | |
| 30 | |
| 31 | ## Quick Start |
| 32 | |
| 33 | ```bash |
| 34 | # Clone the repo |
| 35 | git clone https://github.com/ConflictHQ/fossilrepo.git |
| 36 | cd fossilrepo |
| 37 | |
| 38 | # Start the full stack |
| 39 | make build |
| 40 | |
| 41 | # Seed development data |
| 42 | make seed |
| 43 | |
| 44 | # Open the dashboard |
| 45 | open http://localhost:8000 |
| 46 | ``` |
| 47 | |
| 48 | ## Architecture |
| 49 | |
| 50 | ``` |
| 51 | Caddy (SSL termination, routing, subdomain per repo) |
| 52 | |
| 53 | DDED docs/overrides/partials/source.html |
| --- docs/index.md | |
| +++ docs/index.md | |
| @@ -23,29 +23,49 @@ | |
| 23 | |---|---| |
| 24 | | **Fossil server** | Serves all repos from a single process | |
| 25 | | **Caddy** | SSL termination, subdomain-per-repo routing | |
| 26 | | **Litestream** | Continuous SQLite replication to S3/MinIO | |
| 27 | | **Django management UI** | Repository lifecycle, user management, dashboards | |
| 28 | | **Sync bridge** | Bidirectional sync between Fossil and GitHub/GitLab | |
| 29 | | **Celery workers** | Background sync, scheduled tasks | |
| 30 | |
| 31 | ## Quick Start |
| 32 | |
| 33 | === "Fossil" |
| 34 | |
| 35 | ```bash |
| 36 | fossil clone https://fossilrepo.io/projects/fossilrepo/ fossilrepo.fossil |
| 37 | fossil open fossilrepo.fossil --workdir fossilrepo |
| 38 | cd fossilrepo |
| 39 | |
| 40 | # Start the full stack |
| 41 | make build |
| 42 | |
| 43 | # Seed development data |
| 44 | make seed |
| 45 | |
| 46 | # Open the dashboard |
| 47 | open http://localhost:8000 |
| 48 | ``` |
| 49 | |
| 50 | === "Git" |
| 51 | |
| 52 | ```bash |
| 53 | git clone https://github.com/ConflictHQ/fossilrepo.git |
| 54 | cd fossilrepo |
| 55 | |
| 56 | # Start the full stack |
| 57 | make build |
| 58 | |
| 59 | # Seed development data |
| 60 | make seed |
| 61 | |
| 62 | # Open the dashboard |
| 63 | open http://localhost:8000 |
| 64 | ``` |
| 65 | |
| 66 | Both repositories are kept in sync bidirectionally via the [sync bridge](architecture/sync-bridge.md). |
| 67 | |
| 68 | ## Architecture |
| 69 | |
| 70 | ``` |
| 71 | Caddy (SSL termination, routing, subdomain per repo) |
| 72 | |
| 73 | DDED docs/overrides/partials/source.html |
| --- a/docs/overrides/partials/source.html | ||
| +++ b/docs/overrides/partials/source.html | ||
| @@ -0,0 +1,16 @@ | ||
| 1 | +<a href="https://fossilrepo.io/projects/fossilrepo/" title="Fossil Repository" class="md-source" data-md-component="source"> | |
| 2 | + <div class="md-source__icon md-icon"> | |
| 3 | + {% include ".icons/material/source-repository.svg" %} | |
| 4 | + </div> | |
| 5 | + <div class="md-source__repository"> | |
| 6 | + Fossil | |
| 7 | + </div> | |
| 8 | +</a> | |
| 9 | +<a href="{{ config.repo_url }}" title="{{ lang.t('source') }}" class="md-source" data-md-component="source"> | |
| 10 | + <div class="md-source__icon md-icon"> | |
| 11 | + {% include ".icons/fontawesome/brands/github.svg" %} | |
| 12 | + </div> | |
| 13 | + <div class="md-source__repository"> | |
| 14 | + {{ config.repo_name }} | |
| 15 | + </div> | |
| 16 | +</a> |
| --- a/docs/overrides/partials/source.html | |
| +++ b/docs/overrides/partials/source.html | |
| @@ -0,0 +1,16 @@ | |
| --- a/docs/overrides/partials/source.html | |
| +++ b/docs/overrides/partials/source.html | |
| @@ -0,0 +1,16 @@ | |
| 1 | <a href="https://fossilrepo.io/projects/fossilrepo/" title="Fossil Repository" class="md-source" data-md-component="source"> |
| 2 | <div class="md-source__icon md-icon"> |
| 3 | {% include ".icons/material/source-repository.svg" %} |
| 4 | </div> |
| 5 | <div class="md-source__repository"> |
| 6 | Fossil |
| 7 | </div> |
| 8 | </a> |
| 9 | <a href="{{ config.repo_url }}" title="{{ lang.t('source') }}" class="md-source" data-md-component="source"> |
| 10 | <div class="md-source__icon md-icon"> |
| 11 | {% include ".icons/fontawesome/brands/github.svg" %} |
| 12 | </div> |
| 13 | <div class="md-source__repository"> |
| 14 | {{ config.repo_name }} |
| 15 | </div> |
| 16 | </a> |
| --- fossil/__pycache__/api_views.cpython-314.pyc | ||
| +++ fossil/__pycache__/api_views.cpython-314.pyc | ||
| cannot compute difference between binary files | ||
| 1 | 1 |
| --- fossil/__pycache__/api_views.cpython-314.pyc | |
| +++ fossil/__pycache__/api_views.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/api_views.cpython-314.pyc | |
| +++ fossil/__pycache__/api_views.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/chat.cpython-314.pyc | ||
| +++ fossil/__pycache__/chat.cpython-314.pyc | ||
| cannot compute difference between binary files | ||
| 1 | 1 |
| --- fossil/__pycache__/chat.cpython-314.pyc | |
| +++ fossil/__pycache__/chat.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/chat.cpython-314.pyc | |
| +++ fossil/__pycache__/chat.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/cli.cpython-314.pyc | ||
| +++ fossil/__pycache__/cli.cpython-314.pyc | ||
| cannot compute difference between binary files | ||
| 1 | 1 |
| --- fossil/__pycache__/cli.cpython-314.pyc | |
| +++ fossil/__pycache__/cli.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/cli.cpython-314.pyc | |
| +++ fossil/__pycache__/cli.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/models.cpython-314.pyc | ||
| +++ fossil/__pycache__/models.cpython-314.pyc | ||
| cannot compute difference between binary files | ||
| 1 | 1 |
| --- fossil/__pycache__/models.cpython-314.pyc | |
| +++ fossil/__pycache__/models.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/models.cpython-314.pyc | |
| +++ fossil/__pycache__/models.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/urls.cpython-314.pyc | ||
| +++ fossil/__pycache__/urls.cpython-314.pyc | ||
| cannot compute difference between binary files | ||
| 1 | 1 |
| --- fossil/__pycache__/urls.cpython-314.pyc | |
| +++ fossil/__pycache__/urls.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/urls.cpython-314.pyc | |
| +++ fossil/__pycache__/urls.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/views.cpython-314.pyc | ||
| +++ fossil/__pycache__/views.cpython-314.pyc | ||
| cannot compute difference between binary files | ||
| 1 | 1 |
| --- fossil/__pycache__/views.cpython-314.pyc | |
| +++ fossil/__pycache__/views.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
| --- fossil/__pycache__/views.cpython-314.pyc | |
| +++ fossil/__pycache__/views.cpython-314.pyc | |
| 0 | annot compute difference between binary files |
| 1 |
+24
| --- fossil/api_views.py | ||
| +++ fossil/api_views.py | ||
| @@ -1528,10 +1528,11 @@ | ||
| 1528 | 1528 | - checkin: new checkin pushed |
| 1529 | 1529 | - ticket: ticket created/updated (by count change) |
| 1530 | 1530 | - claim: ticket claimed/released/submitted |
| 1531 | 1531 | - workspace: workspace created/merged/abandoned |
| 1532 | 1532 | - review: code review created/updated |
| 1533 | + - chat: new chat message posted | |
| 1533 | 1534 | |
| 1534 | 1535 | Heartbeat sent every 15 seconds if no events. Poll interval: 5 seconds. |
| 1535 | 1536 | """ |
| 1536 | 1537 | if request.method != "GET": |
| 1537 | 1538 | return JsonResponse({"error": "GET required"}, status=405) |
| @@ -1556,10 +1557,14 @@ | ||
| 1556 | 1557 | |
| 1557 | 1558 | last_claim_id = TicketClaim.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1558 | 1559 | last_workspace_id = AgentWorkspace.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1559 | 1560 | last_review_id = CodeReview.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1560 | 1561 | |
| 1562 | + from fossil.chat import ChatMessage | |
| 1563 | + | |
| 1564 | + last_chat_id = ChatMessage.objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 | |
| 1565 | + | |
| 1561 | 1566 | heartbeat_counter = 0 |
| 1562 | 1567 | |
| 1563 | 1568 | while True: |
| 1564 | 1569 | events = [] |
| 1565 | 1570 | |
| @@ -1632,10 +1637,29 @@ | ||
| 1632 | 1637 | "agent_id": review.agent_id, |
| 1633 | 1638 | }, |
| 1634 | 1639 | } |
| 1635 | 1640 | ) |
| 1636 | 1641 | last_review_id = review.pk |
| 1642 | + | |
| 1643 | + # Check for new chat messages | |
| 1644 | + try: | |
| 1645 | + new_msgs = ChatMessage.objects.filter(repository=repo, pk__gt=last_chat_id).order_by("pk") | |
| 1646 | + for msg in new_msgs: | |
| 1647 | + events.append( | |
| 1648 | + { | |
| 1649 | + "type": "chat", | |
| 1650 | + "data": { | |
| 1651 | + "id": msg.pk, | |
| 1652 | + "username": msg.username, | |
| 1653 | + "body": msg.body, | |
| 1654 | + "timestamp": msg.created_at.strftime("%H:%M"), | |
| 1655 | + }, | |
| 1656 | + } | |
| 1657 | + ) | |
| 1658 | + last_chat_id = msg.pk | |
| 1659 | + except Exception: | |
| 1660 | + pass | |
| 1637 | 1661 | |
| 1638 | 1662 | # Yield events |
| 1639 | 1663 | for event in events: |
| 1640 | 1664 | yield f"event: {event['type']}\ndata: {json.dumps(event['data'])}\n\n" |
| 1641 | 1665 | |
| 1642 | 1666 | |
| 1643 | 1667 | ADDED fossil/chat.py |
| --- fossil/api_views.py | |
| +++ fossil/api_views.py | |
| @@ -1528,10 +1528,11 @@ | |
| 1528 | - checkin: new checkin pushed |
| 1529 | - ticket: ticket created/updated (by count change) |
| 1530 | - claim: ticket claimed/released/submitted |
| 1531 | - workspace: workspace created/merged/abandoned |
| 1532 | - review: code review created/updated |
| 1533 | |
| 1534 | Heartbeat sent every 15 seconds if no events. Poll interval: 5 seconds. |
| 1535 | """ |
| 1536 | if request.method != "GET": |
| 1537 | return JsonResponse({"error": "GET required"}, status=405) |
| @@ -1556,10 +1557,14 @@ | |
| 1556 | |
| 1557 | last_claim_id = TicketClaim.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1558 | last_workspace_id = AgentWorkspace.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1559 | last_review_id = CodeReview.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1560 | |
| 1561 | heartbeat_counter = 0 |
| 1562 | |
| 1563 | while True: |
| 1564 | events = [] |
| 1565 | |
| @@ -1632,10 +1637,29 @@ | |
| 1632 | "agent_id": review.agent_id, |
| 1633 | }, |
| 1634 | } |
| 1635 | ) |
| 1636 | last_review_id = review.pk |
| 1637 | |
| 1638 | # Yield events |
| 1639 | for event in events: |
| 1640 | yield f"event: {event['type']}\ndata: {json.dumps(event['data'])}\n\n" |
| 1641 | |
| 1642 | |
| 1643 | DDED fossil/chat.py |
| --- fossil/api_views.py | |
| +++ fossil/api_views.py | |
| @@ -1528,10 +1528,11 @@ | |
| 1528 | - checkin: new checkin pushed |
| 1529 | - ticket: ticket created/updated (by count change) |
| 1530 | - claim: ticket claimed/released/submitted |
| 1531 | - workspace: workspace created/merged/abandoned |
| 1532 | - review: code review created/updated |
| 1533 | - chat: new chat message posted |
| 1534 | |
| 1535 | Heartbeat sent every 15 seconds if no events. Poll interval: 5 seconds. |
| 1536 | """ |
| 1537 | if request.method != "GET": |
| 1538 | return JsonResponse({"error": "GET required"}, status=405) |
| @@ -1556,10 +1557,14 @@ | |
| 1557 | |
| 1558 | last_claim_id = TicketClaim.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1559 | last_workspace_id = AgentWorkspace.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1560 | last_review_id = CodeReview.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1561 | |
| 1562 | from fossil.chat import ChatMessage |
| 1563 | |
| 1564 | last_chat_id = ChatMessage.objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0 |
| 1565 | |
| 1566 | heartbeat_counter = 0 |
| 1567 | |
| 1568 | while True: |
| 1569 | events = [] |
| 1570 | |
| @@ -1632,10 +1637,29 @@ | |
| 1637 | "agent_id": review.agent_id, |
| 1638 | }, |
| 1639 | } |
| 1640 | ) |
| 1641 | last_review_id = review.pk |
| 1642 | |
| 1643 | # Check for new chat messages |
| 1644 | try: |
| 1645 | new_msgs = ChatMessage.objects.filter(repository=repo, pk__gt=last_chat_id).order_by("pk") |
| 1646 | for msg in new_msgs: |
| 1647 | events.append( |
| 1648 | { |
| 1649 | "type": "chat", |
| 1650 | "data": { |
| 1651 | "id": msg.pk, |
| 1652 | "username": msg.username, |
| 1653 | "body": msg.body, |
| 1654 | "timestamp": msg.created_at.strftime("%H:%M"), |
| 1655 | }, |
| 1656 | } |
| 1657 | ) |
| 1658 | last_chat_id = msg.pk |
| 1659 | except Exception: |
| 1660 | pass |
| 1661 | |
| 1662 | # Yield events |
| 1663 | for event in events: |
| 1664 | yield f"event: {event['type']}\ndata: {json.dumps(event['data'])}\n\n" |
| 1665 | |
| 1666 | |
| 1667 | DDED fossil/chat.py |
+18
| --- a/fossil/chat.py | ||
| +++ b/fossil/chat.py | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +"""Real-time chat for Fossil repositories.""" | |
| 2 | + | |
| 3 | +from django.contrib.auth.models import User | |
| 4 | +from django.db import models | |
| 5 | + | |
| 6 | + | |
| 7 | +class ChatMessage(models.Model): | |
| 8 | + repository = models.ForeignKey("fossil.FossilRepository", on_delete=models.CASCADE, related_name="chat_messages") | |
| 9 | + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) | |
| 10 | + username = models.CharField(max_length=150) # denormalized display name | |
| 11 | + body = models.TextField() | |
| 12 | + created_at = models.DateTimeField(auto_now_add=True) | |
| 13 | + | |
| 14 | + class Meta: | |
| 15 | + ordering = ["created_at"] | |
| 16 | + | |
| 17 | + def __str__(self): | |
| 18 | + return f"{self.username}: {self.body[:50]}" |
| --- a/fossil/chat.py | |
| +++ b/fossil/chat.py | |
| @@ -0,0 +1,18 @@ | |
| --- a/fossil/chat.py | |
| +++ b/fossil/chat.py | |
| @@ -0,0 +1,18 @@ | |
| 1 | """Real-time chat for Fossil repositories.""" |
| 2 | |
| 3 | from django.contrib.auth.models import User |
| 4 | from django.db import models |
| 5 | |
| 6 | |
| 7 | class ChatMessage(models.Model): |
| 8 | repository = models.ForeignKey("fossil.FossilRepository", on_delete=models.CASCADE, related_name="chat_messages") |
| 9 | user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) |
| 10 | username = models.CharField(max_length=150) # denormalized display name |
| 11 | body = models.TextField() |
| 12 | created_at = models.DateTimeField(auto_now_add=True) |
| 13 | |
| 14 | class Meta: |
| 15 | ordering = ["created_at"] |
| 16 | |
| 17 | def __str__(self): |
| 18 | return f"{self.username}: {self.body[:50]}" |
+37
| --- fossil/cli.py | ||
| +++ fossil/cli.py | ||
| @@ -496,5 +496,42 @@ | ||
| 496 | 496 | cmd = [self.binary, "shun", "--list", "-R", str(repo_path)] |
| 497 | 497 | result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, env=self._env) |
| 498 | 498 | if result.returncode == 0: |
| 499 | 499 | return [line.strip() for line in result.stdout.strip().splitlines() if line.strip()] |
| 500 | 500 | return [] |
| 501 | + | |
| 502 | + def bundle_export(self, repo_path: Path, branch: str = "", checkin: str = "") -> bytes: | |
| 503 | + """Export a Fossil bundle. Returns raw bytes of the .bundle file.""" | |
| 504 | + import tempfile | |
| 505 | + | |
| 506 | + with tempfile.NamedTemporaryFile(suffix=".bundle", delete=False) as tmp: | |
| 507 | + tmp_path = Path(tmp.name) | |
| 508 | + try: | |
| 509 | + cmd = [self.binary, "bundle", "export", str(tmp_path), "-R", str(repo_path)] | |
| 510 | + if branch: | |
| 511 | + cmd += ["--branch", branch] | |
| 512 | + elif checkin: | |
| 513 | + cmd += ["--checkin", checkin] | |
| 514 | + else: | |
| 515 | + cmd += ["--branch", "trunk"] | |
| 516 | + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, env=self._env) | |
| 517 | + if result.returncode != 0: | |
| 518 | + return b"" | |
| 519 | + return tmp_path.read_bytes() | |
| 520 | + finally: | |
| 521 | + tmp_path.unlink(missing_ok=True) | |
| 522 | + | |
| 523 | + def bundle_import(self, repo_path: Path, bundle_bytes: bytes, publish: bool = False) -> bool: | |
| 524 | + """Import a Fossil bundle into the repo. Returns True on success.""" | |
| 525 | + import tempfile | |
| 526 | + | |
| 527 | + with tempfile.NamedTemporaryFile(suffix=".bundle", delete=False) as tmp: | |
| 528 | + tmp.write(bundle_bytes) | |
| 529 | + tmp_path = Path(tmp.name) | |
| 530 | + try: | |
| 531 | + cmd = [self.binary, "bundle", "import", str(tmp_path), "-R", str(repo_path)] | |
| 532 | + if publish: | |
| 533 | + cmd.append("--publish") | |
| 534 | + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, env=self._env) | |
| 535 | + return result.returncode == 0 | |
| 536 | + finally: | |
| 537 | + tmp_path.unlink(missing_ok=True) | |
| 501 | 538 | |
| 502 | 539 | ADDED fossil/migrations/0014_chat_message.py |
| --- fossil/cli.py | |
| +++ fossil/cli.py | |
| @@ -496,5 +496,42 @@ | |
| 496 | cmd = [self.binary, "shun", "--list", "-R", str(repo_path)] |
| 497 | result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, env=self._env) |
| 498 | if result.returncode == 0: |
| 499 | return [line.strip() for line in result.stdout.strip().splitlines() if line.strip()] |
| 500 | return [] |
| 501 | |
| 502 | DDED fossil/migrations/0014_chat_message.py |
| --- fossil/cli.py | |
| +++ fossil/cli.py | |
| @@ -496,5 +496,42 @@ | |
| 496 | cmd = [self.binary, "shun", "--list", "-R", str(repo_path)] |
| 497 | result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, env=self._env) |
| 498 | if result.returncode == 0: |
| 499 | return [line.strip() for line in result.stdout.strip().splitlines() if line.strip()] |
| 500 | return [] |
| 501 | |
| 502 | def bundle_export(self, repo_path: Path, branch: str = "", checkin: str = "") -> bytes: |
| 503 | """Export a Fossil bundle. Returns raw bytes of the .bundle file.""" |
| 504 | import tempfile |
| 505 | |
| 506 | with tempfile.NamedTemporaryFile(suffix=".bundle", delete=False) as tmp: |
| 507 | tmp_path = Path(tmp.name) |
| 508 | try: |
| 509 | cmd = [self.binary, "bundle", "export", str(tmp_path), "-R", str(repo_path)] |
| 510 | if branch: |
| 511 | cmd += ["--branch", branch] |
| 512 | elif checkin: |
| 513 | cmd += ["--checkin", checkin] |
| 514 | else: |
| 515 | cmd += ["--branch", "trunk"] |
| 516 | result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, env=self._env) |
| 517 | if result.returncode != 0: |
| 518 | return b"" |
| 519 | return tmp_path.read_bytes() |
| 520 | finally: |
| 521 | tmp_path.unlink(missing_ok=True) |
| 522 | |
| 523 | def bundle_import(self, repo_path: Path, bundle_bytes: bytes, publish: bool = False) -> bool: |
| 524 | """Import a Fossil bundle into the repo. Returns True on success.""" |
| 525 | import tempfile |
| 526 | |
| 527 | with tempfile.NamedTemporaryFile(suffix=".bundle", delete=False) as tmp: |
| 528 | tmp.write(bundle_bytes) |
| 529 | tmp_path = Path(tmp.name) |
| 530 | try: |
| 531 | cmd = [self.binary, "bundle", "import", str(tmp_path), "-R", str(repo_path)] |
| 532 | if publish: |
| 533 | cmd.append("--publish") |
| 534 | result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, env=self._env) |
| 535 | return result.returncode == 0 |
| 536 | finally: |
| 537 | tmp_path.unlink(missing_ok=True) |
| 538 | |
| 539 | DDED fossil/migrations/0014_chat_message.py |
| --- a/fossil/migrations/0014_chat_message.py | ||
| +++ b/fossil/migrations/0014_chat_message.py | ||
| @@ -0,0 +1,53 @@ | ||
| 1 | +# Generated by Django 5.2.12 on 2026-04-14 04:10 | |
| 2 | + | |
| 3 | +import django.db.models.deletion | |
| 4 | +from django.conf import settings | |
| 5 | +from django.db import migrations, models | |
| 6 | + | |
| 7 | + | |
| 8 | +class Migration(migrations.Migration): | |
| 9 | + | |
| 10 | + dependencies = [ | |
| 11 | + ("fossil", "0013_ticketsyncmapping_wikisyncmapping"), | |
| 12 | + migrations.swappable_dependency(settings.AUTH_USER_MODEL), | |
| 13 | + ] | |
| 14 | + | |
| 15 | + operations = [ | |
| 16 | + migrations.CreateModel( | |
| 17 | + name="ChatMessage", | |
| 18 | + fields=[ | |
| 19 | + ( | |
| 20 | + "id", | |
| 21 | + models.BigAutoField( | |
| 22 | + auto_created=True, | |
| 23 | + primary_key=True, | |
| 24 | + serialize=False, | |
| 25 | + verbose_name="ID", | |
| 26 | + ), | |
| 27 | + ), | |
| 28 | + ("username", models.CharField(max_length=150)), | |
| 29 | + ("body", models.TextField()), | |
| 30 | + ("created_at", models.DateTimeField(auto_now_add=True)), | |
| 31 | + ( | |
| 32 | + "repository", | |
| 33 | + models.ForeignKey( | |
| 34 | + on_delete=django.db.models.deletion.CASCADE, | |
| 35 | + related_name="chat_messages", | |
| 36 | + to="fossil.fossilrepository", | |
| 37 | + ), | |
| 38 | + ), | |
| 39 | + ( | |
| 40 | + "user", | |
| 41 | + models.ForeignKey( | |
| 42 | + blank=True, | |
| 43 | + null=True, | |
| 44 | + on_delete=django.db.models.deletion.SET_NULL, | |
| 45 | + to=settings.AUTH_USER_MODEL, | |
| 46 | + ), | |
| 47 | + ), | |
| 48 | + ], | |
| 49 | + options={ | |
| 50 | + "ordering": ["created_at"], | |
| 51 | + }, | |
| 52 | + ), | |
| 53 | + ] |
| --- a/fossil/migrations/0014_chat_message.py | |
| +++ b/fossil/migrations/0014_chat_message.py | |
| @@ -0,0 +1,53 @@ | |
| --- a/fossil/migrations/0014_chat_message.py | |
| +++ b/fossil/migrations/0014_chat_message.py | |
| @@ -0,0 +1,53 @@ | |
| 1 | # Generated by Django 5.2.12 on 2026-04-14 04:10 |
| 2 | |
| 3 | import django.db.models.deletion |
| 4 | from django.conf import settings |
| 5 | from django.db import migrations, models |
| 6 | |
| 7 | |
| 8 | class Migration(migrations.Migration): |
| 9 | |
| 10 | dependencies = [ |
| 11 | ("fossil", "0013_ticketsyncmapping_wikisyncmapping"), |
| 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
| 13 | ] |
| 14 | |
| 15 | operations = [ |
| 16 | migrations.CreateModel( |
| 17 | name="ChatMessage", |
| 18 | fields=[ |
| 19 | ( |
| 20 | "id", |
| 21 | models.BigAutoField( |
| 22 | auto_created=True, |
| 23 | primary_key=True, |
| 24 | serialize=False, |
| 25 | verbose_name="ID", |
| 26 | ), |
| 27 | ), |
| 28 | ("username", models.CharField(max_length=150)), |
| 29 | ("body", models.TextField()), |
| 30 | ("created_at", models.DateTimeField(auto_now_add=True)), |
| 31 | ( |
| 32 | "repository", |
| 33 | models.ForeignKey( |
| 34 | on_delete=django.db.models.deletion.CASCADE, |
| 35 | related_name="chat_messages", |
| 36 | to="fossil.fossilrepository", |
| 37 | ), |
| 38 | ), |
| 39 | ( |
| 40 | "user", |
| 41 | models.ForeignKey( |
| 42 | blank=True, |
| 43 | null=True, |
| 44 | on_delete=django.db.models.deletion.SET_NULL, |
| 45 | to=settings.AUTH_USER_MODEL, |
| 46 | ), |
| 47 | ), |
| 48 | ], |
| 49 | options={ |
| 50 | "ordering": ["created_at"], |
| 51 | }, |
| 52 | ), |
| 53 | ] |
+1
| --- fossil/models.py | ||
| +++ fossil/models.py | ||
| @@ -68,10 +68,11 @@ | ||
| 68 | 68 | |
| 69 | 69 | # Import related models so they're discoverable by Django |
| 70 | 70 | from fossil.agent_claims import TicketClaim # noqa: E402, F401 |
| 71 | 71 | from fossil.api_tokens import APIToken # noqa: E402, F401 |
| 72 | 72 | from fossil.branch_protection import BranchProtection # noqa: E402, F401 |
| 73 | +from fossil.chat import ChatMessage # noqa: E402, F401 | |
| 73 | 74 | from fossil.ci import StatusCheck # noqa: E402, F401 |
| 74 | 75 | from fossil.code_reviews import CodeReview, ReviewComment # noqa: E402, F401 |
| 75 | 76 | from fossil.forum import ForumPost # noqa: E402, F401 |
| 76 | 77 | from fossil.notifications import Notification, NotificationPreference, ProjectWatch # noqa: E402, F401 |
| 77 | 78 | from fossil.releases import Release, ReleaseAsset # noqa: E402, F401 |
| 78 | 79 |
| --- fossil/models.py | |
| +++ fossil/models.py | |
| @@ -68,10 +68,11 @@ | |
| 68 | |
| 69 | # Import related models so they're discoverable by Django |
| 70 | from fossil.agent_claims import TicketClaim # noqa: E402, F401 |
| 71 | from fossil.api_tokens import APIToken # noqa: E402, F401 |
| 72 | from fossil.branch_protection import BranchProtection # noqa: E402, F401 |
| 73 | from fossil.ci import StatusCheck # noqa: E402, F401 |
| 74 | from fossil.code_reviews import CodeReview, ReviewComment # noqa: E402, F401 |
| 75 | from fossil.forum import ForumPost # noqa: E402, F401 |
| 76 | from fossil.notifications import Notification, NotificationPreference, ProjectWatch # noqa: E402, F401 |
| 77 | from fossil.releases import Release, ReleaseAsset # noqa: E402, F401 |
| 78 |
| --- fossil/models.py | |
| +++ fossil/models.py | |
| @@ -68,10 +68,11 @@ | |
| 68 | |
| 69 | # Import related models so they're discoverable by Django |
| 70 | from fossil.agent_claims import TicketClaim # noqa: E402, F401 |
| 71 | from fossil.api_tokens import APIToken # noqa: E402, F401 |
| 72 | from fossil.branch_protection import BranchProtection # noqa: E402, F401 |
| 73 | from fossil.chat import ChatMessage # noqa: E402, F401 |
| 74 | from fossil.ci import StatusCheck # noqa: E402, F401 |
| 75 | from fossil.code_reviews import CodeReview, ReviewComment # noqa: E402, F401 |
| 76 | from fossil.forum import ForumPost # noqa: E402, F401 |
| 77 | from fossil.notifications import Notification, NotificationPreference, ProjectWatch # noqa: E402, F401 |
| 78 | from fossil.releases import Release, ReleaseAsset # noqa: E402, F401 |
| 79 |
+6
| --- fossil/urls.py | ||
| +++ fossil/urls.py | ||
| @@ -136,6 +136,12 @@ | ||
| 136 | 136 | path("admin/shun/add/", views.shun_artifact, name="shun_artifact"), |
| 137 | 137 | # SQLite Explorer |
| 138 | 138 | path("explorer/", views.repo_explorer, name="explorer"), |
| 139 | 139 | path("explorer/table/<str:table_name>/", views.repo_explorer_table, name="explorer_table"), |
| 140 | 140 | path("explorer/query/", views.repo_explorer_query, name="explorer_query"), |
| 141 | + # Bundle export/import | |
| 142 | + path("bundle/export/", views.bundle_export, name="bundle_export"), | |
| 143 | + path("bundle/import/", views.bundle_import, name="bundle_import"), | |
| 144 | + # Chat | |
| 145 | + path("chat/", views.chat_room, name="chat"), | |
| 146 | + path("chat/send/", views.chat_send, name="chat_send"), | |
| 141 | 147 | ] |
| 142 | 148 |
| --- fossil/urls.py | |
| +++ fossil/urls.py | |
| @@ -136,6 +136,12 @@ | |
| 136 | path("admin/shun/add/", views.shun_artifact, name="shun_artifact"), |
| 137 | # SQLite Explorer |
| 138 | path("explorer/", views.repo_explorer, name="explorer"), |
| 139 | path("explorer/table/<str:table_name>/", views.repo_explorer_table, name="explorer_table"), |
| 140 | path("explorer/query/", views.repo_explorer_query, name="explorer_query"), |
| 141 | ] |
| 142 |
| --- fossil/urls.py | |
| +++ fossil/urls.py | |
| @@ -136,6 +136,12 @@ | |
| 136 | path("admin/shun/add/", views.shun_artifact, name="shun_artifact"), |
| 137 | # SQLite Explorer |
| 138 | path("explorer/", views.repo_explorer, name="explorer"), |
| 139 | path("explorer/table/<str:table_name>/", views.repo_explorer_table, name="explorer_table"), |
| 140 | path("explorer/query/", views.repo_explorer_query, name="explorer_query"), |
| 141 | # Bundle export/import |
| 142 | path("bundle/export/", views.bundle_export, name="bundle_export"), |
| 143 | path("bundle/import/", views.bundle_import, name="bundle_import"), |
| 144 | # Chat |
| 145 | path("chat/", views.chat_room, name="chat"), |
| 146 | path("chat/send/", views.chat_send, name="chat_send"), |
| 147 | ] |
| 148 |
+113
-13
| --- fossil/views.py | ||
| +++ fossil/views.py | ||
| @@ -41,11 +41,11 @@ | ||
| 41 | 41 | path = m.group(1).strip() |
| 42 | 42 | text = m.group(2).strip() |
| 43 | 43 | if path.startswith("./"): |
| 44 | 44 | path = "/" + base_path + path[2:] |
| 45 | 45 | elif not path.startswith("/") and not path.startswith("http"): |
| 46 | - path = "/" + base_path + path | |
| 46 | + path = "/" + base_path + path if base_path else "/wiki/" + path | |
| 47 | 47 | return f"[{text}]({path})" |
| 48 | 48 | |
| 49 | 49 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_to_md_link, content) |
| 50 | 50 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL) |
| 51 | 51 | html = md.markdown(content, extensions=["fenced_code", "tables", "toc", "footnotes", "def_list", "attr_list"]) |
| @@ -73,21 +73,21 @@ | ||
| 73 | 73 | text = match.group(2).strip() |
| 74 | 74 | # Convert relative paths to absolute using base_path |
| 75 | 75 | if path.startswith("./"): |
| 76 | 76 | path = "/" + base_path + path[2:] |
| 77 | 77 | elif not path.startswith("/") and not path.startswith("http"): |
| 78 | - path = "/" + base_path + path | |
| 78 | + path = "/" + base_path + path if base_path else "/wiki/" + path | |
| 79 | 79 | return f'<a href="{path}">{text}</a>' |
| 80 | 80 | |
| 81 | 81 | # Match [path | text] with flexible whitespace around the pipe |
| 82 | 82 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_link_replace, content) |
| 83 | 83 | # Interwiki links: [wikipedia:Article] -> external link |
| 84 | 84 | content = re.sub(r"\[wikipedia:([^\]]+)\]", r'<a href="https://en.wikipedia.org/wiki/\1">\1</a>', content) |
| 85 | 85 | # Anchor links: [#anchor-name] -> local anchor |
| 86 | 86 | content = re.sub(r"\[#([^\]]+)\]", r'<a href="#\1">\1</a>', content) |
| 87 | - # Bare wiki links: [PageName] (no pipe, not a URL) | |
| 88 | - content = re.sub(r"\[([A-Z][a-zA-Z0-9_]+)\]", r'<a href="\1">\1</a>', content) | |
| 87 | + # Bare wiki links: [PageName] (no pipe, not a URL) — use /wiki/ prefix so _rewrite_fossil_links maps it correctly | |
| 88 | + content = re.sub(r"\[([A-Z][a-zA-Z0-9_]+)\]", r'<a href="/wiki/\1">\1</a>', content) | |
| 89 | 89 | |
| 90 | 90 | # Verbatim blocks |
| 91 | 91 | # Pikchr diagrams: <verbatim type="pikchr">...</verbatim> → SVG |
| 92 | 92 | def _render_pikchr_block(m): |
| 93 | 93 | try: |
| @@ -1361,13 +1361,16 @@ | ||
| 1361 | 1361 | severity = request.POST.get("severity", "") |
| 1362 | 1362 | if title: |
| 1363 | 1363 | from fossil.cli import FossilCLI |
| 1364 | 1364 | |
| 1365 | 1365 | cli = FossilCLI() |
| 1366 | + priority = request.POST.get("priority", "") | |
| 1366 | 1367 | fields = {"title": title, "type": ticket_type, "comment": body, "status": "Open"} |
| 1367 | 1368 | if severity: |
| 1368 | 1369 | fields["severity"] = severity |
| 1370 | + if priority: | |
| 1371 | + fields["priority"] = priority | |
| 1369 | 1372 | # Collect custom field values |
| 1370 | 1373 | for cf in custom_fields: |
| 1371 | 1374 | if cf.field_type == "checkbox": |
| 1372 | 1375 | val = "1" if request.POST.get(f"custom_{cf.name}") == "on" else "0" |
| 1373 | 1376 | else: |
| @@ -2919,21 +2922,16 @@ | ||
| 2919 | 2922 | rid_to_idx[entry.rid] = i |
| 2920 | 2923 | if entry.event_type == "ci": |
| 2921 | 2924 | rid_to_rail[entry.rid] = max(entry.rail, 0) |
| 2922 | 2925 | |
| 2923 | 2926 | # Track which rids have a child on the same rail (for leaf detection). |
| 2924 | - # Also track which rails have had a previous entry (for fork detection: | |
| 2925 | - # first entry on a rail whose parent is on a different rail = fork). | |
| 2926 | 2927 | has_child_on_rail: set[int] = set() # parent rids that have a same-rail child |
| 2927 | - rail_first_seen: dict[int, int] = {} # rail -> index of first entry on that rail | |
| 2928 | 2928 | |
| 2929 | - for i, entry in enumerate(entries): | |
| 2929 | + for _i, entry in enumerate(entries): | |
| 2930 | 2930 | if entry.event_type != "ci": |
| 2931 | 2931 | continue |
| 2932 | 2932 | rail = max(entry.rail, 0) |
| 2933 | - if rail not in rail_first_seen: | |
| 2934 | - rail_first_seen[rail] = i | |
| 2935 | 2933 | # Mark the primary parent as having a child on this rail |
| 2936 | 2934 | if entry.parent_rid in rid_to_rail and rid_to_rail[entry.parent_rid] == rail: |
| 2937 | 2935 | has_child_on_rail.add(entry.parent_rid) |
| 2938 | 2936 | |
| 2939 | 2937 | # Precompute: for each checkin, the range of rows its vertical line spans |
| @@ -2956,15 +2954,17 @@ | ||
| 2956 | 2954 | for i, entry in enumerate(entries): |
| 2957 | 2955 | if entry.event_type != "ci": |
| 2958 | 2956 | continue |
| 2959 | 2957 | child_rail = max(entry.rail, 0) |
| 2960 | 2958 | |
| 2961 | - # Fork detection: this entry's primary parent is on a different rail, | |
| 2962 | - # and this is the first entry we've seen on this rail. | |
| 2959 | + # Fork detection: primary parent is on a different rail = actual branch point. | |
| 2960 | + # Draw the connector at the fork commit itself (the oldest entry on the branch), | |
| 2961 | + # not at the newest entry. In newest-first order, this is the entry whose parent | |
| 2962 | + # is on a different rail — there is exactly one such entry per branch. | |
| 2963 | 2963 | if entry.parent_rid in rid_to_rail: |
| 2964 | 2964 | parent_rail = rid_to_rail[entry.parent_rid] |
| 2965 | - if child_rail != parent_rail and rail_first_seen.get(child_rail) == i: | |
| 2965 | + if child_rail != parent_rail: | |
| 2966 | 2966 | row_fork_from[i] = parent_rail |
| 2967 | 2967 | # Draw the fork connector at this row (where the branch starts) |
| 2968 | 2968 | left_rail = min(child_rail, parent_rail) |
| 2969 | 2969 | right_rail = max(child_rail, parent_rail) |
| 2970 | 2970 | left_x = rail_offset + left_rail * rail_pitch |
| @@ -4427,5 +4427,105 @@ | ||
| 4427 | 4427 | "error": error, |
| 4428 | 4428 | "table_names": table_names, |
| 4429 | 4429 | "active_tab": "explorer", |
| 4430 | 4430 | }, |
| 4431 | 4431 | ) |
| 4432 | + | |
| 4433 | + | |
| 4434 | +# --- Bundle Export / Import --- | |
| 4435 | + | |
| 4436 | + | |
| 4437 | +@login_required | |
| 4438 | +def bundle_export(request, slug): | |
| 4439 | + """Export a Fossil bundle file for download.""" | |
| 4440 | + project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin") | |
| 4441 | + branch = request.GET.get("branch", "").strip() | |
| 4442 | + checkin = request.GET.get("checkin", "").strip() | |
| 4443 | + | |
| 4444 | + from fossil.cli import FossilCLI | |
| 4445 | + | |
| 4446 | + cli = FossilCLI() | |
| 4447 | + data = cli.bundle_export(fossil_repo.full_path, branch=branch, checkin=checkin) | |
| 4448 | + if not data: | |
| 4449 | + raise Http404("Bundle export failed") | |
| 4450 | + | |
| 4451 | + filename = f"{project.slug}-{branch or checkin or 'trunk'}.bundle" | |
| 4452 | + response = HttpResponse(data, content_type="application/octet-stream") | |
| 4453 | + response["Content-Disposition"] = f'attachment; filename="{filename}"' | |
| 4454 | + return response | |
| 4455 | + | |
| 4456 | + | |
| 4457 | +@login_required | |
| 4458 | +def bundle_import(request, slug): | |
| 4459 | + """Import a Fossil bundle file into the repository.""" | |
| 4460 | + project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin") | |
| 4461 | + | |
| 4462 | + if request.method == "POST": | |
| 4463 | + bundle_file = request.FILES.get("bundle") | |
| 4464 | + publish = request.POST.get("publish") == "on" | |
| 4465 | + if bundle_file: | |
| 4466 | + from fossil.cli import FossilCLI | |
| 4467 | + | |
| 4468 | + cli = FossilCLI() | |
| 4469 | + ok = cli.bundle_import(fossil_repo.full_path, bundle_file.read(), publish=publish) | |
| 4470 | + | |
| 4471 | + from django.contrib import messages | |
| 4472 | + | |
| 4473 | + if ok: | |
| 4474 | + messages.success(request, "Bundle imported successfully.") | |
| 4475 | + else: | |
| 4476 | + messages.error(request, "Bundle import failed. Check the file is a valid Fossil bundle.") | |
| 4477 | + return redirect("fossil:repo_settings", slug=slug) | |
| 4478 | + | |
| 4479 | + return render(request, "fossil/bundle_import.html", {"project": project, "active_tab": "settings"}) | |
| 4480 | + | |
| 4481 | + | |
| 4482 | +# --- Chat --- | |
| 4483 | + | |
| 4484 | + | |
| 4485 | +@login_required | |
| 4486 | +def chat_room(request, slug): | |
| 4487 | + project, fossil_repo, reader = _get_repo_and_reader(slug, request) | |
| 4488 | + from fossil.chat import ChatMessage | |
| 4489 | + | |
| 4490 | + messages = ChatMessage.objects.filter(repository=fossil_repo).select_related("user").order_by("-created_at")[:50] | |
| 4491 | + messages = list(reversed(messages)) | |
| 4492 | + return render( | |
| 4493 | + request, | |
| 4494 | + "fossil/chat.html", | |
| 4495 | + { | |
| 4496 | + "project": project, | |
| 4497 | + "fossil_repo": fossil_repo, | |
| 4498 | + "messages": messages, | |
| 4499 | + "active_tab": "chat", | |
| 4500 | + }, | |
| 4501 | + ) | |
| 4502 | + | |
| 4503 | + | |
| 4504 | +@login_required | |
| 4505 | +def chat_send(request, slug): | |
| 4506 | + from fossil.chat import ChatMessage | |
| 4507 | + | |
| 4508 | + if request.method == "POST": | |
| 4509 | + project, fossil_repo, reader = _get_repo_and_reader(slug, request, "write") | |
| 4510 | + body = request.POST.get("body", "").strip() | |
| 4511 | + if body: | |
| 4512 | + ChatMessage.objects.create( | |
| 4513 | + repository=fossil_repo, | |
| 4514 | + user=request.user, | |
| 4515 | + username=request.user.username, | |
| 4516 | + body=body[:2000], | |
| 4517 | + ) | |
| 4518 | + else: | |
| 4519 | + project, fossil_repo, reader = _get_repo_and_reader(slug, request) | |
| 4520 | + | |
| 4521 | + # Return the updated message list partial for HTMX swap | |
| 4522 | + chat_messages = ChatMessage.objects.filter(repository=fossil_repo).select_related("user").order_by("-created_at")[:50] | |
| 4523 | + chat_messages = list(reversed(chat_messages)) | |
| 4524 | + return render( | |
| 4525 | + request, | |
| 4526 | + "fossil/partials/chat_messages.html", | |
| 4527 | + { | |
| 4528 | + "project": project, | |
| 4529 | + "messages": chat_messages, | |
| 4530 | + }, | |
| 4531 | + ) | |
| 4432 | 4532 |
| --- fossil/views.py | |
| +++ fossil/views.py | |
| @@ -41,11 +41,11 @@ | |
| 41 | path = m.group(1).strip() |
| 42 | text = m.group(2).strip() |
| 43 | if path.startswith("./"): |
| 44 | path = "/" + base_path + path[2:] |
| 45 | elif not path.startswith("/") and not path.startswith("http"): |
| 46 | path = "/" + base_path + path |
| 47 | return f"[{text}]({path})" |
| 48 | |
| 49 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_to_md_link, content) |
| 50 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL) |
| 51 | html = md.markdown(content, extensions=["fenced_code", "tables", "toc", "footnotes", "def_list", "attr_list"]) |
| @@ -73,21 +73,21 @@ | |
| 73 | text = match.group(2).strip() |
| 74 | # Convert relative paths to absolute using base_path |
| 75 | if path.startswith("./"): |
| 76 | path = "/" + base_path + path[2:] |
| 77 | elif not path.startswith("/") and not path.startswith("http"): |
| 78 | path = "/" + base_path + path |
| 79 | return f'<a href="{path}">{text}</a>' |
| 80 | |
| 81 | # Match [path | text] with flexible whitespace around the pipe |
| 82 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_link_replace, content) |
| 83 | # Interwiki links: [wikipedia:Article] -> external link |
| 84 | content = re.sub(r"\[wikipedia:([^\]]+)\]", r'<a href="https://en.wikipedia.org/wiki/\1">\1</a>', content) |
| 85 | # Anchor links: [#anchor-name] -> local anchor |
| 86 | content = re.sub(r"\[#([^\]]+)\]", r'<a href="#\1">\1</a>', content) |
| 87 | # Bare wiki links: [PageName] (no pipe, not a URL) |
| 88 | content = re.sub(r"\[([A-Z][a-zA-Z0-9_]+)\]", r'<a href="\1">\1</a>', content) |
| 89 | |
| 90 | # Verbatim blocks |
| 91 | # Pikchr diagrams: <verbatim type="pikchr">...</verbatim> → SVG |
| 92 | def _render_pikchr_block(m): |
| 93 | try: |
| @@ -1361,13 +1361,16 @@ | |
| 1361 | severity = request.POST.get("severity", "") |
| 1362 | if title: |
| 1363 | from fossil.cli import FossilCLI |
| 1364 | |
| 1365 | cli = FossilCLI() |
| 1366 | fields = {"title": title, "type": ticket_type, "comment": body, "status": "Open"} |
| 1367 | if severity: |
| 1368 | fields["severity"] = severity |
| 1369 | # Collect custom field values |
| 1370 | for cf in custom_fields: |
| 1371 | if cf.field_type == "checkbox": |
| 1372 | val = "1" if request.POST.get(f"custom_{cf.name}") == "on" else "0" |
| 1373 | else: |
| @@ -2919,21 +2922,16 @@ | |
| 2919 | rid_to_idx[entry.rid] = i |
| 2920 | if entry.event_type == "ci": |
| 2921 | rid_to_rail[entry.rid] = max(entry.rail, 0) |
| 2922 | |
| 2923 | # Track which rids have a child on the same rail (for leaf detection). |
| 2924 | # Also track which rails have had a previous entry (for fork detection: |
| 2925 | # first entry on a rail whose parent is on a different rail = fork). |
| 2926 | has_child_on_rail: set[int] = set() # parent rids that have a same-rail child |
| 2927 | rail_first_seen: dict[int, int] = {} # rail -> index of first entry on that rail |
| 2928 | |
| 2929 | for i, entry in enumerate(entries): |
| 2930 | if entry.event_type != "ci": |
| 2931 | continue |
| 2932 | rail = max(entry.rail, 0) |
| 2933 | if rail not in rail_first_seen: |
| 2934 | rail_first_seen[rail] = i |
| 2935 | # Mark the primary parent as having a child on this rail |
| 2936 | if entry.parent_rid in rid_to_rail and rid_to_rail[entry.parent_rid] == rail: |
| 2937 | has_child_on_rail.add(entry.parent_rid) |
| 2938 | |
| 2939 | # Precompute: for each checkin, the range of rows its vertical line spans |
| @@ -2956,15 +2954,17 @@ | |
| 2956 | for i, entry in enumerate(entries): |
| 2957 | if entry.event_type != "ci": |
| 2958 | continue |
| 2959 | child_rail = max(entry.rail, 0) |
| 2960 | |
| 2961 | # Fork detection: this entry's primary parent is on a different rail, |
| 2962 | # and this is the first entry we've seen on this rail. |
| 2963 | if entry.parent_rid in rid_to_rail: |
| 2964 | parent_rail = rid_to_rail[entry.parent_rid] |
| 2965 | if child_rail != parent_rail and rail_first_seen.get(child_rail) == i: |
| 2966 | row_fork_from[i] = parent_rail |
| 2967 | # Draw the fork connector at this row (where the branch starts) |
| 2968 | left_rail = min(child_rail, parent_rail) |
| 2969 | right_rail = max(child_rail, parent_rail) |
| 2970 | left_x = rail_offset + left_rail * rail_pitch |
| @@ -4427,5 +4427,105 @@ | |
| 4427 | "error": error, |
| 4428 | "table_names": table_names, |
| 4429 | "active_tab": "explorer", |
| 4430 | }, |
| 4431 | ) |
| 4432 |
| --- fossil/views.py | |
| +++ fossil/views.py | |
| @@ -41,11 +41,11 @@ | |
| 41 | path = m.group(1).strip() |
| 42 | text = m.group(2).strip() |
| 43 | if path.startswith("./"): |
| 44 | path = "/" + base_path + path[2:] |
| 45 | elif not path.startswith("/") and not path.startswith("http"): |
| 46 | path = "/" + base_path + path if base_path else "/wiki/" + path |
| 47 | return f"[{text}]({path})" |
| 48 | |
| 49 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_to_md_link, content) |
| 50 | content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL) |
| 51 | html = md.markdown(content, extensions=["fenced_code", "tables", "toc", "footnotes", "def_list", "attr_list"]) |
| @@ -73,21 +73,21 @@ | |
| 73 | text = match.group(2).strip() |
| 74 | # Convert relative paths to absolute using base_path |
| 75 | if path.startswith("./"): |
| 76 | path = "/" + base_path + path[2:] |
| 77 | elif not path.startswith("/") and not path.startswith("http"): |
| 78 | path = "/" + base_path + path if base_path else "/wiki/" + path |
| 79 | return f'<a href="{path}">{text}</a>' |
| 80 | |
| 81 | # Match [path | text] with flexible whitespace around the pipe |
| 82 | content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_link_replace, content) |
| 83 | # Interwiki links: [wikipedia:Article] -> external link |
| 84 | content = re.sub(r"\[wikipedia:([^\]]+)\]", r'<a href="https://en.wikipedia.org/wiki/\1">\1</a>', content) |
| 85 | # Anchor links: [#anchor-name] -> local anchor |
| 86 | content = re.sub(r"\[#([^\]]+)\]", r'<a href="#\1">\1</a>', content) |
| 87 | # Bare wiki links: [PageName] (no pipe, not a URL) — use /wiki/ prefix so _rewrite_fossil_links maps it correctly |
| 88 | content = re.sub(r"\[([A-Z][a-zA-Z0-9_]+)\]", r'<a href="/wiki/\1">\1</a>', content) |
| 89 | |
| 90 | # Verbatim blocks |
| 91 | # Pikchr diagrams: <verbatim type="pikchr">...</verbatim> → SVG |
| 92 | def _render_pikchr_block(m): |
| 93 | try: |
| @@ -1361,13 +1361,16 @@ | |
| 1361 | severity = request.POST.get("severity", "") |
| 1362 | if title: |
| 1363 | from fossil.cli import FossilCLI |
| 1364 | |
| 1365 | cli = FossilCLI() |
| 1366 | priority = request.POST.get("priority", "") |
| 1367 | fields = {"title": title, "type": ticket_type, "comment": body, "status": "Open"} |
| 1368 | if severity: |
| 1369 | fields["severity"] = severity |
| 1370 | if priority: |
| 1371 | fields["priority"] = priority |
| 1372 | # Collect custom field values |
| 1373 | for cf in custom_fields: |
| 1374 | if cf.field_type == "checkbox": |
| 1375 | val = "1" if request.POST.get(f"custom_{cf.name}") == "on" else "0" |
| 1376 | else: |
| @@ -2919,21 +2922,16 @@ | |
| 2922 | rid_to_idx[entry.rid] = i |
| 2923 | if entry.event_type == "ci": |
| 2924 | rid_to_rail[entry.rid] = max(entry.rail, 0) |
| 2925 | |
| 2926 | # Track which rids have a child on the same rail (for leaf detection). |
| 2927 | has_child_on_rail: set[int] = set() # parent rids that have a same-rail child |
| 2928 | |
| 2929 | for _i, entry in enumerate(entries): |
| 2930 | if entry.event_type != "ci": |
| 2931 | continue |
| 2932 | rail = max(entry.rail, 0) |
| 2933 | # Mark the primary parent as having a child on this rail |
| 2934 | if entry.parent_rid in rid_to_rail and rid_to_rail[entry.parent_rid] == rail: |
| 2935 | has_child_on_rail.add(entry.parent_rid) |
| 2936 | |
| 2937 | # Precompute: for each checkin, the range of rows its vertical line spans |
| @@ -2956,15 +2954,17 @@ | |
| 2954 | for i, entry in enumerate(entries): |
| 2955 | if entry.event_type != "ci": |
| 2956 | continue |
| 2957 | child_rail = max(entry.rail, 0) |
| 2958 | |
| 2959 | # Fork detection: primary parent is on a different rail = actual branch point. |
| 2960 | # Draw the connector at the fork commit itself (the oldest entry on the branch), |
| 2961 | # not at the newest entry. In newest-first order, this is the entry whose parent |
| 2962 | # is on a different rail — there is exactly one such entry per branch. |
| 2963 | if entry.parent_rid in rid_to_rail: |
| 2964 | parent_rail = rid_to_rail[entry.parent_rid] |
| 2965 | if child_rail != parent_rail: |
| 2966 | row_fork_from[i] = parent_rail |
| 2967 | # Draw the fork connector at this row (where the branch starts) |
| 2968 | left_rail = min(child_rail, parent_rail) |
| 2969 | right_rail = max(child_rail, parent_rail) |
| 2970 | left_x = rail_offset + left_rail * rail_pitch |
| @@ -4427,5 +4427,105 @@ | |
| 4427 | "error": error, |
| 4428 | "table_names": table_names, |
| 4429 | "active_tab": "explorer", |
| 4430 | }, |
| 4431 | ) |
| 4432 | |
| 4433 | |
| 4434 | # --- Bundle Export / Import --- |
| 4435 | |
| 4436 | |
| 4437 | @login_required |
| 4438 | def bundle_export(request, slug): |
| 4439 | """Export a Fossil bundle file for download.""" |
| 4440 | project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin") |
| 4441 | branch = request.GET.get("branch", "").strip() |
| 4442 | checkin = request.GET.get("checkin", "").strip() |
| 4443 | |
| 4444 | from fossil.cli import FossilCLI |
| 4445 | |
| 4446 | cli = FossilCLI() |
| 4447 | data = cli.bundle_export(fossil_repo.full_path, branch=branch, checkin=checkin) |
| 4448 | if not data: |
| 4449 | raise Http404("Bundle export failed") |
| 4450 | |
| 4451 | filename = f"{project.slug}-{branch or checkin or 'trunk'}.bundle" |
| 4452 | response = HttpResponse(data, content_type="application/octet-stream") |
| 4453 | response["Content-Disposition"] = f'attachment; filename="{filename}"' |
| 4454 | return response |
| 4455 | |
| 4456 | |
| 4457 | @login_required |
| 4458 | def bundle_import(request, slug): |
| 4459 | """Import a Fossil bundle file into the repository.""" |
| 4460 | project, fossil_repo, reader = _get_repo_and_reader(slug, request, "admin") |
| 4461 | |
| 4462 | if request.method == "POST": |
| 4463 | bundle_file = request.FILES.get("bundle") |
| 4464 | publish = request.POST.get("publish") == "on" |
| 4465 | if bundle_file: |
| 4466 | from fossil.cli import FossilCLI |
| 4467 | |
| 4468 | cli = FossilCLI() |
| 4469 | ok = cli.bundle_import(fossil_repo.full_path, bundle_file.read(), publish=publish) |
| 4470 | |
| 4471 | from django.contrib import messages |
| 4472 | |
| 4473 | if ok: |
| 4474 | messages.success(request, "Bundle imported successfully.") |
| 4475 | else: |
| 4476 | messages.error(request, "Bundle import failed. Check the file is a valid Fossil bundle.") |
| 4477 | return redirect("fossil:repo_settings", slug=slug) |
| 4478 | |
| 4479 | return render(request, "fossil/bundle_import.html", {"project": project, "active_tab": "settings"}) |
| 4480 | |
| 4481 | |
| 4482 | # --- Chat --- |
| 4483 | |
| 4484 | |
| 4485 | @login_required |
| 4486 | def chat_room(request, slug): |
| 4487 | project, fossil_repo, reader = _get_repo_and_reader(slug, request) |
| 4488 | from fossil.chat import ChatMessage |
| 4489 | |
| 4490 | messages = ChatMessage.objects.filter(repository=fossil_repo).select_related("user").order_by("-created_at")[:50] |
| 4491 | messages = list(reversed(messages)) |
| 4492 | return render( |
| 4493 | request, |
| 4494 | "fossil/chat.html", |
| 4495 | { |
| 4496 | "project": project, |
| 4497 | "fossil_repo": fossil_repo, |
| 4498 | "messages": messages, |
| 4499 | "active_tab": "chat", |
| 4500 | }, |
| 4501 | ) |
| 4502 | |
| 4503 | |
| 4504 | @login_required |
| 4505 | def chat_send(request, slug): |
| 4506 | from fossil.chat import ChatMessage |
| 4507 | |
| 4508 | if request.method == "POST": |
| 4509 | project, fossil_repo, reader = _get_repo_and_reader(slug, request, "write") |
| 4510 | body = request.POST.get("body", "").strip() |
| 4511 | if body: |
| 4512 | ChatMessage.objects.create( |
| 4513 | repository=fossil_repo, |
| 4514 | user=request.user, |
| 4515 | username=request.user.username, |
| 4516 | body=body[:2000], |
| 4517 | ) |
| 4518 | else: |
| 4519 | project, fossil_repo, reader = _get_repo_and_reader(slug, request) |
| 4520 | |
| 4521 | # Return the updated message list partial for HTMX swap |
| 4522 | chat_messages = ChatMessage.objects.filter(repository=fossil_repo).select_related("user").order_by("-created_at")[:50] |
| 4523 | chat_messages = list(reversed(chat_messages)) |
| 4524 | return render( |
| 4525 | request, |
| 4526 | "fossil/partials/chat_messages.html", |
| 4527 | { |
| 4528 | "project": project, |
| 4529 | "messages": chat_messages, |
| 4530 | }, |
| 4531 | ) |
| 4532 |
+1
| --- install.sh | ||
| +++ install.sh | ||
| @@ -10,10 +10,11 @@ | ||
| 10 | 10 | # curl -sSL https://get.fossilrepo.dev | bash |
| 11 | 11 | # -- or -- |
| 12 | 12 | # ./install.sh --docker --domain fossil.example.com --ssl |
| 13 | 13 | # |
| 14 | 14 | # https://github.com/ConflictHQ/fossilrepo |
| 15 | +# Copyright (c) 2026 Leo Mata & CONFLICT LLC | |
| 15 | 16 | # MIT License |
| 16 | 17 | # ============================================================================ |
| 17 | 18 | |
| 18 | 19 | set -euo pipefail |
| 19 | 20 | |
| 20 | 21 |
| --- install.sh | |
| +++ install.sh | |
| @@ -10,10 +10,11 @@ | |
| 10 | # curl -sSL https://get.fossilrepo.dev | bash |
| 11 | # -- or -- |
| 12 | # ./install.sh --docker --domain fossil.example.com --ssl |
| 13 | # |
| 14 | # https://github.com/ConflictHQ/fossilrepo |
| 15 | # MIT License |
| 16 | # ============================================================================ |
| 17 | |
| 18 | set -euo pipefail |
| 19 | |
| 20 |
| --- install.sh | |
| +++ install.sh | |
| @@ -10,10 +10,11 @@ | |
| 10 | # curl -sSL https://get.fossilrepo.dev | bash |
| 11 | # -- or -- |
| 12 | # ./install.sh --docker --domain fossil.example.com --ssl |
| 13 | # |
| 14 | # https://github.com/ConflictHQ/fossilrepo |
| 15 | # Copyright (c) 2026 Leo Mata & CONFLICT LLC |
| 16 | # MIT License |
| 17 | # ============================================================================ |
| 18 | |
| 19 | set -euo pipefail |
| 20 | |
| 21 |
+5
-1
| --- mkdocs.yml | ||
| +++ mkdocs.yml | ||
| @@ -38,11 +38,11 @@ | ||
| 38 | 38 | - search.highlight |
| 39 | 39 | - content.code.copy |
| 40 | 40 | - content.tabs.link |
| 41 | 41 | - header.autohide |
| 42 | 42 | icon: |
| 43 | - repo: fontawesome/brands/github | |
| 43 | + repo: material/source-repository | |
| 44 | 44 | |
| 45 | 45 | extra_css: |
| 46 | 46 | - assets/css/custom.css |
| 47 | 47 | |
| 48 | 48 | plugins: |
| @@ -83,9 +83,13 @@ | ||
| 83 | 83 | - Reference: api/reference.md |
| 84 | 84 | - Agentic Development: api/agentic-development.md |
| 85 | 85 | |
| 86 | 86 | extra: |
| 87 | 87 | social: |
| 88 | + - icon: material/source-repository | |
| 89 | + link: https://fossilrepo.io/fossilrepo | |
| 90 | + name: Fossil Repository (primary) | |
| 88 | 91 | - icon: fontawesome/brands/github |
| 89 | 92 | link: https://github.com/ConflictHQ/fossilrepo |
| 93 | + name: GitHub Mirror | |
| 90 | 94 | |
| 91 | 95 | copyright: Copyright © 2026 CONFLICT LLC |
| 92 | 96 |
| --- mkdocs.yml | |
| +++ mkdocs.yml | |
| @@ -38,11 +38,11 @@ | |
| 38 | - search.highlight |
| 39 | - content.code.copy |
| 40 | - content.tabs.link |
| 41 | - header.autohide |
| 42 | icon: |
| 43 | repo: fontawesome/brands/github |
| 44 | |
| 45 | extra_css: |
| 46 | - assets/css/custom.css |
| 47 | |
| 48 | plugins: |
| @@ -83,9 +83,13 @@ | |
| 83 | - Reference: api/reference.md |
| 84 | - Agentic Development: api/agentic-development.md |
| 85 | |
| 86 | extra: |
| 87 | social: |
| 88 | - icon: fontawesome/brands/github |
| 89 | link: https://github.com/ConflictHQ/fossilrepo |
| 90 | |
| 91 | copyright: Copyright © 2026 CONFLICT LLC |
| 92 |
| --- mkdocs.yml | |
| +++ mkdocs.yml | |
| @@ -38,11 +38,11 @@ | |
| 38 | - search.highlight |
| 39 | - content.code.copy |
| 40 | - content.tabs.link |
| 41 | - header.autohide |
| 42 | icon: |
| 43 | repo: material/source-repository |
| 44 | |
| 45 | extra_css: |
| 46 | - assets/css/custom.css |
| 47 | |
| 48 | plugins: |
| @@ -83,9 +83,13 @@ | |
| 83 | - Reference: api/reference.md |
| 84 | - Agentic Development: api/agentic-development.md |
| 85 | |
| 86 | extra: |
| 87 | social: |
| 88 | - icon: material/source-repository |
| 89 | link: https://fossilrepo.io/fossilrepo |
| 90 | name: Fossil Repository (primary) |
| 91 | - icon: fontawesome/brands/github |
| 92 | link: https://github.com/ConflictHQ/fossilrepo |
| 93 | name: GitHub Mirror |
| 94 | |
| 95 | copyright: Copyright © 2026 CONFLICT LLC |
| 96 |
+1
| --- pyproject.toml | ||
| +++ pyproject.toml | ||
| @@ -4,10 +4,11 @@ | ||
| 4 | 4 | description = "Self-hosted Fossil SCM forge — code hosting, issues, wiki, and continuous backups in one command." |
| 5 | 5 | license = "MIT" |
| 6 | 6 | requires-python = ">=3.12" |
| 7 | 7 | readme = "README.md" |
| 8 | 8 | authors = [ |
| 9 | + { name = "Leo Mata", email = "[email protected]" }, | |
| 9 | 10 | { name = "CONFLICT LLC", email = "[email protected]" }, |
| 10 | 11 | ] |
| 11 | 12 | keywords = ["fossil", "scm", "vcs", "code-hosting", "self-hosted", "forge"] |
| 12 | 13 | classifiers = [ |
| 13 | 14 | "Development Status :: 3 - Alpha", |
| 14 | 15 |
| --- pyproject.toml | |
| +++ pyproject.toml | |
| @@ -4,10 +4,11 @@ | |
| 4 | description = "Self-hosted Fossil SCM forge — code hosting, issues, wiki, and continuous backups in one command." |
| 5 | license = "MIT" |
| 6 | requires-python = ">=3.12" |
| 7 | readme = "README.md" |
| 8 | authors = [ |
| 9 | { name = "CONFLICT LLC", email = "[email protected]" }, |
| 10 | ] |
| 11 | keywords = ["fossil", "scm", "vcs", "code-hosting", "self-hosted", "forge"] |
| 12 | classifiers = [ |
| 13 | "Development Status :: 3 - Alpha", |
| 14 |
| --- pyproject.toml | |
| +++ pyproject.toml | |
| @@ -4,10 +4,11 @@ | |
| 4 | description = "Self-hosted Fossil SCM forge — code hosting, issues, wiki, and continuous backups in one command." |
| 5 | license = "MIT" |
| 6 | requires-python = ">=3.12" |
| 7 | readme = "README.md" |
| 8 | authors = [ |
| 9 | { name = "Leo Mata", email = "[email protected]" }, |
| 10 | { name = "CONFLICT LLC", email = "[email protected]" }, |
| 11 | ] |
| 12 | keywords = ["fossil", "scm", "vcs", "code-hosting", "self-hosted", "forge"] |
| 13 | classifiers = [ |
| 14 | "Development Status :: 3 - Alpha", |
| 15 |
| --- templates/fossil/_project_nav.html | ||
| +++ templates/fossil/_project_nav.html | ||
| @@ -24,10 +24,14 @@ | ||
| 24 | 24 | Wiki |
| 25 | 25 | </a> |
| 26 | 26 | <a href="{% url 'fossil:forum' slug=project.slug %}" |
| 27 | 27 | class="px-3 py-2.5 sm:px-4 sm:py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'forum' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 transition-colors{% endif %}"> |
| 28 | 28 | Forum |
| 29 | + </a> | |
| 30 | + <a href="{% url 'fossil:chat' slug=project.slug %}" | |
| 31 | + class="px-3 py-2.5 sm:px-4 sm:py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'chat' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 transition-colors{% endif %}"> | |
| 32 | + Chat | |
| 29 | 33 | </a> |
| 30 | 34 | <a href="{% url 'fossil:releases' slug=project.slug %}" |
| 31 | 35 | class="px-3 py-2.5 sm:px-4 sm:py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'releases' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 transition-colors{% endif %}"> |
| 32 | 36 | Releases |
| 33 | 37 | </a> |
| 34 | 38 | |
| 35 | 39 | ADDED templates/fossil/bundle_import.html |
| 36 | 40 | ADDED templates/fossil/chat.html |
| --- templates/fossil/_project_nav.html | |
| +++ templates/fossil/_project_nav.html | |
| @@ -24,10 +24,14 @@ | |
| 24 | Wiki |
| 25 | </a> |
| 26 | <a href="{% url 'fossil:forum' slug=project.slug %}" |
| 27 | class="px-3 py-2.5 sm:px-4 sm:py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'forum' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 transition-colors{% endif %}"> |
| 28 | Forum |
| 29 | </a> |
| 30 | <a href="{% url 'fossil:releases' slug=project.slug %}" |
| 31 | class="px-3 py-2.5 sm:px-4 sm:py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'releases' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 transition-colors{% endif %}"> |
| 32 | Releases |
| 33 | </a> |
| 34 | |
| 35 | DDED templates/fossil/bundle_import.html |
| 36 | DDED templates/fossil/chat.html |
| --- templates/fossil/_project_nav.html | |
| +++ templates/fossil/_project_nav.html | |
| @@ -24,10 +24,14 @@ | |
| 24 | Wiki |
| 25 | </a> |
| 26 | <a href="{% url 'fossil:forum' slug=project.slug %}" |
| 27 | class="px-3 py-2.5 sm:px-4 sm:py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'forum' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 transition-colors{% endif %}"> |
| 28 | Forum |
| 29 | </a> |
| 30 | <a href="{% url 'fossil:chat' slug=project.slug %}" |
| 31 | class="px-3 py-2.5 sm:px-4 sm:py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'chat' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 transition-colors{% endif %}"> |
| 32 | Chat |
| 33 | </a> |
| 34 | <a href="{% url 'fossil:releases' slug=project.slug %}" |
| 35 | class="px-3 py-2.5 sm:px-4 sm:py-2 text-sm font-medium rounded-t-md whitespace-nowrap {% if active_tab == 'releases' %}bg-gray-800 text-gray-100 border-b-2 border-brand{% else %}text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 transition-colors{% endif %}"> |
| 36 | Releases |
| 37 | </a> |
| 38 | |
| 39 | DDED templates/fossil/bundle_import.html |
| 40 | DDED templates/fossil/chat.html |
| --- a/templates/fossil/bundle_import.html | ||
| +++ b/templates/fossil/bundle_import.html | ||
| @@ -0,0 +1,29 @@ | ||
| 1 | +{% extends "base.html" %} | |
| 2 | +{% block title %}Import Bundle — {{ project.name }} — Fossilrepo{% endblock %} | |
| 3 | + | |
| 4 | +{% block content %} | |
| 5 | +<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> | |
| 6 | +{% include "fossil/_project_nav.html" %} | |
| 7 | + | |
| 8 | +<div class="mx-auto max-w-xl mt-6"> | |
| 9 | + <h2 class="text-xl font-bold text-gray-100 mb-4">Import Fossil Bundle</h2> | |
| 10 | + <form method="post" enctype="multipart/form-data" class="space-y-4 rounded-lg bg-gray-800 p-6 border border-gray-700"> | |
| 11 | + {% csrf_token %} | |
| 12 | + <div> | |
| 13 | + <label class="block text-sm font-medium text-gray-300 mb-1">Bundle file <span class="text-red-400">*</span></label> | |
| 14 | + <input type="file" name="bundle" required accept=".bundle" | |
| 15 | + class="w-full text-sm text-gray-300 file:mr-3 file:py-1.5 file:px-3 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-gray-700 file:text-gray-200 hover:file:bg-gray-600"> | |
| 16 | + <p class="mt-1 text-xs text-gray-500">A .bundle file exported from fossil bundle export.</p> | |
| 17 | + </div> | |
| 18 | + <div class="flex items-center gap-2"> | |
| 19 | + <input type="checkbox" name="publish" id="publish" class="rounded border-gray-600 bg-gray-900 text-brand focus:ring-brand"> | |
| 20 | + <label for="publish" class="text-sm text-gray-300">Publish (make imported artifacts public)</label> | |
| 21 | + </div> | |
| 22 | + <div class="flex justify-end gap-3 pt-2"> | |
| 23 | + <a href="{% url 'fossil:repo_settings' slug=project.slug %}" | |
| 24 | + class="rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">Cancel</a> | |
| 25 | + <button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">Import Bundle</button> | |
| 26 | + </div> | |
| 27 | + </form> | |
| 28 | +</div> | |
| 29 | +{% endblock %} |
| --- a/templates/fossil/bundle_import.html | |
| +++ b/templates/fossil/bundle_import.html | |
| @@ -0,0 +1,29 @@ | |
| --- a/templates/fossil/bundle_import.html | |
| +++ b/templates/fossil/bundle_import.html | |
| @@ -0,0 +1,29 @@ | |
| 1 | {% extends "base.html" %} |
| 2 | {% block title %}Import Bundle — {{ project.name }} — Fossilrepo{% endblock %} |
| 3 | |
| 4 | {% block content %} |
| 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | {% include "fossil/_project_nav.html" %} |
| 7 | |
| 8 | <div class="mx-auto max-w-xl mt-6"> |
| 9 | <h2 class="text-xl font-bold text-gray-100 mb-4">Import Fossil Bundle</h2> |
| 10 | <form method="post" enctype="multipart/form-data" class="space-y-4 rounded-lg bg-gray-800 p-6 border border-gray-700"> |
| 11 | {% csrf_token %} |
| 12 | <div> |
| 13 | <label class="block text-sm font-medium text-gray-300 mb-1">Bundle file <span class="text-red-400">*</span></label> |
| 14 | <input type="file" name="bundle" required accept=".bundle" |
| 15 | class="w-full text-sm text-gray-300 file:mr-3 file:py-1.5 file:px-3 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-gray-700 file:text-gray-200 hover:file:bg-gray-600"> |
| 16 | <p class="mt-1 text-xs text-gray-500">A .bundle file exported from fossil bundle export.</p> |
| 17 | </div> |
| 18 | <div class="flex items-center gap-2"> |
| 19 | <input type="checkbox" name="publish" id="publish" class="rounded border-gray-600 bg-gray-900 text-brand focus:ring-brand"> |
| 20 | <label for="publish" class="text-sm text-gray-300">Publish (make imported artifacts public)</label> |
| 21 | </div> |
| 22 | <div class="flex justify-end gap-3 pt-2"> |
| 23 | <a href="{% url 'fossil:repo_settings' slug=project.slug %}" |
| 24 | class="rounded-md bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">Cancel</a> |
| 25 | <button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">Import Bundle</button> |
| 26 | </div> |
| 27 | </form> |
| 28 | </div> |
| 29 | {% endblock %} |
| --- a/templates/fossil/chat.html | ||
| +++ b/templates/fossil/chat.html | ||
| @@ -0,0 +1,40 @@ | ||
| 1 | +{% extends "base.html" %} | |
| 2 | +{% load fossil_filters %} | |
| 3 | +{% block title %}Chat — {{ 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="rounded-lg bg-gray-800 border border-gray-700 flex flex-col" style="height: 600px;"> | |
| 10 | + <div class="px-4 py-3 border-b border-gray-700 flex-shrink-0"> | |
| 11 | + <h2 class="text-sm font-medium text-gray-300">Project Chat</h2> | |
| 12 | + </div> | |
| 13 | + | |
| 14 | + <!-- Message list --> | |
| 15 | + <div id="chat-messages" | |
| 16 | + class="flex-1 overflow-y-auto px-4 py-3 space-y-2" | |
| 17 | + hx-get="{% url 'fossil:chat_send' slug=project.slug %}" | |
| 18 | + hx-trigger="every 5s" | |
| 19 | + hx-swap="innerHTML"> | |
| 20 | + {% include "fossil/partials/chat_messages.html" %} | |
| 21 | + </div> | |
| 22 | + | |
| 23 | + <!-- Input form --> | |
| 24 | + <div class="px-4 py-3 border-t border-gray-700 flex-shrink-0"> | |
| 25 | + <form hx-post="{% url 'fossil:chat_send' slug=project.slug %}" | |
| 26 | + hx-target="#chat-messages" | |
| 27 | + hx-swap="innerHTML" | |
| 28 | + hx-on::after-request="this.reset(); document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight" | |
| 29 | + class="flex gap-2"> | |
| 30 | + {% csrf_token %} | |
| 31 | + <input type="text" name="body" placeholder="Send a message..." required maxlength="2000" autocomplete="off" | |
| 32 | + class="flex-1 rounded-md border-gray-600 bg-gray-900 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand text-sm transition-colors"> | |
| 33 | + <button type="submit" | |
| 34 | + class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white hover:bg-brand-hover transition-colors flex-shrink-0"> | |
| 35 | + Send | |
| 36 | + </button> | |
| 37 | + </form> | |
| 38 | + </div> | |
| 39 | +</div> | |
| 40 | +{% endblock %} |
| --- a/templates/fossil/chat.html | |
| +++ b/templates/fossil/chat.html | |
| @@ -0,0 +1,40 @@ | |
| --- a/templates/fossil/chat.html | |
| +++ b/templates/fossil/chat.html | |
| @@ -0,0 +1,40 @@ | |
| 1 | {% extends "base.html" %} |
| 2 | {% load fossil_filters %} |
| 3 | {% block title %}Chat — {{ 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="rounded-lg bg-gray-800 border border-gray-700 flex flex-col" style="height: 600px;"> |
| 10 | <div class="px-4 py-3 border-b border-gray-700 flex-shrink-0"> |
| 11 | <h2 class="text-sm font-medium text-gray-300">Project Chat</h2> |
| 12 | </div> |
| 13 | |
| 14 | <!-- Message list --> |
| 15 | <div id="chat-messages" |
| 16 | class="flex-1 overflow-y-auto px-4 py-3 space-y-2" |
| 17 | hx-get="{% url 'fossil:chat_send' slug=project.slug %}" |
| 18 | hx-trigger="every 5s" |
| 19 | hx-swap="innerHTML"> |
| 20 | {% include "fossil/partials/chat_messages.html" %} |
| 21 | </div> |
| 22 | |
| 23 | <!-- Input form --> |
| 24 | <div class="px-4 py-3 border-t border-gray-700 flex-shrink-0"> |
| 25 | <form hx-post="{% url 'fossil:chat_send' slug=project.slug %}" |
| 26 | hx-target="#chat-messages" |
| 27 | hx-swap="innerHTML" |
| 28 | hx-on::after-request="this.reset(); document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight" |
| 29 | class="flex gap-2"> |
| 30 | {% csrf_token %} |
| 31 | <input type="text" name="body" placeholder="Send a message..." required maxlength="2000" autocomplete="off" |
| 32 | class="flex-1 rounded-md border-gray-600 bg-gray-900 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand text-sm transition-colors"> |
| 33 | <button type="submit" |
| 34 | class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white hover:bg-brand-hover transition-colors flex-shrink-0"> |
| 35 | Send |
| 36 | </button> |
| 37 | </form> |
| 38 | </div> |
| 39 | </div> |
| 40 | {% endblock %} |
| --- templates/fossil/checkin_detail.html | ||
| +++ templates/fossil/checkin_detail.html | ||
| @@ -61,11 +61,11 @@ | ||
| 61 | 61 | <div class="rounded-lg bg-gray-800 border border-gray-700 shadow-sm"> |
| 62 | 62 | <div class="px-6 py-5"> |
| 63 | 63 | <p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p> |
| 64 | 64 | <div class="mt-3 flex items-center gap-x-4 gap-y-1 flex-wrap text-sm"> |
| 65 | 65 | <a href="{% url 'fossil:user_activity' slug=project.slug username=checkin.user %}" class="font-medium text-gray-200 hover:text-brand-light">{{ checkin.user|display_user }}</a> |
| 66 | - <span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }}</span> | |
| 66 | + <span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }} UTC</span> | |
| 67 | 67 | {% if checkin.branch %} |
| 68 | 68 | <span class="inline-flex items-center rounded-md bg-brand/10 border border-brand/20 px-2 py-0.5 text-xs text-brand-light"> |
| 69 | 69 | {{ checkin.branch }} |
| 70 | 70 | </span> |
| 71 | 71 | {% endif %} |
| 72 | 72 |
| --- templates/fossil/checkin_detail.html | |
| +++ templates/fossil/checkin_detail.html | |
| @@ -61,11 +61,11 @@ | |
| 61 | <div class="rounded-lg bg-gray-800 border border-gray-700 shadow-sm"> |
| 62 | <div class="px-6 py-5"> |
| 63 | <p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p> |
| 64 | <div class="mt-3 flex items-center gap-x-4 gap-y-1 flex-wrap text-sm"> |
| 65 | <a href="{% url 'fossil:user_activity' slug=project.slug username=checkin.user %}" class="font-medium text-gray-200 hover:text-brand-light">{{ checkin.user|display_user }}</a> |
| 66 | <span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }}</span> |
| 67 | {% if checkin.branch %} |
| 68 | <span class="inline-flex items-center rounded-md bg-brand/10 border border-brand/20 px-2 py-0.5 text-xs text-brand-light"> |
| 69 | {{ checkin.branch }} |
| 70 | </span> |
| 71 | {% endif %} |
| 72 |
| --- templates/fossil/checkin_detail.html | |
| +++ templates/fossil/checkin_detail.html | |
| @@ -61,11 +61,11 @@ | |
| 61 | <div class="rounded-lg bg-gray-800 border border-gray-700 shadow-sm"> |
| 62 | <div class="px-6 py-5"> |
| 63 | <p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p> |
| 64 | <div class="mt-3 flex items-center gap-x-4 gap-y-1 flex-wrap text-sm"> |
| 65 | <a href="{% url 'fossil:user_activity' slug=project.slug username=checkin.user %}" class="font-medium text-gray-200 hover:text-brand-light">{{ checkin.user|display_user }}</a> |
| 66 | <span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }} UTC</span> |
| 67 | {% if checkin.branch %} |
| 68 | <span class="inline-flex items-center rounded-md bg-brand/10 border border-brand/20 px-2 py-0.5 text-xs text-brand-light"> |
| 69 | {{ checkin.branch }} |
| 70 | </span> |
| 71 | {% endif %} |
| 72 |
| --- templates/fossil/code_browser.html | ||
| +++ templates/fossil/code_browser.html | ||
| @@ -37,11 +37,11 @@ | ||
| 37 | 37 | {% if latest_commit %} |
| 38 | 38 | <div class="flex flex-wrap items-center gap-x-3 gap-y-1 mt-2 text-xs text-gray-500"> |
| 39 | 39 | <a href="{% url 'fossil:user_activity' slug=project.slug username=latest_commit.user %}" class="font-medium text-gray-300 hover:text-brand-light">{{ latest_commit.user|display_user }}</a> |
| 40 | 40 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=latest_commit.uuid %}" class="text-gray-400 truncate hover:text-brand-light">{{ latest_commit.comment|truncatechars:80 }}</a> |
| 41 | 41 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=latest_commit.uuid %}" class="font-mono text-brand-light hover:text-brand">{{ latest_commit.uuid|truncatechars:10 }}</a> |
| 42 | - <span>{{ latest_commit.timestamp|date:"Y-m-d H:i" }}</span> | |
| 42 | + <span>{{ latest_commit.timestamp|date:"Y-m-d H:i" }} UTC</span> | |
| 43 | 43 | </div> |
| 44 | 44 | {% endif %} |
| 45 | 45 | </div> |
| 46 | 46 | |
| 47 | 47 | <!-- File table --> |
| 48 | 48 |
| --- templates/fossil/code_browser.html | |
| +++ templates/fossil/code_browser.html | |
| @@ -37,11 +37,11 @@ | |
| 37 | {% if latest_commit %} |
| 38 | <div class="flex flex-wrap items-center gap-x-3 gap-y-1 mt-2 text-xs text-gray-500"> |
| 39 | <a href="{% url 'fossil:user_activity' slug=project.slug username=latest_commit.user %}" class="font-medium text-gray-300 hover:text-brand-light">{{ latest_commit.user|display_user }}</a> |
| 40 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=latest_commit.uuid %}" class="text-gray-400 truncate hover:text-brand-light">{{ latest_commit.comment|truncatechars:80 }}</a> |
| 41 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=latest_commit.uuid %}" class="font-mono text-brand-light hover:text-brand">{{ latest_commit.uuid|truncatechars:10 }}</a> |
| 42 | <span>{{ latest_commit.timestamp|date:"Y-m-d H:i" }}</span> |
| 43 | </div> |
| 44 | {% endif %} |
| 45 | </div> |
| 46 | |
| 47 | <!-- File table --> |
| 48 |
| --- templates/fossil/code_browser.html | |
| +++ templates/fossil/code_browser.html | |
| @@ -37,11 +37,11 @@ | |
| 37 | {% if latest_commit %} |
| 38 | <div class="flex flex-wrap items-center gap-x-3 gap-y-1 mt-2 text-xs text-gray-500"> |
| 39 | <a href="{% url 'fossil:user_activity' slug=project.slug username=latest_commit.user %}" class="font-medium text-gray-300 hover:text-brand-light">{{ latest_commit.user|display_user }}</a> |
| 40 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=latest_commit.uuid %}" class="text-gray-400 truncate hover:text-brand-light">{{ latest_commit.comment|truncatechars:80 }}</a> |
| 41 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=latest_commit.uuid %}" class="font-mono text-brand-light hover:text-brand">{{ latest_commit.uuid|truncatechars:10 }}</a> |
| 42 | <span>{{ latest_commit.timestamp|date:"Y-m-d H:i" }} UTC</span> |
| 43 | </div> |
| 44 | {% endif %} |
| 45 | </div> |
| 46 | |
| 47 | <!-- File table --> |
| 48 |
+2
-2
| --- templates/fossil/compare.html | ||
| +++ templates/fossil/compare.html | ||
| @@ -59,16 +59,16 @@ | ||
| 59 | 59 | {% if from_detail and to_detail %} |
| 60 | 60 | <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6"> |
| 61 | 61 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 62 | 62 | <div class="text-xs text-gray-500 mb-1">From</div> |
| 63 | 63 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=from_detail.uuid %}" class="text-sm text-brand-light hover:text-brand">{{ from_detail.comment|truncatechars:60 }}</a> |
| 64 | - <div class="mt-1 text-xs text-gray-500">{{ from_detail.user|display_user }} · {{ from_detail.timestamp|date:"Y-m-d H:i" }}</div> | |
| 64 | + <div class="mt-1 text-xs text-gray-500">{{ from_detail.user|display_user }} · {{ from_detail.timestamp|date:"Y-m-d H:i" }} UTC</div> | |
| 65 | 65 | </div> |
| 66 | 66 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 67 | 67 | <div class="text-xs text-gray-500 mb-1">To</div> |
| 68 | 68 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=to_detail.uuid %}" class="text-sm text-brand-light hover:text-brand">{{ to_detail.comment|truncatechars:60 }}</a> |
| 69 | - <div class="mt-1 text-xs text-gray-500">{{ to_detail.user|display_user }} · {{ to_detail.timestamp|date:"Y-m-d H:i" }}</div> | |
| 69 | + <div class="mt-1 text-xs text-gray-500">{{ to_detail.user|display_user }} · {{ to_detail.timestamp|date:"Y-m-d H:i" }} UTC</div> | |
| 70 | 70 | </div> |
| 71 | 71 | </div> |
| 72 | 72 | |
| 73 | 73 | {% if file_diffs %} |
| 74 | 74 | <div x-data="{ mode: localStorage.getItem('diff-mode') || 'unified' }" x-init="$watch('mode', val => localStorage.setItem('diff-mode', val))" x-ref="diffToggle"> |
| 75 | 75 |
| --- templates/fossil/compare.html | |
| +++ templates/fossil/compare.html | |
| @@ -59,16 +59,16 @@ | |
| 59 | {% if from_detail and to_detail %} |
| 60 | <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6"> |
| 61 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 62 | <div class="text-xs text-gray-500 mb-1">From</div> |
| 63 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=from_detail.uuid %}" class="text-sm text-brand-light hover:text-brand">{{ from_detail.comment|truncatechars:60 }}</a> |
| 64 | <div class="mt-1 text-xs text-gray-500">{{ from_detail.user|display_user }} · {{ from_detail.timestamp|date:"Y-m-d H:i" }}</div> |
| 65 | </div> |
| 66 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 67 | <div class="text-xs text-gray-500 mb-1">To</div> |
| 68 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=to_detail.uuid %}" class="text-sm text-brand-light hover:text-brand">{{ to_detail.comment|truncatechars:60 }}</a> |
| 69 | <div class="mt-1 text-xs text-gray-500">{{ to_detail.user|display_user }} · {{ to_detail.timestamp|date:"Y-m-d H:i" }}</div> |
| 70 | </div> |
| 71 | </div> |
| 72 | |
| 73 | {% if file_diffs %} |
| 74 | <div x-data="{ mode: localStorage.getItem('diff-mode') || 'unified' }" x-init="$watch('mode', val => localStorage.setItem('diff-mode', val))" x-ref="diffToggle"> |
| 75 |
| --- templates/fossil/compare.html | |
| +++ templates/fossil/compare.html | |
| @@ -59,16 +59,16 @@ | |
| 59 | {% if from_detail and to_detail %} |
| 60 | <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6"> |
| 61 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 62 | <div class="text-xs text-gray-500 mb-1">From</div> |
| 63 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=from_detail.uuid %}" class="text-sm text-brand-light hover:text-brand">{{ from_detail.comment|truncatechars:60 }}</a> |
| 64 | <div class="mt-1 text-xs text-gray-500">{{ from_detail.user|display_user }} · {{ from_detail.timestamp|date:"Y-m-d H:i" }} UTC</div> |
| 65 | </div> |
| 66 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 67 | <div class="text-xs text-gray-500 mb-1">To</div> |
| 68 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=to_detail.uuid %}" class="text-sm text-brand-light hover:text-brand">{{ to_detail.comment|truncatechars:60 }}</a> |
| 69 | <div class="mt-1 text-xs text-gray-500">{{ to_detail.user|display_user }} · {{ to_detail.timestamp|date:"Y-m-d H:i" }} UTC</div> |
| 70 | </div> |
| 71 | </div> |
| 72 | |
| 73 | {% if file_diffs %} |
| 74 | <div x-data="{ mode: localStorage.getItem('diff-mode') || 'unified' }" x-init="$watch('mode', val => localStorage.setItem('diff-mode', val))" x-ref="diffToggle"> |
| 75 |
| --- templates/fossil/file_history.html | ||
| +++ templates/fossil/file_history.html | ||
| @@ -24,11 +24,11 @@ | ||
| 24 | 24 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}" |
| 25 | 25 | class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:100 }}</a> |
| 26 | 26 | <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500"> |
| 27 | 27 | <a href="{% url 'fossil:user_activity' slug=project.slug username=commit.user %}" class="hover:text-gray-300">{{ commit.user|display_user }}</a> |
| 28 | 28 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}" class="font-mono text-brand-light hover:text-brand">{{ commit.uuid|truncatechars:10 }}</a> |
| 29 | - <span>{{ commit.timestamp|date:"Y-m-d H:i" }}</span> | |
| 29 | + <span>{{ commit.timestamp|date:"Y-m-d H:i" }} UTC</span> | |
| 30 | 30 | </div> |
| 31 | 31 | </div> |
| 32 | 32 | </div> |
| 33 | 33 | {% empty %} |
| 34 | 34 | <p class="text-sm text-gray-500 py-8 text-center">No history found.</p> |
| 35 | 35 | |
| 36 | 36 | ADDED templates/fossil/partials/chat_messages.html |
| --- templates/fossil/file_history.html | |
| +++ templates/fossil/file_history.html | |
| @@ -24,11 +24,11 @@ | |
| 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=commit.user %}" class="hover:text-gray-300">{{ commit.user|display_user }}</a> |
| 28 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}" class="font-mono text-brand-light hover:text-brand">{{ commit.uuid|truncatechars:10 }}</a> |
| 29 | <span>{{ commit.timestamp|date:"Y-m-d H:i" }}</span> |
| 30 | </div> |
| 31 | </div> |
| 32 | </div> |
| 33 | {% empty %} |
| 34 | <p class="text-sm text-gray-500 py-8 text-center">No history found.</p> |
| 35 | |
| 36 | DDED templates/fossil/partials/chat_messages.html |
| --- templates/fossil/file_history.html | |
| +++ templates/fossil/file_history.html | |
| @@ -24,11 +24,11 @@ | |
| 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=commit.user %}" class="hover:text-gray-300">{{ commit.user|display_user }}</a> |
| 28 | <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}" class="font-mono text-brand-light hover:text-brand">{{ commit.uuid|truncatechars:10 }}</a> |
| 29 | <span>{{ commit.timestamp|date:"Y-m-d H:i" }} UTC</span> |
| 30 | </div> |
| 31 | </div> |
| 32 | </div> |
| 33 | {% empty %} |
| 34 | <p class="text-sm text-gray-500 py-8 text-center">No history found.</p> |
| 35 | |
| 36 | DDED templates/fossil/partials/chat_messages.html |
| --- a/templates/fossil/partials/chat_messages.html | ||
| +++ b/templates/fossil/partials/chat_messages.html | ||
| @@ -0,0 +1,8 @@ | ||
| 1 | +{% load fossil_filters %} | |
| 2 | +{% for msg in messages %} | |
| 3 | +<div class="flex gap-2 text-sm" id="chat-msg-{{ msg.pk }}"> | |
| 4 | + <span class="font-medium text-brand-light flex-shrink-0">{{ msg.username|display_user }}</span> | |
| 5 | + <span class="text-gray-400 flex-shrink-0 text-xs mt-0.5">{{ msg.created_at|time:"H:i" }}</span> | |
| 6 | + <span class="text-gray-200 break-words min-w-0">{{ msg.body }}</span> | |
| 7 | +</div> | |
| 8 | +{% endfor %} |
| --- a/templates/fossil/partials/chat_messages.html | |
| +++ b/templates/fossil/partials/chat_messages.html | |
| @@ -0,0 +1,8 @@ | |
| --- a/templates/fossil/partials/chat_messages.html | |
| +++ b/templates/fossil/partials/chat_messages.html | |
| @@ -0,0 +1,8 @@ | |
| 1 | {% load fossil_filters %} |
| 2 | {% for msg in messages %} |
| 3 | <div class="flex gap-2 text-sm" id="chat-msg-{{ msg.pk }}"> |
| 4 | <span class="font-medium text-brand-light flex-shrink-0">{{ msg.username|display_user }}</span> |
| 5 | <span class="text-gray-400 flex-shrink-0 text-xs mt-0.5">{{ msg.created_at|time:"H:i" }}</span> |
| 6 | <span class="text-gray-200 break-words min-w-0">{{ msg.body }}</span> |
| 7 | </div> |
| 8 | {% endfor %} |
| --- templates/fossil/repo_settings.html | ||
| +++ templates/fossil/repo_settings.html | ||
| @@ -171,10 +171,26 @@ | ||
| 171 | 171 | <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/> |
| 172 | 172 | </svg> |
| 173 | 173 | </a> |
| 174 | 174 | </div> |
| 175 | 175 | </div> |
| 176 | + | |
| 177 | + <!-- Bundle export/import --> | |
| 178 | + <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> | |
| 179 | + <h3 class="text-sm font-medium text-gray-300 mb-3">Bundle</h3> | |
| 180 | + <p class="text-xs text-gray-500 mb-3">Export a Fossil bundle to back up or share a branch. Import a bundle to receive changes from another Fossil instance.</p> | |
| 181 | + <div class="flex flex-wrap gap-2"> | |
| 182 | + <a href="{% url 'fossil:bundle_export' slug=project.slug %}" | |
| 183 | + class="inline-flex items-center rounded-md bg-gray-700 px-3 py-1.5 text-xs font-semibold text-gray-300 hover:bg-gray-600 border border-gray-600"> | |
| 184 | + Export trunk bundle | |
| 185 | + </a> | |
| 186 | + <a href="{% url 'fossil:bundle_import' slug=project.slug %}" | |
| 187 | + class="inline-flex items-center rounded-md bg-gray-700 px-3 py-1.5 text-xs font-semibold text-gray-300 hover:bg-gray-600 border border-gray-600"> | |
| 188 | + Import bundle | |
| 189 | + </a> | |
| 190 | + </div> | |
| 191 | + </div> | |
| 176 | 192 | |
| 177 | 193 | <!-- Danger Zone --> |
| 178 | 194 | <div class="rounded-lg border-2 border-red-900/50 p-5"> |
| 179 | 195 | <h2 class="text-lg font-semibold text-red-400 mb-2">Danger Zone</h2> |
| 180 | 196 | <p class="text-sm text-gray-400 mb-4">Destructive operations that cannot be undone.</p> |
| 181 | 197 |
| --- templates/fossil/repo_settings.html | |
| +++ templates/fossil/repo_settings.html | |
| @@ -171,10 +171,26 @@ | |
| 171 | <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/> |
| 172 | </svg> |
| 173 | </a> |
| 174 | </div> |
| 175 | </div> |
| 176 | |
| 177 | <!-- Danger Zone --> |
| 178 | <div class="rounded-lg border-2 border-red-900/50 p-5"> |
| 179 | <h2 class="text-lg font-semibold text-red-400 mb-2">Danger Zone</h2> |
| 180 | <p class="text-sm text-gray-400 mb-4">Destructive operations that cannot be undone.</p> |
| 181 |
| --- templates/fossil/repo_settings.html | |
| +++ templates/fossil/repo_settings.html | |
| @@ -171,10 +171,26 @@ | |
| 171 | <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/> |
| 172 | </svg> |
| 173 | </a> |
| 174 | </div> |
| 175 | </div> |
| 176 | |
| 177 | <!-- Bundle export/import --> |
| 178 | <div class="rounded-lg bg-gray-800 border border-gray-700 p-4"> |
| 179 | <h3 class="text-sm font-medium text-gray-300 mb-3">Bundle</h3> |
| 180 | <p class="text-xs text-gray-500 mb-3">Export a Fossil bundle to back up or share a branch. Import a bundle to receive changes from another Fossil instance.</p> |
| 181 | <div class="flex flex-wrap gap-2"> |
| 182 | <a href="{% url 'fossil:bundle_export' slug=project.slug %}" |
| 183 | class="inline-flex items-center rounded-md bg-gray-700 px-3 py-1.5 text-xs font-semibold text-gray-300 hover:bg-gray-600 border border-gray-600"> |
| 184 | Export trunk bundle |
| 185 | </a> |
| 186 | <a href="{% url 'fossil:bundle_import' slug=project.slug %}" |
| 187 | class="inline-flex items-center rounded-md bg-gray-700 px-3 py-1.5 text-xs font-semibold text-gray-300 hover:bg-gray-600 border border-gray-600"> |
| 188 | Import bundle |
| 189 | </a> |
| 190 | </div> |
| 191 | </div> |
| 192 | |
| 193 | <!-- Danger Zone --> |
| 194 | <div class="rounded-lg border-2 border-red-900/50 p-5"> |
| 195 | <h2 class="text-lg font-semibold text-red-400 mb-2">Danger Zone</h2> |
| 196 | <p class="text-sm text-gray-400 mb-4">Destructive operations that cannot be undone.</p> |
| 197 |
| --- templates/fossil/technote_detail.html | ||
| +++ templates/fossil/technote_detail.html | ||
| @@ -18,11 +18,11 @@ | ||
| 18 | 18 | <h2 class="text-xl font-bold text-gray-100 mb-1">{{ note.comment }}</h2> |
| 19 | 19 | <div class="flex items-center gap-3 text-xs text-gray-500"> |
| 20 | 20 | <a href="{% url 'fossil:user_activity' slug=project.slug username=note.user %}" |
| 21 | 21 | class="hover:text-brand-light">{{ note.user|display_user }}</a> |
| 22 | 22 | <span class="font-mono text-brand-light">{{ note.uuid|truncatechars:16 }}</span> |
| 23 | - <span>{{ note.timestamp|date:"Y-m-d H:i" }}</span> | |
| 23 | + <span>{{ note.timestamp|date:"Y-m-d H:i" }} UTC</span> | |
| 24 | 24 | </div> |
| 25 | 25 | </div> |
| 26 | 26 | {% if has_write %} |
| 27 | 27 | <div class="flex items-center gap-2 flex-shrink-0"> |
| 28 | 28 | <a href="{% url 'fossil:technote_edit' slug=project.slug technote_id=note.uuid %}" |
| 29 | 29 |
| --- templates/fossil/technote_detail.html | |
| +++ templates/fossil/technote_detail.html | |
| @@ -18,11 +18,11 @@ | |
| 18 | <h2 class="text-xl font-bold text-gray-100 mb-1">{{ note.comment }}</h2> |
| 19 | <div class="flex items-center gap-3 text-xs text-gray-500"> |
| 20 | <a href="{% url 'fossil:user_activity' slug=project.slug username=note.user %}" |
| 21 | class="hover:text-brand-light">{{ note.user|display_user }}</a> |
| 22 | <span class="font-mono text-brand-light">{{ note.uuid|truncatechars:16 }}</span> |
| 23 | <span>{{ note.timestamp|date:"Y-m-d H:i" }}</span> |
| 24 | </div> |
| 25 | </div> |
| 26 | {% if has_write %} |
| 27 | <div class="flex items-center gap-2 flex-shrink-0"> |
| 28 | <a href="{% url 'fossil:technote_edit' slug=project.slug technote_id=note.uuid %}" |
| 29 |
| --- templates/fossil/technote_detail.html | |
| +++ templates/fossil/technote_detail.html | |
| @@ -18,11 +18,11 @@ | |
| 18 | <h2 class="text-xl font-bold text-gray-100 mb-1">{{ note.comment }}</h2> |
| 19 | <div class="flex items-center gap-3 text-xs text-gray-500"> |
| 20 | <a href="{% url 'fossil:user_activity' slug=project.slug username=note.user %}" |
| 21 | class="hover:text-brand-light">{{ note.user|display_user }}</a> |
| 22 | <span class="font-mono text-brand-light">{{ note.uuid|truncatechars:16 }}</span> |
| 23 | <span>{{ note.timestamp|date:"Y-m-d H:i" }} UTC</span> |
| 24 | </div> |
| 25 | </div> |
| 26 | {% if has_write %} |
| 27 | <div class="flex items-center gap-2 flex-shrink-0"> |
| 28 | <a href="{% url 'fossil:technote_edit' slug=project.slug technote_id=note.uuid %}" |
| 29 |
| --- templates/fossil/technote_list.html | ||
| +++ templates/fossil/technote_list.html | ||
| @@ -43,11 +43,11 @@ | ||
| 43 | 43 | <div class="flex-1 min-w-0"> |
| 44 | 44 | <p class="text-sm text-gray-200">{{ note.comment|truncatechars:200 }}</p> |
| 45 | 45 | <div class="mt-2 flex items-center gap-3 text-xs text-gray-500"> |
| 46 | 46 | <span class="hover:text-brand-light">{{ note.user|display_user }}</span> |
| 47 | 47 | <span class="font-mono text-brand-light">{{ note.uuid|truncatechars:10 }}</span> |
| 48 | - <span>{{ note.timestamp|date:"Y-m-d H:i" }}</span> | |
| 48 | + <span>{{ note.timestamp|date:"Y-m-d H:i" }} UTC</span> | |
| 49 | 49 | </div> |
| 50 | 50 | </div> |
| 51 | 51 | </div> |
| 52 | 52 | </a> |
| 53 | 53 | {% endfor %} |
| 54 | 54 |
| --- templates/fossil/technote_list.html | |
| +++ templates/fossil/technote_list.html | |
| @@ -43,11 +43,11 @@ | |
| 43 | <div class="flex-1 min-w-0"> |
| 44 | <p class="text-sm text-gray-200">{{ note.comment|truncatechars:200 }}</p> |
| 45 | <div class="mt-2 flex items-center gap-3 text-xs text-gray-500"> |
| 46 | <span class="hover:text-brand-light">{{ note.user|display_user }}</span> |
| 47 | <span class="font-mono text-brand-light">{{ note.uuid|truncatechars:10 }}</span> |
| 48 | <span>{{ note.timestamp|date:"Y-m-d H:i" }}</span> |
| 49 | </div> |
| 50 | </div> |
| 51 | </div> |
| 52 | </a> |
| 53 | {% endfor %} |
| 54 |
| --- templates/fossil/technote_list.html | |
| +++ templates/fossil/technote_list.html | |
| @@ -43,11 +43,11 @@ | |
| 43 | <div class="flex-1 min-w-0"> |
| 44 | <p class="text-sm text-gray-200">{{ note.comment|truncatechars:200 }}</p> |
| 45 | <div class="mt-2 flex items-center gap-3 text-xs text-gray-500"> |
| 46 | <span class="hover:text-brand-light">{{ note.user|display_user }}</span> |
| 47 | <span class="font-mono text-brand-light">{{ note.uuid|truncatechars:10 }}</span> |
| 48 | <span>{{ note.timestamp|date:"Y-m-d H:i" }} UTC</span> |
| 49 | </div> |
| 50 | </div> |
| 51 | </div> |
| 52 | </a> |
| 53 | {% endfor %} |
| 54 |
| --- templates/fossil/ticket_detail.html | ||
| +++ templates/fossil/ticket_detail.html | ||
| @@ -58,11 +58,11 @@ | ||
| 58 | 58 | <dt class="text-xs font-medium text-gray-500 uppercase">Subsystem</dt> |
| 59 | 59 | <dd class="mt-0.5 text-sm text-gray-200">{{ ticket.subsystem|default:"—" }}</dd> |
| 60 | 60 | </div> |
| 61 | 61 | <div> |
| 62 | 62 | <dt class="text-xs font-medium text-gray-500 uppercase">Created</dt> |
| 63 | - <dd class="mt-0.5 text-sm text-gray-200">{{ ticket.created|date:"N j, Y g:i a" }}</dd> | |
| 63 | + <dd class="mt-0.5 text-sm text-gray-200">{{ ticket.created|date:"N j, Y g:i a" }} UTC</dd> | |
| 64 | 64 | </div> |
| 65 | 65 | </dl> |
| 66 | 66 | </div> |
| 67 | 67 | |
| 68 | 68 | <!-- Body/description --> |
| 69 | 69 |
| --- templates/fossil/ticket_detail.html | |
| +++ templates/fossil/ticket_detail.html | |
| @@ -58,11 +58,11 @@ | |
| 58 | <dt class="text-xs font-medium text-gray-500 uppercase">Subsystem</dt> |
| 59 | <dd class="mt-0.5 text-sm text-gray-200">{{ ticket.subsystem|default:"—" }}</dd> |
| 60 | </div> |
| 61 | <div> |
| 62 | <dt class="text-xs font-medium text-gray-500 uppercase">Created</dt> |
| 63 | <dd class="mt-0.5 text-sm text-gray-200">{{ ticket.created|date:"N j, Y g:i a" }}</dd> |
| 64 | </div> |
| 65 | </dl> |
| 66 | </div> |
| 67 | |
| 68 | <!-- Body/description --> |
| 69 |
| --- templates/fossil/ticket_detail.html | |
| +++ templates/fossil/ticket_detail.html | |
| @@ -58,11 +58,11 @@ | |
| 58 | <dt class="text-xs font-medium text-gray-500 uppercase">Subsystem</dt> |
| 59 | <dd class="mt-0.5 text-sm text-gray-200">{{ ticket.subsystem|default:"—" }}</dd> |
| 60 | </div> |
| 61 | <div> |
| 62 | <dt class="text-xs font-medium text-gray-500 uppercase">Created</dt> |
| 63 | <dd class="mt-0.5 text-sm text-gray-200">{{ ticket.created|date:"N j, Y g:i a" }} UTC</dd> |
| 64 | </div> |
| 65 | </dl> |
| 66 | </div> |
| 67 | |
| 68 | <!-- Body/description --> |
| 69 |
+12
-2
| --- templates/fossil/ticket_form.html | ||
| +++ templates/fossil/ticket_form.html | ||
| @@ -19,11 +19,11 @@ | ||
| 19 | 19 | <label class="block text-sm font-medium text-gray-300 mb-1">Title <span class="text-red-400">*</span></label> |
| 20 | 20 | <input type="text" name="title" required placeholder="Ticket title" |
| 21 | 21 | class="w-full rounded-md border-gray-600 bg-gray-900 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 22 | 22 | </div> |
| 23 | 23 | |
| 24 | - <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| 24 | + <div class="grid grid-cols-1 sm:grid-cols-3 gap-4"> | |
| 25 | 25 | <div> |
| 26 | 26 | <label class="block text-sm font-medium text-gray-300 mb-1">Type</label> |
| 27 | 27 | <select name="type" class="w-full rounded-md border-gray-600 bg-gray-800 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 28 | 28 | <option value="Code_Defect">Code Defect</option> |
| 29 | 29 | <option value="Feature_Request">Feature Request</option> |
| @@ -32,17 +32,27 @@ | ||
| 32 | 32 | </select> |
| 33 | 33 | </div> |
| 34 | 34 | <div> |
| 35 | 35 | <label class="block text-sm font-medium text-gray-300 mb-1">Severity</label> |
| 36 | 36 | <select name="severity" class="w-full rounded-md border-gray-600 bg-gray-800 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 37 | - <option value="">--</option> | |
| 37 | + <option value="">—</option> | |
| 38 | 38 | <option value="Critical">Critical</option> |
| 39 | 39 | <option value="Important">Important</option> |
| 40 | 40 | <option value="Minor">Minor</option> |
| 41 | 41 | <option value="Cosmetic">Cosmetic</option> |
| 42 | 42 | </select> |
| 43 | 43 | </div> |
| 44 | + <div> | |
| 45 | + <label class="block text-sm font-medium text-gray-300 mb-1">Priority</label> | |
| 46 | + <select name="priority" class="w-full rounded-md border-gray-600 bg-gray-800 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> | |
| 47 | + <option value="">—</option> | |
| 48 | + <option value="Critical">Critical</option> | |
| 49 | + <option value="Important">Important</option> | |
| 50 | + <option value="Minor">Minor</option> | |
| 51 | + <option value="Zero">Zero</option> | |
| 52 | + </select> | |
| 53 | + </div> | |
| 44 | 54 | </div> |
| 45 | 55 | |
| 46 | 56 | <div> |
| 47 | 57 | <label class="block text-sm font-medium text-gray-300 mb-1">Description</label> |
| 48 | 58 | <textarea name="body" rows="10" placeholder="Describe the issue..." |
| 49 | 59 |
| --- templates/fossil/ticket_form.html | |
| +++ templates/fossil/ticket_form.html | |
| @@ -19,11 +19,11 @@ | |
| 19 | <label class="block text-sm font-medium text-gray-300 mb-1">Title <span class="text-red-400">*</span></label> |
| 20 | <input type="text" name="title" required placeholder="Ticket title" |
| 21 | class="w-full rounded-md border-gray-600 bg-gray-900 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 22 | </div> |
| 23 | |
| 24 | <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> |
| 25 | <div> |
| 26 | <label class="block text-sm font-medium text-gray-300 mb-1">Type</label> |
| 27 | <select name="type" class="w-full rounded-md border-gray-600 bg-gray-800 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 28 | <option value="Code_Defect">Code Defect</option> |
| 29 | <option value="Feature_Request">Feature Request</option> |
| @@ -32,17 +32,27 @@ | |
| 32 | </select> |
| 33 | </div> |
| 34 | <div> |
| 35 | <label class="block text-sm font-medium text-gray-300 mb-1">Severity</label> |
| 36 | <select name="severity" class="w-full rounded-md border-gray-600 bg-gray-800 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 37 | <option value="">--</option> |
| 38 | <option value="Critical">Critical</option> |
| 39 | <option value="Important">Important</option> |
| 40 | <option value="Minor">Minor</option> |
| 41 | <option value="Cosmetic">Cosmetic</option> |
| 42 | </select> |
| 43 | </div> |
| 44 | </div> |
| 45 | |
| 46 | <div> |
| 47 | <label class="block text-sm font-medium text-gray-300 mb-1">Description</label> |
| 48 | <textarea name="body" rows="10" placeholder="Describe the issue..." |
| 49 |
| --- templates/fossil/ticket_form.html | |
| +++ templates/fossil/ticket_form.html | |
| @@ -19,11 +19,11 @@ | |
| 19 | <label class="block text-sm font-medium text-gray-300 mb-1">Title <span class="text-red-400">*</span></label> |
| 20 | <input type="text" name="title" required placeholder="Ticket title" |
| 21 | class="w-full rounded-md border-gray-600 bg-gray-900 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 22 | </div> |
| 23 | |
| 24 | <div class="grid grid-cols-1 sm:grid-cols-3 gap-4"> |
| 25 | <div> |
| 26 | <label class="block text-sm font-medium text-gray-300 mb-1">Type</label> |
| 27 | <select name="type" class="w-full rounded-md border-gray-600 bg-gray-800 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 28 | <option value="Code_Defect">Code Defect</option> |
| 29 | <option value="Feature_Request">Feature Request</option> |
| @@ -32,17 +32,27 @@ | |
| 32 | </select> |
| 33 | </div> |
| 34 | <div> |
| 35 | <label class="block text-sm font-medium text-gray-300 mb-1">Severity</label> |
| 36 | <select name="severity" class="w-full rounded-md border-gray-600 bg-gray-800 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 37 | <option value="">—</option> |
| 38 | <option value="Critical">Critical</option> |
| 39 | <option value="Important">Important</option> |
| 40 | <option value="Minor">Minor</option> |
| 41 | <option value="Cosmetic">Cosmetic</option> |
| 42 | </select> |
| 43 | </div> |
| 44 | <div> |
| 45 | <label class="block text-sm font-medium text-gray-300 mb-1">Priority</label> |
| 46 | <select name="priority" class="w-full rounded-md border-gray-600 bg-gray-800 text-gray-100 shadow-inner focus:border-brand focus:ring-1 focus:ring-brand sm:text-sm transition-colors"> |
| 47 | <option value="">—</option> |
| 48 | <option value="Critical">Critical</option> |
| 49 | <option value="Important">Important</option> |
| 50 | <option value="Minor">Minor</option> |
| 51 | <option value="Zero">Zero</option> |
| 52 | </select> |
| 53 | </div> |
| 54 | </div> |
| 55 | |
| 56 | <div> |
| 57 | <label class="block text-sm font-medium text-gray-300 mb-1">Description</label> |
| 58 | <textarea name="body" rows="10" placeholder="Describe the issue..." |
| 59 |
| --- templates/fossil/unversioned_list.html | ||
| +++ templates/fossil/unversioned_list.html | ||
| @@ -41,11 +41,11 @@ | ||
| 41 | 41 | <tbody class="divide-y divide-gray-700/70"> |
| 42 | 42 | {% for file in files %} |
| 43 | 43 | <tr class="hover:bg-gray-700/30 transition-colors"> |
| 44 | 44 | <td class="px-6 py-3 text-sm text-gray-200 font-mono">{{ file.name }}</td> |
| 45 | 45 | <td class="px-6 py-3 text-sm text-gray-400">{{ file.size|filesizeformat }}</td> |
| 46 | - <td class="px-6 py-3 text-sm text-gray-400">{% if file.mtime %}{{ file.mtime|date:"Y-m-d H:i" }}{% else %}--{% endif %}</td> | |
| 46 | + <td class="px-6 py-3 text-sm text-gray-400">{% if file.mtime %}{{ file.mtime|date:"Y-m-d H:i" }} UTC{% else %}--{% endif %}</td> | |
| 47 | 47 | <td class="px-6 py-3 text-right"> |
| 48 | 48 | <a href="{% url 'fossil:unversioned_download' slug=project.slug filename=file.name %}" |
| 49 | 49 | class="inline-flex items-center rounded-md bg-gray-700 px-3 py-1 text-xs font-semibold text-gray-300 hover:bg-gray-600"> |
| 50 | 50 | Download |
| 51 | 51 | </a> |
| 52 | 52 |
| --- templates/fossil/unversioned_list.html | |
| +++ templates/fossil/unversioned_list.html | |
| @@ -41,11 +41,11 @@ | |
| 41 | <tbody class="divide-y divide-gray-700/70"> |
| 42 | {% for file in files %} |
| 43 | <tr class="hover:bg-gray-700/30 transition-colors"> |
| 44 | <td class="px-6 py-3 text-sm text-gray-200 font-mono">{{ file.name }}</td> |
| 45 | <td class="px-6 py-3 text-sm text-gray-400">{{ file.size|filesizeformat }}</td> |
| 46 | <td class="px-6 py-3 text-sm text-gray-400">{% if file.mtime %}{{ file.mtime|date:"Y-m-d H:i" }}{% else %}--{% endif %}</td> |
| 47 | <td class="px-6 py-3 text-right"> |
| 48 | <a href="{% url 'fossil:unversioned_download' slug=project.slug filename=file.name %}" |
| 49 | class="inline-flex items-center rounded-md bg-gray-700 px-3 py-1 text-xs font-semibold text-gray-300 hover:bg-gray-600"> |
| 50 | Download |
| 51 | </a> |
| 52 |
| --- templates/fossil/unversioned_list.html | |
| +++ templates/fossil/unversioned_list.html | |
| @@ -41,11 +41,11 @@ | |
| 41 | <tbody class="divide-y divide-gray-700/70"> |
| 42 | {% for file in files %} |
| 43 | <tr class="hover:bg-gray-700/30 transition-colors"> |
| 44 | <td class="px-6 py-3 text-sm text-gray-200 font-mono">{{ file.name }}</td> |
| 45 | <td class="px-6 py-3 text-sm text-gray-400">{{ file.size|filesizeformat }}</td> |
| 46 | <td class="px-6 py-3 text-sm text-gray-400">{% if file.mtime %}{{ file.mtime|date:"Y-m-d H:i" }} UTC{% else %}--{% endif %}</td> |
| 47 | <td class="px-6 py-3 text-right"> |
| 48 | <a href="{% url 'fossil:unversioned_download' slug=project.slug filename=file.name %}" |
| 49 | class="inline-flex items-center rounded-md bg-gray-700 px-3 py-1 text-xs font-semibold text-gray-300 hover:bg-gray-600"> |
| 50 | Download |
| 51 | </a> |
| 52 |
+9
-2
| --- tests/test_views_coverage.py | ||
| +++ tests/test_views_coverage.py | ||
| @@ -179,12 +179,19 @@ | ||
| 179 | 179 | |
| 180 | 180 | def test_fossil_bare_wiki_link(self): |
| 181 | 181 | from fossil.views import _render_fossil_content |
| 182 | 182 | |
| 183 | 183 | content = "<p>See [PageName]</p>" |
| 184 | - html = _render_fossil_content(content) | |
| 185 | - assert 'href="PageName"' in html | |
| 184 | + html = _render_fossil_content(content, project_slug="my-project") | |
| 185 | + assert 'href="/projects/my-project/fossil/wiki/page/PageName"' in html | |
| 186 | + | |
| 187 | + def test_fossil_pipe_wiki_link_no_base_path(self): | |
| 188 | + from fossil.views import _render_fossil_content | |
| 189 | + | |
| 190 | + content = "<p>See [Architecture | Architecture]</p>" | |
| 191 | + html = _render_fossil_content(content, project_slug="my-project") | |
| 192 | + assert 'href="/projects/my-project/fossil/wiki/page/Architecture"' in html | |
| 186 | 193 | |
| 187 | 194 | def test_markdown_fossil_link_resolved(self): |
| 188 | 195 | from fossil.views import _render_fossil_content |
| 189 | 196 | |
| 190 | 197 | content = "# Page\n\n[./file.wiki | Link Text]" |
| 191 | 198 |
| --- tests/test_views_coverage.py | |
| +++ tests/test_views_coverage.py | |
| @@ -179,12 +179,19 @@ | |
| 179 | |
| 180 | def test_fossil_bare_wiki_link(self): |
| 181 | from fossil.views import _render_fossil_content |
| 182 | |
| 183 | content = "<p>See [PageName]</p>" |
| 184 | html = _render_fossil_content(content) |
| 185 | assert 'href="PageName"' in html |
| 186 | |
| 187 | def test_markdown_fossil_link_resolved(self): |
| 188 | from fossil.views import _render_fossil_content |
| 189 | |
| 190 | content = "# Page\n\n[./file.wiki | Link Text]" |
| 191 |
| --- tests/test_views_coverage.py | |
| +++ tests/test_views_coverage.py | |
| @@ -179,12 +179,19 @@ | |
| 179 | |
| 180 | def test_fossil_bare_wiki_link(self): |
| 181 | from fossil.views import _render_fossil_content |
| 182 | |
| 183 | content = "<p>See [PageName]</p>" |
| 184 | html = _render_fossil_content(content, project_slug="my-project") |
| 185 | assert 'href="/projects/my-project/fossil/wiki/page/PageName"' in html |
| 186 | |
| 187 | def test_fossil_pipe_wiki_link_no_base_path(self): |
| 188 | from fossil.views import _render_fossil_content |
| 189 | |
| 190 | content = "<p>See [Architecture | Architecture]</p>" |
| 191 | html = _render_fossil_content(content, project_slug="my-project") |
| 192 | assert 'href="/projects/my-project/fossil/wiki/page/Architecture"' in html |
| 193 | |
| 194 | def test_markdown_fossil_link_resolved(self): |
| 195 | from fossil.views import _render_fossil_content |
| 196 | |
| 197 | content = "# Page\n\n[./file.wiki | Link Text]" |
| 198 |