FossilRepo

v0.1.0 feature complete: chat, bundles, wiki links, author attribution

ragelink 2026-04-14 04:34 UTC trunk
Commit 46f6d5efaa3c9c1881e937dee379111bd0ea41735ba60cb0eda84182d96163d4
+1 -1
--- LICENSE
+++ LICENSE
@@ -1,8 +1,8 @@
11
MIT License
22
3
-Copyright (c) 2026 Conflict LLC
3
+Copyright (c) 2026 Leo Mata & CONFLICT LLC
44
55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal
77
in the Software without restriction, including without limitation the rights
88
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99
--- 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 @@
141141
}
142142
143143
[data-md-color-scheme="slate"] ::-webkit-scrollbar-thumb:hover {
144144
background: #3a3a3a;
145145
}
146
+
147
+/* Dual repo links in header */
148
+.md-header__source {
149
+ display: flex;
150
+ gap: 0.4rem;
151
+}
146152
147153
/* Content max width for readability */
148154
.md-grid {
149155
max-width: 1220px;
150156
}
151157
--- 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
--- docs/getting-started/installation.md
+++ docs/getting-started/installation.md
@@ -1,17 +1,31 @@
11
# Setup Guide
22
33
## Quick Start (Docker)
44
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
+ ```
1327
1428
Visit http://localhost:8000. Login: `admin` / `admin`.
1529
1630
## Default Users
1731
1832
--- 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 @@
2323
|---|---|
2424
| **Fossil server** | Serves all repos from a single process |
2525
| **Caddy** | SSL termination, subdomain-per-repo routing |
2626
| **Litestream** | Continuous SQLite replication to S3/MinIO |
2727
| **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 |
2929
| **Celery workers** | Background sync, scheduled tasks |
3030
3131
## Quick Start
3232
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).
4767
4868
## Architecture
4969
5070
```
5171
Caddy (SSL termination, routing, subdomain per repo)
5272
5373
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
11
--- 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
11
--- 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
11
--- 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
11
--- 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
11
--- 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
11
--- 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
--- fossil/api_views.py
+++ fossil/api_views.py
@@ -1528,10 +1528,11 @@
15281528
- checkin: new checkin pushed
15291529
- ticket: ticket created/updated (by count change)
15301530
- claim: ticket claimed/released/submitted
15311531
- workspace: workspace created/merged/abandoned
15321532
- review: code review created/updated
1533
+ - chat: new chat message posted
15331534
15341535
Heartbeat sent every 15 seconds if no events. Poll interval: 5 seconds.
15351536
"""
15361537
if request.method != "GET":
15371538
return JsonResponse({"error": "GET required"}, status=405)
@@ -1556,10 +1557,14 @@
15561557
15571558
last_claim_id = TicketClaim.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0
15581559
last_workspace_id = AgentWorkspace.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0
15591560
last_review_id = CodeReview.all_objects.filter(repository=repo).order_by("-pk").values_list("pk", flat=True).first() or 0
15601561
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
+
15611566
heartbeat_counter = 0
15621567
15631568
while True:
15641569
events = []
15651570
@@ -1632,10 +1637,29 @@
16321637
"agent_id": review.agent_id,
16331638
},
16341639
}
16351640
)
16361641
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
16371661
16381662
# Yield events
16391663
for event in events:
16401664
yield f"event: {event['type']}\ndata: {json.dumps(event['data'])}\n\n"
16411665
16421666
16431667
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
--- 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]}"
--- fossil/cli.py
+++ fossil/cli.py
@@ -496,5 +496,42 @@
496496
cmd = [self.binary, "shun", "--list", "-R", str(repo_path)]
497497
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, env=self._env)
498498
if result.returncode == 0:
499499
return [line.strip() for line in result.stdout.strip().splitlines() if line.strip()]
500500
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)
501538
502539
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 ]
--- fossil/models.py
+++ fossil/models.py
@@ -68,10 +68,11 @@
6868
6969
# Import related models so they're discoverable by Django
7070
from fossil.agent_claims import TicketClaim # noqa: E402, F401
7171
from fossil.api_tokens import APIToken # noqa: E402, F401
7272
from fossil.branch_protection import BranchProtection # noqa: E402, F401
73
+from fossil.chat import ChatMessage # noqa: E402, F401
7374
from fossil.ci import StatusCheck # noqa: E402, F401
7475
from fossil.code_reviews import CodeReview, ReviewComment # noqa: E402, F401
7576
from fossil.forum import ForumPost # noqa: E402, F401
7677
from fossil.notifications import Notification, NotificationPreference, ProjectWatch # noqa: E402, F401
7778
from fossil.releases import Release, ReleaseAsset # noqa: E402, F401
7879
--- 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
--- fossil/urls.py
+++ fossil/urls.py
@@ -136,6 +136,12 @@
136136
path("admin/shun/add/", views.shun_artifact, name="shun_artifact"),
137137
# SQLite Explorer
138138
path("explorer/", views.repo_explorer, name="explorer"),
139139
path("explorer/table/<str:table_name>/", views.repo_explorer_table, name="explorer_table"),
140140
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"),
141147
]
142148
--- 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 @@
4141
path = m.group(1).strip()
4242
text = m.group(2).strip()
4343
if path.startswith("./"):
4444
path = "/" + base_path + path[2:]
4545
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
4747
return f"[{text}]({path})"
4848
4949
content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_to_md_link, content)
5050
content = re.sub(r"<verbatim>(.*?)</verbatim>", r"```\n\1\n```", content, flags=re.DOTALL)
5151
html = md.markdown(content, extensions=["fenced_code", "tables", "toc", "footnotes", "def_list", "attr_list"])
@@ -73,21 +73,21 @@
7373
text = match.group(2).strip()
7474
# Convert relative paths to absolute using base_path
7575
if path.startswith("./"):
7676
path = "/" + base_path + path[2:]
7777
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
7979
return f'<a href="{path}">{text}</a>'
8080
8181
# Match [path | text] with flexible whitespace around the pipe
8282
content = re.sub(r"\[([^\]\|]+?)\s*\|\s*([^\]]+?)\]", _fossil_link_replace, content)
8383
# Interwiki links: [wikipedia:Article] -> external link
8484
content = re.sub(r"\[wikipedia:([^\]]+)\]", r'<a href="https://en.wikipedia.org/wiki/\1">\1</a>', content)
8585
# Anchor links: [#anchor-name] -> local anchor
8686
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)
8989
9090
# Verbatim blocks
9191
# Pikchr diagrams: <verbatim type="pikchr">...</verbatim> → SVG
9292
def _render_pikchr_block(m):
9393
try:
@@ -1361,13 +1361,16 @@
13611361
severity = request.POST.get("severity", "")
13621362
if title:
13631363
from fossil.cli import FossilCLI
13641364
13651365
cli = FossilCLI()
1366
+ priority = request.POST.get("priority", "")
13661367
fields = {"title": title, "type": ticket_type, "comment": body, "status": "Open"}
13671368
if severity:
13681369
fields["severity"] = severity
1370
+ if priority:
1371
+ fields["priority"] = priority
13691372
# Collect custom field values
13701373
for cf in custom_fields:
13711374
if cf.field_type == "checkbox":
13721375
val = "1" if request.POST.get(f"custom_{cf.name}") == "on" else "0"
13731376
else:
@@ -2919,21 +2922,16 @@
29192922
rid_to_idx[entry.rid] = i
29202923
if entry.event_type == "ci":
29212924
rid_to_rail[entry.rid] = max(entry.rail, 0)
29222925
29232926
# 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).
29262927
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
29282928
2929
- for i, entry in enumerate(entries):
2929
+ for _i, entry in enumerate(entries):
29302930
if entry.event_type != "ci":
29312931
continue
29322932
rail = max(entry.rail, 0)
2933
- if rail not in rail_first_seen:
2934
- rail_first_seen[rail] = i
29352933
# Mark the primary parent as having a child on this rail
29362934
if entry.parent_rid in rid_to_rail and rid_to_rail[entry.parent_rid] == rail:
29372935
has_child_on_rail.add(entry.parent_rid)
29382936
29392937
# Precompute: for each checkin, the range of rows its vertical line spans
@@ -2956,15 +2954,17 @@
29562954
for i, entry in enumerate(entries):
29572955
if entry.event_type != "ci":
29582956
continue
29592957
child_rail = max(entry.rail, 0)
29602958
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.
29632963
if entry.parent_rid in rid_to_rail:
29642964
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:
29662966
row_fork_from[i] = parent_rail
29672967
# Draw the fork connector at this row (where the branch starts)
29682968
left_rail = min(child_rail, parent_rail)
29692969
right_rail = max(child_rail, parent_rail)
29702970
left_x = rail_offset + left_rail * rail_pitch
@@ -4427,5 +4427,105 @@
44274427
"error": error,
44284428
"table_names": table_names,
44294429
"active_tab": "explorer",
44304430
},
44314431
)
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
+ )
44324532
--- 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 @@
1010
# curl -sSL https://get.fossilrepo.dev | bash
1111
# -- or --
1212
# ./install.sh --docker --domain fossil.example.com --ssl
1313
#
1414
# https://github.com/ConflictHQ/fossilrepo
15
+# Copyright (c) 2026 Leo Mata & CONFLICT LLC
1516
# MIT License
1617
# ============================================================================
1718
1819
set -euo pipefail
1920
2021
--- 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 @@
3838
- search.highlight
3939
- content.code.copy
4040
- content.tabs.link
4141
- header.autohide
4242
icon:
43
- repo: fontawesome/brands/github
43
+ repo: material/source-repository
4444
4545
extra_css:
4646
- assets/css/custom.css
4747
4848
plugins:
@@ -83,9 +83,13 @@
8383
- Reference: api/reference.md
8484
- Agentic Development: api/agentic-development.md
8585
8686
extra:
8787
social:
88
+ - icon: material/source-repository
89
+ link: https://fossilrepo.io/fossilrepo
90
+ name: Fossil Repository (primary)
8891
- icon: fontawesome/brands/github
8992
link: https://github.com/ConflictHQ/fossilrepo
93
+ name: GitHub Mirror
9094
9195
copyright: Copyright &copy; 2026 CONFLICT LLC
9296
--- 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 &copy; 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 &copy; 2026 CONFLICT LLC
96
--- pyproject.toml
+++ pyproject.toml
@@ -4,10 +4,11 @@
44
description = "Self-hosted Fossil SCM forge — code hosting, issues, wiki, and continuous backups in one command."
55
license = "MIT"
66
requires-python = ">=3.12"
77
readme = "README.md"
88
authors = [
9
+ { name = "Leo Mata", email = "[email protected]" },
910
{ name = "CONFLICT LLC", email = "[email protected]" },
1011
]
1112
keywords = ["fossil", "scm", "vcs", "code-hosting", "self-hosted", "forge"]
1213
classifiers = [
1314
"Development Status :: 3 - Alpha",
1415
--- 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 @@
2424
Wiki
2525
</a>
2626
<a href="{% url 'fossil:forum' slug=project.slug %}"
2727
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 %}">
2828
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
2933
</a>
3034
<a href="{% url 'fossil:releases' slug=project.slug %}"
3135
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 %}">
3236
Releases
3337
</a>
3438
3539
ADDED templates/fossil/bundle_import.html
3640
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 @@
6161
<div class="rounded-lg bg-gray-800 border border-gray-700 shadow-sm">
6262
<div class="px-6 py-5">
6363
<p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p>
6464
<div class="mt-3 flex items-center gap-x-4 gap-y-1 flex-wrap text-sm">
6565
<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>
6767
{% if checkin.branch %}
6868
<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">
6969
{{ checkin.branch }}
7070
</span>
7171
{% endif %}
7272
--- 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 @@
3737
{% if latest_commit %}
3838
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 mt-2 text-xs text-gray-500">
3939
<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>
4040
<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>
4141
<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>
4343
</div>
4444
{% endif %}
4545
</div>
4646
4747
<!-- File table -->
4848
--- 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
--- templates/fossil/compare.html
+++ templates/fossil/compare.html
@@ -59,16 +59,16 @@
5959
{% if from_detail and to_detail %}
6060
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
6161
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
6262
<div class="text-xs text-gray-500 mb-1">From</div>
6363
<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 }} &middot; {{ 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 }} &middot; {{ from_detail.timestamp|date:"Y-m-d H:i" }} UTC</div>
6565
</div>
6666
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
6767
<div class="text-xs text-gray-500 mb-1">To</div>
6868
<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 }} &middot; {{ 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 }} &middot; {{ to_detail.timestamp|date:"Y-m-d H:i" }} UTC</div>
7070
</div>
7171
</div>
7272
7373
{% if file_diffs %}
7474
<div x-data="{ mode: localStorage.getItem('diff-mode') || 'unified' }" x-init="$watch('mode', val => localStorage.setItem('diff-mode', val))" x-ref="diffToggle">
7575
--- 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 }} &middot; {{ 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 }} &middot; {{ 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 }} &middot; {{ 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 }} &middot; {{ 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 @@
2424
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
2525
class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:100 }}</a>
2626
<div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
2727
<a href="{% url 'fossil:user_activity' slug=project.slug username=commit.user %}" class="hover:text-gray-300">{{ commit.user|display_user }}</a>
2828
<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>
3030
</div>
3131
</div>
3232
</div>
3333
{% empty %}
3434
<p class="text-sm text-gray-500 py-8 text-center">No history found.</p>
3535
3636
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 @@
171171
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
172172
</svg>
173173
</a>
174174
</div>
175175
</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>
176192
177193
<!-- Danger Zone -->
178194
<div class="rounded-lg border-2 border-red-900/50 p-5">
179195
<h2 class="text-lg font-semibold text-red-400 mb-2">Danger Zone</h2>
180196
<p class="text-sm text-gray-400 mb-4">Destructive operations that cannot be undone.</p>
181197
--- 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 @@
1818
<h2 class="text-xl font-bold text-gray-100 mb-1">{{ note.comment }}</h2>
1919
<div class="flex items-center gap-3 text-xs text-gray-500">
2020
<a href="{% url 'fossil:user_activity' slug=project.slug username=note.user %}"
2121
class="hover:text-brand-light">{{ note.user|display_user }}</a>
2222
<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>
2424
</div>
2525
</div>
2626
{% if has_write %}
2727
<div class="flex items-center gap-2 flex-shrink-0">
2828
<a href="{% url 'fossil:technote_edit' slug=project.slug technote_id=note.uuid %}"
2929
--- 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 @@
4343
<div class="flex-1 min-w-0">
4444
<p class="text-sm text-gray-200">{{ note.comment|truncatechars:200 }}</p>
4545
<div class="mt-2 flex items-center gap-3 text-xs text-gray-500">
4646
<span class="hover:text-brand-light">{{ note.user|display_user }}</span>
4747
<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>
4949
</div>
5050
</div>
5151
</div>
5252
</a>
5353
{% endfor %}
5454
--- 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 @@
5858
<dt class="text-xs font-medium text-gray-500 uppercase">Subsystem</dt>
5959
<dd class="mt-0.5 text-sm text-gray-200">{{ ticket.subsystem|default:"—" }}</dd>
6060
</div>
6161
<div>
6262
<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>
6464
</div>
6565
</dl>
6666
</div>
6767
6868
<!-- Body/description -->
6969
--- 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
--- templates/fossil/ticket_form.html
+++ templates/fossil/ticket_form.html
@@ -19,11 +19,11 @@
1919
<label class="block text-sm font-medium text-gray-300 mb-1">Title <span class="text-red-400">*</span></label>
2020
<input type="text" name="title" required placeholder="Ticket title"
2121
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">
2222
</div>
2323
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">
2525
<div>
2626
<label class="block text-sm font-medium text-gray-300 mb-1">Type</label>
2727
<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">
2828
<option value="Code_Defect">Code Defect</option>
2929
<option value="Feature_Request">Feature Request</option>
@@ -32,17 +32,27 @@
3232
</select>
3333
</div>
3434
<div>
3535
<label class="block text-sm font-medium text-gray-300 mb-1">Severity</label>
3636
<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>
3838
<option value="Critical">Critical</option>
3939
<option value="Important">Important</option>
4040
<option value="Minor">Minor</option>
4141
<option value="Cosmetic">Cosmetic</option>
4242
</select>
4343
</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>
4454
</div>
4555
4656
<div>
4757
<label class="block text-sm font-medium text-gray-300 mb-1">Description</label>
4858
<textarea name="body" rows="10" placeholder="Describe the issue..."
4959
--- 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 @@
4141
<tbody class="divide-y divide-gray-700/70">
4242
{% for file in files %}
4343
<tr class="hover:bg-gray-700/30 transition-colors">
4444
<td class="px-6 py-3 text-sm text-gray-200 font-mono">{{ file.name }}</td>
4545
<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>
4747
<td class="px-6 py-3 text-right">
4848
<a href="{% url 'fossil:unversioned_download' slug=project.slug filename=file.name %}"
4949
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">
5050
Download
5151
</a>
5252
--- 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
--- tests/test_views_coverage.py
+++ tests/test_views_coverage.py
@@ -179,12 +179,19 @@
179179
180180
def test_fossil_bare_wiki_link(self):
181181
from fossil.views import _render_fossil_content
182182
183183
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
186193
187194
def test_markdown_fossil_link_resolved(self):
188195
from fossil.views import _render_fossil_content
189196
190197
content = "# Page\n\n[./file.wiki | Link Text]"
191198
--- 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

Keyboard Shortcuts

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