FossilRepo

Add unified diff view on checkin detail, project overview with repo stats Checkin detail: - Compute unified diff for each changed file (difflib) - Color-coded diff lines: green for additions, red for deletions, blue for hunk headers - File summary bar with +/- counts and jump-to-file links - A/M/D badges per file with links to source view Project overview: - Repository stats sidebar: checkins, files, tickets, wiki pages (each links to respective view) - Recent activity: last 5 commits with clickable hashes - Fossil tab navigation on project detail when repo exists - GitHub-style layout: main content + sidebar

lmata 2026-04-06 14:12 trunk
Commit 0e0b43fd9b312cf586e7c18b7966211b21d70626c7bd7f3aa5cedc4a1710716b
+65 -2
--- fossil/views.py
+++ fossil/views.py
@@ -264,21 +264,84 @@
264264
P.PROJECT_VIEW.check(request.user)
265265
project, fossil_repo, reader = _get_repo_and_reader(slug)
266266
267267
with reader:
268268
checkin = reader.get_checkin_detail(checkin_uuid)
269
+ if not checkin:
270
+ raise Http404("Checkin not found")
269271
270
- if not checkin:
271
- raise Http404("Checkin not found")
272
+ # Compute diffs for each changed file
273
+ import difflib
274
+
275
+ file_diffs = []
276
+ for f in checkin.files_changed:
277
+ old_text = ""
278
+ new_text = ""
279
+ if f["prev_uuid"]:
280
+ try:
281
+ old_bytes = reader.get_file_content(f["prev_uuid"])
282
+ old_text = old_bytes.decode("utf-8", errors="replace")
283
+ except Exception:
284
+ old_text = ""
285
+ if f["uuid"]:
286
+ try:
287
+ new_bytes = reader.get_file_content(f["uuid"])
288
+ new_text = new_bytes.decode("utf-8", errors="replace")
289
+ except Exception:
290
+ new_text = ""
291
+
292
+ # Check if binary
293
+ is_binary = "\x00" in old_text[:1024] or "\x00" in new_text[:1024]
294
+ diff_lines = []
295
+ additions = 0
296
+ deletions = 0
297
+
298
+ if not is_binary and (old_text or new_text):
299
+ diff = difflib.unified_diff(
300
+ old_text.splitlines(keepends=True),
301
+ new_text.splitlines(keepends=True),
302
+ fromfile=f"a/{f['name']}",
303
+ tofile=f"b/{f['name']}",
304
+ lineterm="",
305
+ n=3,
306
+ )
307
+ for line in diff:
308
+ line_type = "context"
309
+ if line.startswith("+++") or line.startswith("---"):
310
+ line_type = "header"
311
+ elif line.startswith("@@"):
312
+ line_type = "hunk"
313
+ elif line.startswith("+"):
314
+ line_type = "add"
315
+ additions += 1
316
+ elif line.startswith("-"):
317
+ line_type = "del"
318
+ deletions += 1
319
+ diff_lines.append({"text": line, "type": line_type})
320
+
321
+ ext = f["name"].rsplit(".", 1)[-1] if "." in f["name"] else ""
322
+ file_diffs.append(
323
+ {
324
+ "name": f["name"],
325
+ "change_type": f["change_type"],
326
+ "uuid": f["uuid"],
327
+ "is_binary": is_binary,
328
+ "diff_lines": diff_lines,
329
+ "additions": additions,
330
+ "deletions": deletions,
331
+ "language": ext,
332
+ }
333
+ )
272334
273335
return render(
274336
request,
275337
"fossil/checkin_detail.html",
276338
{
277339
"project": project,
278340
"fossil_repo": fossil_repo,
279341
"checkin": checkin,
342
+ "file_diffs": file_diffs,
280343
"active_tab": "timeline",
281344
},
282345
)
283346
284347
285348
--- fossil/views.py
+++ fossil/views.py
@@ -264,21 +264,84 @@
264 P.PROJECT_VIEW.check(request.user)
265 project, fossil_repo, reader = _get_repo_and_reader(slug)
266
267 with reader:
268 checkin = reader.get_checkin_detail(checkin_uuid)
 
 
269
270 if not checkin:
271 raise Http404("Checkin not found")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
273 return render(
274 request,
275 "fossil/checkin_detail.html",
276 {
277 "project": project,
278 "fossil_repo": fossil_repo,
279 "checkin": checkin,
 
280 "active_tab": "timeline",
281 },
282 )
283
284
285
--- fossil/views.py
+++ fossil/views.py
@@ -264,21 +264,84 @@
264 P.PROJECT_VIEW.check(request.user)
265 project, fossil_repo, reader = _get_repo_and_reader(slug)
266
267 with reader:
268 checkin = reader.get_checkin_detail(checkin_uuid)
269 if not checkin:
270 raise Http404("Checkin not found")
271
272 # Compute diffs for each changed file
273 import difflib
274
275 file_diffs = []
276 for f in checkin.files_changed:
277 old_text = ""
278 new_text = ""
279 if f["prev_uuid"]:
280 try:
281 old_bytes = reader.get_file_content(f["prev_uuid"])
282 old_text = old_bytes.decode("utf-8", errors="replace")
283 except Exception:
284 old_text = ""
285 if f["uuid"]:
286 try:
287 new_bytes = reader.get_file_content(f["uuid"])
288 new_text = new_bytes.decode("utf-8", errors="replace")
289 except Exception:
290 new_text = ""
291
292 # Check if binary
293 is_binary = "\x00" in old_text[:1024] or "\x00" in new_text[:1024]
294 diff_lines = []
295 additions = 0
296 deletions = 0
297
298 if not is_binary and (old_text or new_text):
299 diff = difflib.unified_diff(
300 old_text.splitlines(keepends=True),
301 new_text.splitlines(keepends=True),
302 fromfile=f"a/{f['name']}",
303 tofile=f"b/{f['name']}",
304 lineterm="",
305 n=3,
306 )
307 for line in diff:
308 line_type = "context"
309 if line.startswith("+++") or line.startswith("---"):
310 line_type = "header"
311 elif line.startswith("@@"):
312 line_type = "hunk"
313 elif line.startswith("+"):
314 line_type = "add"
315 additions += 1
316 elif line.startswith("-"):
317 line_type = "del"
318 deletions += 1
319 diff_lines.append({"text": line, "type": line_type})
320
321 ext = f["name"].rsplit(".", 1)[-1] if "." in f["name"] else ""
322 file_diffs.append(
323 {
324 "name": f["name"],
325 "change_type": f["change_type"],
326 "uuid": f["uuid"],
327 "is_binary": is_binary,
328 "diff_lines": diff_lines,
329 "additions": additions,
330 "deletions": deletions,
331 "language": ext,
332 }
333 )
334
335 return render(
336 request,
337 "fossil/checkin_detail.html",
338 {
339 "project": project,
340 "fossil_repo": fossil_repo,
341 "checkin": checkin,
342 "file_diffs": file_diffs,
343 "active_tab": "timeline",
344 },
345 )
346
347
348
--- projects/views.py
+++ projects/views.py
@@ -49,11 +49,31 @@
4949
@login_required
5050
def project_detail(request, slug):
5151
P.PROJECT_VIEW.check(request.user)
5252
project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True)
5353
project_teams = project.project_teams.filter(deleted_at__isnull=True).select_related("team")
54
- return render(request, "projects/project_detail.html", {"project": project, "project_teams": project_teams})
54
+
55
+ # Get Fossil repo stats if available
56
+ repo_stats = None
57
+ recent_commits = []
58
+ try:
59
+ from fossil.models import FossilRepository
60
+ from fossil.reader import FossilReader
61
+
62
+ fossil_repo = FossilRepository.objects.filter(project=project, deleted_at__isnull=True).first()
63
+ if fossil_repo and fossil_repo.exists_on_disk:
64
+ with FossilReader(fossil_repo.full_path) as reader:
65
+ repo_stats = reader.get_metadata()
66
+ recent_commits = reader.get_timeline(limit=5, event_type="ci")
67
+ except Exception:
68
+ pass
69
+
70
+ return render(
71
+ request,
72
+ "projects/project_detail.html",
73
+ {"project": project, "project_teams": project_teams, "repo_stats": repo_stats, "recent_commits": recent_commits},
74
+ )
5575
5676
5777
@login_required
5878
def project_update(request, slug):
5979
P.PROJECT_CHANGE.check(request.user)
6080
--- projects/views.py
+++ projects/views.py
@@ -49,11 +49,31 @@
49 @login_required
50 def project_detail(request, slug):
51 P.PROJECT_VIEW.check(request.user)
52 project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True)
53 project_teams = project.project_teams.filter(deleted_at__isnull=True).select_related("team")
54 return render(request, "projects/project_detail.html", {"project": project, "project_teams": project_teams})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
56
57 @login_required
58 def project_update(request, slug):
59 P.PROJECT_CHANGE.check(request.user)
60
--- projects/views.py
+++ projects/views.py
@@ -49,11 +49,31 @@
49 @login_required
50 def project_detail(request, slug):
51 P.PROJECT_VIEW.check(request.user)
52 project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True)
53 project_teams = project.project_teams.filter(deleted_at__isnull=True).select_related("team")
54
55 # Get Fossil repo stats if available
56 repo_stats = None
57 recent_commits = []
58 try:
59 from fossil.models import FossilRepository
60 from fossil.reader import FossilReader
61
62 fossil_repo = FossilRepository.objects.filter(project=project, deleted_at__isnull=True).first()
63 if fossil_repo and fossil_repo.exists_on_disk:
64 with FossilReader(fossil_repo.full_path) as reader:
65 repo_stats = reader.get_metadata()
66 recent_commits = reader.get_timeline(limit=5, event_type="ci")
67 except Exception:
68 pass
69
70 return render(
71 request,
72 "projects/project_detail.html",
73 {"project": project, "project_teams": project_teams, "repo_stats": repo_stats, "recent_commits": recent_commits},
74 )
75
76
77 @login_required
78 def project_update(request, slug):
79 P.PROJECT_CHANGE.check(request.user)
80
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -1,7 +1,22 @@
11
{% extends "base.html" %}
22
{% block title %}{{ checkin.uuid|truncatechars:12 }} — {{ project.name }} — Fossilrepo{% endblock %}
3
+
4
+{% block extra_head %}
5
+<style>
6
+ .diff-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
7
+ .diff-table td { padding: 0 8px; white-space: pre; vertical-align: top; line-height: 1.4rem; }
8
+ .diff-line-add { background: rgba(34, 197, 94, 0.1); }
9
+ .diff-line-add td:last-child { color: #86efac; }
10
+ .diff-line-del { background: rgba(239, 68, 68, 0.1); }
11
+ .diff-line-del td:last-child { color: #fca5a5; }
12
+ .diff-line-hunk { background: rgba(96, 165, 250, 0.08); }
13
+ .diff-line-hunk td { color: #93c5fd; font-weight: 500; }
14
+ .diff-line-header td { color: #6b7280; font-weight: 600; }
15
+ .diff-gutter { width: 1%; user-select: none; color: #4b5563; text-align: right; padding: 0 6px; border-right: 1px solid #374151; }
16
+</style>
17
+{% endblock %}
318
419
{% block content %}
520
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
621
{% include "fossil/_project_nav.html" %}
722
@@ -10,11 +25,11 @@
1025
<div class="rounded-lg bg-gray-800 border border-gray-700">
1126
<div class="px-6 py-5">
1227
<p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p>
1328
<div class="mt-3 flex items-center gap-4 flex-wrap text-sm">
1429
<span class="font-medium text-gray-200">{{ checkin.user }}</span>
15
- <span class="text-gray-500">{{ checkin.timestamp|date:"N j, Y g:i a" }}</span>
30
+ <span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }}</span>
1631
{% if checkin.branch %}
1732
<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">
1833
{{ checkin.branch }}
1934
</span>
2035
{% endif %}
@@ -21,53 +36,99 @@
2136
{% if checkin.is_merge %}
2237
<span class="inline-flex items-center rounded-full bg-purple-900/50 px-2 py-0.5 text-xs text-purple-300">merge</span>
2338
{% endif %}
2439
</div>
2540
</div>
26
- <div class="px-6 py-3 border-t border-gray-700 bg-gray-800/50 flex items-center gap-6 text-xs">
41
+ <div class="px-6 py-3 border-t border-gray-700 bg-gray-800/50 flex items-center gap-6 flex-wrap text-xs">
2742
<div class="flex items-center gap-2">
2843
<span class="text-gray-500">Commit</span>
29
- <code class="font-mono text-gray-300 text-xs break-all">{{ checkin.uuid }}</code>
44
+ <code class="font-mono text-gray-300 break-all">{{ checkin.uuid }}</code>
3045
</div>
3146
{% if checkin.parent_uuid %}
3247
<div class="flex items-center gap-2">
3348
<span class="text-gray-500">Parent</span>
3449
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=checkin.parent_uuid %}"
35
- class="font-mono text-brand-light hover:text-brand text-xs">{{ checkin.parent_uuid|truncatechars:16 }}</a>
36
- </div>
37
- {% endif %}
38
- <div class="flex items-center gap-2">
39
- <span class="text-gray-500">Files changed</span>
40
- <span class="text-gray-300">{{ checkin.files_changed|length }}</span>
41
- </div>
42
- </div>
43
- </div>
44
-
45
- <!-- Changed files -->
46
- {% if checkin.files_changed %}
47
- <div class="rounded-lg bg-gray-800 border border-gray-700">
48
- <div class="px-4 py-3 border-b border-gray-700 text-sm font-medium text-gray-300">
49
- {{ checkin.files_changed|length }} file{{ checkin.files_changed|length|pluralize }} changed
50
- </div>
51
- <div class="divide-y divide-gray-700">
52
- {% for f in checkin.files_changed %}
53
- <div class="px-4 py-2.5 flex items-center gap-3 hover:bg-gray-700/30">
54
- {% if f.change_type == "added" %}
55
- <span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-green-900/50 text-green-300">A</span>
56
- {% elif f.change_type == "deleted" %}
50
+ class="font-mono text-brand-light hover:text-brand">{{ checkin.parent_uuid|truncatechars:16 }}</a>
51
+ </div>
52
+ {% endif %}
53
+ </div>
54
+ </div>
55
+
56
+ <!-- File summary bar -->
57
+ {% if file_diffs %}
58
+ <div class="rounded-lg bg-gray-800 border border-gray-700 px-4 py-3">
59
+ <div class="flex items-center gap-4 text-sm">
60
+ <span class="text-gray-300 font-medium">{{ file_diffs|length }} file{{ file_diffs|length|pluralize }} changed</span>
61
+ {% with total_add=0 total_del=0 %}
62
+ {% for fd in file_diffs %}
63
+ {% if fd.additions %}
64
+ <span class="text-green-400">+{{ fd.additions }}</span>
65
+ {% endif %}
66
+ {% if fd.deletions %}
67
+ <span class="text-red-400">-{{ fd.deletions }}</span>
68
+ {% endif %}
69
+ {% endfor %}
70
+ {% endwith %}
71
+ </div>
72
+ <div class="mt-2 flex flex-wrap gap-2">
73
+ {% for fd in file_diffs %}
74
+ <a href="#diff-{{ forloop.counter }}" class="text-xs font-mono text-brand-light hover:text-brand">
75
+ {% if fd.change_type == "added" %}
76
+ <span class="text-green-400">+</span>
77
+ {% elif fd.change_type == "deleted" %}
78
+ <span class="text-red-400">-</span>
79
+ {% else %}
80
+ <span class="text-yellow-400">~</span>
81
+ {% endif %}
82
+ {{ fd.name }}
83
+ </a>
84
+ {% endfor %}
85
+ </div>
86
+ </div>
87
+
88
+ <!-- Diffs -->
89
+ {% for fd in file_diffs %}
90
+ <div class="rounded-lg bg-gray-800 border border-gray-700 overflow-hidden" id="diff-{{ forloop.counter }}">
91
+ <div class="px-4 py-2.5 border-b border-gray-700 flex items-center justify-between bg-gray-900/50">
92
+ <div class="flex items-center gap-3">
93
+ {% if fd.change_type == "added" %}
94
+ <span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-green-900/50 text-green-300">A</span>
95
+ {% elif fd.change_type == "deleted" %}
5796
<span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-red-900/50 text-red-300">D</span>
5897
{% else %}
5998
<span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-yellow-900/50 text-yellow-300">M</span>
6099
{% endif %}
61
- {% if f.uuid %}
62
- <a href="{% url 'fossil:code_file' slug=project.slug filepath=f.name %}"
63
- class="text-sm font-mono text-brand-light hover:text-brand">{{ f.name }}</a>
100
+ {% if fd.uuid %}
101
+ <a href="{% url 'fossil:code_file' slug=project.slug filepath=fd.name %}"
102
+ class="text-sm font-mono text-brand-light hover:text-brand">{{ fd.name }}</a>
64103
{% else %}
65
- <span class="text-sm font-mono text-gray-400">{{ f.name }}</span>
104
+ <span class="text-sm font-mono text-gray-400">{{ fd.name }}</span>
66105
{% endif %}
67106
</div>
68
- {% endfor %}
107
+ <div class="flex items-center gap-2 text-xs">
108
+ {% if fd.additions %}<span class="text-green-400">+{{ fd.additions }}</span>{% endif %}
109
+ {% if fd.deletions %}<span class="text-red-400">-{{ fd.deletions }}</span>{% endif %}
110
+ </div>
111
+ </div>
112
+ <div class="overflow-x-auto">
113
+ {% if fd.is_binary %}
114
+ <p class="p-4 text-sm text-gray-500">Binary file</p>
115
+ {% elif fd.diff_lines %}
116
+ <table class="diff-table">
117
+ <tbody>
118
+ {% for dl in fd.diff_lines %}
119
+ <tr class="diff-line-{{ dl.type }}">
120
+ <td class="diff-gutter"></td>
121
+ <td>{{ dl.text }}</td>
122
+ </tr>
123
+ {% endfor %}
124
+ </tbody>
125
+ </table>
126
+ {% else %}
127
+ <p class="p-4 text-sm text-gray-500">No diff available</p>
128
+ {% endif %}
69129
</div>
70130
</div>
131
+ {% endfor %}
71132
{% endif %}
72133
</div>
73134
{% endblock %}
74135
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -1,7 +1,22 @@
1 {% extends "base.html" %}
2 {% block title %}{{ checkin.uuid|truncatechars:12 }} — {{ 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
@@ -10,11 +25,11 @@
10 <div class="rounded-lg bg-gray-800 border border-gray-700">
11 <div class="px-6 py-5">
12 <p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p>
13 <div class="mt-3 flex items-center gap-4 flex-wrap text-sm">
14 <span class="font-medium text-gray-200">{{ checkin.user }}</span>
15 <span class="text-gray-500">{{ checkin.timestamp|date:"N j, Y g:i a" }}</span>
16 {% if checkin.branch %}
17 <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">
18 {{ checkin.branch }}
19 </span>
20 {% endif %}
@@ -21,53 +36,99 @@
21 {% if checkin.is_merge %}
22 <span class="inline-flex items-center rounded-full bg-purple-900/50 px-2 py-0.5 text-xs text-purple-300">merge</span>
23 {% endif %}
24 </div>
25 </div>
26 <div class="px-6 py-3 border-t border-gray-700 bg-gray-800/50 flex items-center gap-6 text-xs">
27 <div class="flex items-center gap-2">
28 <span class="text-gray-500">Commit</span>
29 <code class="font-mono text-gray-300 text-xs break-all">{{ checkin.uuid }}</code>
30 </div>
31 {% if checkin.parent_uuid %}
32 <div class="flex items-center gap-2">
33 <span class="text-gray-500">Parent</span>
34 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=checkin.parent_uuid %}"
35 class="font-mono text-brand-light hover:text-brand text-xs">{{ checkin.parent_uuid|truncatechars:16 }}</a>
36 </div>
37 {% endif %}
38 <div class="flex items-center gap-2">
39 <span class="text-gray-500">Files changed</span>
40 <span class="text-gray-300">{{ checkin.files_changed|length }}</span>
41 </div>
42 </div>
43 </div>
44
45 <!-- Changed files -->
46 {% if checkin.files_changed %}
47 <div class="rounded-lg bg-gray-800 border border-gray-700">
48 <div class="px-4 py-3 border-b border-gray-700 text-sm font-medium text-gray-300">
49 {{ checkin.files_changed|length }} file{{ checkin.files_changed|length|pluralize }} changed
50 </div>
51 <div class="divide-y divide-gray-700">
52 {% for f in checkin.files_changed %}
53 <div class="px-4 py-2.5 flex items-center gap-3 hover:bg-gray-700/30">
54 {% if f.change_type == "added" %}
55 <span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-green-900/50 text-green-300">A</span>
56 {% elif f.change_type == "deleted" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57 <span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-red-900/50 text-red-300">D</span>
58 {% else %}
59 <span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-yellow-900/50 text-yellow-300">M</span>
60 {% endif %}
61 {% if f.uuid %}
62 <a href="{% url 'fossil:code_file' slug=project.slug filepath=f.name %}"
63 class="text-sm font-mono text-brand-light hover:text-brand">{{ f.name }}</a>
64 {% else %}
65 <span class="text-sm font-mono text-gray-400">{{ f.name }}</span>
66 {% endif %}
67 </div>
68 {% endfor %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69 </div>
70 </div>
 
71 {% endif %}
72 </div>
73 {% endblock %}
74
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -1,7 +1,22 @@
1 {% extends "base.html" %}
2 {% block title %}{{ checkin.uuid|truncatechars:12 }} — {{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block extra_head %}
5 <style>
6 .diff-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
7 .diff-table td { padding: 0 8px; white-space: pre; vertical-align: top; line-height: 1.4rem; }
8 .diff-line-add { background: rgba(34, 197, 94, 0.1); }
9 .diff-line-add td:last-child { color: #86efac; }
10 .diff-line-del { background: rgba(239, 68, 68, 0.1); }
11 .diff-line-del td:last-child { color: #fca5a5; }
12 .diff-line-hunk { background: rgba(96, 165, 250, 0.08); }
13 .diff-line-hunk td { color: #93c5fd; font-weight: 500; }
14 .diff-line-header td { color: #6b7280; font-weight: 600; }
15 .diff-gutter { width: 1%; user-select: none; color: #4b5563; text-align: right; padding: 0 6px; border-right: 1px solid #374151; }
16 </style>
17 {% endblock %}
18
19 {% block content %}
20 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
21 {% include "fossil/_project_nav.html" %}
22
@@ -10,11 +25,11 @@
25 <div class="rounded-lg bg-gray-800 border border-gray-700">
26 <div class="px-6 py-5">
27 <p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p>
28 <div class="mt-3 flex items-center gap-4 flex-wrap text-sm">
29 <span class="font-medium text-gray-200">{{ checkin.user }}</span>
30 <span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }}</span>
31 {% if checkin.branch %}
32 <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">
33 {{ checkin.branch }}
34 </span>
35 {% endif %}
@@ -21,53 +36,99 @@
36 {% if checkin.is_merge %}
37 <span class="inline-flex items-center rounded-full bg-purple-900/50 px-2 py-0.5 text-xs text-purple-300">merge</span>
38 {% endif %}
39 </div>
40 </div>
41 <div class="px-6 py-3 border-t border-gray-700 bg-gray-800/50 flex items-center gap-6 flex-wrap text-xs">
42 <div class="flex items-center gap-2">
43 <span class="text-gray-500">Commit</span>
44 <code class="font-mono text-gray-300 break-all">{{ checkin.uuid }}</code>
45 </div>
46 {% if checkin.parent_uuid %}
47 <div class="flex items-center gap-2">
48 <span class="text-gray-500">Parent</span>
49 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=checkin.parent_uuid %}"
50 class="font-mono text-brand-light hover:text-brand">{{ checkin.parent_uuid|truncatechars:16 }}</a>
51 </div>
52 {% endif %}
53 </div>
54 </div>
55
56 <!-- File summary bar -->
57 {% if file_diffs %}
58 <div class="rounded-lg bg-gray-800 border border-gray-700 px-4 py-3">
59 <div class="flex items-center gap-4 text-sm">
60 <span class="text-gray-300 font-medium">{{ file_diffs|length }} file{{ file_diffs|length|pluralize }} changed</span>
61 {% with total_add=0 total_del=0 %}
62 {% for fd in file_diffs %}
63 {% if fd.additions %}
64 <span class="text-green-400">+{{ fd.additions }}</span>
65 {% endif %}
66 {% if fd.deletions %}
67 <span class="text-red-400">-{{ fd.deletions }}</span>
68 {% endif %}
69 {% endfor %}
70 {% endwith %}
71 </div>
72 <div class="mt-2 flex flex-wrap gap-2">
73 {% for fd in file_diffs %}
74 <a href="#diff-{{ forloop.counter }}" class="text-xs font-mono text-brand-light hover:text-brand">
75 {% if fd.change_type == "added" %}
76 <span class="text-green-400">+</span>
77 {% elif fd.change_type == "deleted" %}
78 <span class="text-red-400">-</span>
79 {% else %}
80 <span class="text-yellow-400">~</span>
81 {% endif %}
82 {{ fd.name }}
83 </a>
84 {% endfor %}
85 </div>
86 </div>
87
88 <!-- Diffs -->
89 {% for fd in file_diffs %}
90 <div class="rounded-lg bg-gray-800 border border-gray-700 overflow-hidden" id="diff-{{ forloop.counter }}">
91 <div class="px-4 py-2.5 border-b border-gray-700 flex items-center justify-between bg-gray-900/50">
92 <div class="flex items-center gap-3">
93 {% if fd.change_type == "added" %}
94 <span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-green-900/50 text-green-300">A</span>
95 {% elif fd.change_type == "deleted" %}
96 <span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-red-900/50 text-red-300">D</span>
97 {% else %}
98 <span class="inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold bg-yellow-900/50 text-yellow-300">M</span>
99 {% endif %}
100 {% if fd.uuid %}
101 <a href="{% url 'fossil:code_file' slug=project.slug filepath=fd.name %}"
102 class="text-sm font-mono text-brand-light hover:text-brand">{{ fd.name }}</a>
103 {% else %}
104 <span class="text-sm font-mono text-gray-400">{{ fd.name }}</span>
105 {% endif %}
106 </div>
107 <div class="flex items-center gap-2 text-xs">
108 {% if fd.additions %}<span class="text-green-400">+{{ fd.additions }}</span>{% endif %}
109 {% if fd.deletions %}<span class="text-red-400">-{{ fd.deletions }}</span>{% endif %}
110 </div>
111 </div>
112 <div class="overflow-x-auto">
113 {% if fd.is_binary %}
114 <p class="p-4 text-sm text-gray-500">Binary file</p>
115 {% elif fd.diff_lines %}
116 <table class="diff-table">
117 <tbody>
118 {% for dl in fd.diff_lines %}
119 <tr class="diff-line-{{ dl.type }}">
120 <td class="diff-gutter"></td>
121 <td>{{ dl.text }}</td>
122 </tr>
123 {% endfor %}
124 </tbody>
125 </table>
126 {% else %}
127 <p class="p-4 text-sm text-gray-500">No diff available</p>
128 {% endif %}
129 </div>
130 </div>
131 {% endfor %}
132 {% endif %}
133 </div>
134 {% endblock %}
135
--- templates/projects/project_detail.html
+++ templates/projects/project_detail.html
@@ -1,78 +1,136 @@
11
{% extends "base.html" %}
22
{% block title %}{{ project.name }} — Fossilrepo{% endblock %}
33
44
{% block content %}
5
-<div class="mb-6">
6
- <a href="{% url 'projects:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Projects</a>
7
-</div>
8
-
9
-<div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
10
- <div class="px-6 py-5 sm:flex sm:items-center sm:justify-between">
11
- <div>
12
- <h1 class="text-2xl font-bold text-gray-100">{{ project.name }}</h1>
13
- <p class="mt-1 text-sm text-gray-400">{{ project.slug }}</p>
14
- </div>
15
- <div class="mt-4 flex gap-3 sm:mt-0">
16
- {% if perms.projects.change_project %}
17
- <a href="{% url 'projects:update' slug=project.slug %}"
18
- class="rounded-md bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
19
- Edit
20
- </a>
21
- {% endif %}
22
- {% if perms.projects.delete_project %}
23
- <a href="{% url 'projects:delete' slug=project.slug %}"
24
- class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500">
25
- Delete
26
- </a>
27
- {% endif %}
28
- </div>
29
- </div>
30
-
31
- <div class="border-t border-gray-700 px-6 py-5">
32
- <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
33
- <div>
34
- <dt class="text-sm font-medium text-gray-400">Visibility</dt>
35
- <dd class="mt-1 text-sm">
36
- {% if project.visibility == "public" %}
37
- <span class="inline-flex rounded-full bg-green-900/50 px-2 text-xs font-semibold leading-5 text-green-300">Public</span>
38
- {% elif project.visibility == "internal" %}
39
- <span class="inline-flex rounded-full bg-yellow-900/50 px-2 text-xs font-semibold leading-5 text-yellow-300">Internal</span>
40
- {% else %}
41
- <span class="inline-flex rounded-full bg-gray-700 px-2 text-xs font-semibold leading-5 text-gray-300">Private</span>
42
- {% endif %}
43
- </dd>
44
- </div>
45
- <div>
46
- <dt class="text-sm font-medium text-gray-400">GUID</dt>
47
- <dd class="mt-1 text-sm text-gray-400 font-mono">{{ project.guid }}</dd>
48
- </div>
49
- <div class="sm:col-span-2">
50
- <dt class="text-sm font-medium text-gray-400">Description</dt>
51
- <dd class="mt-1 text-sm text-gray-100">{{ project.description|default:"No description." }}</dd>
52
- </div>
53
- <div>
54
- <dt class="text-sm font-medium text-gray-400">Created</dt>
55
- <dd class="mt-1 text-sm text-gray-400">{{ project.created_at|date:"N j, Y g:i a" }} by {{ project.created_by|default:"system" }}</dd>
56
- </div>
57
- <div>
58
- <dt class="text-sm font-medium text-gray-400">Updated</dt>
59
- <dd class="mt-1 text-sm text-gray-400">{{ project.updated_at|date:"N j, Y g:i a" }}</dd>
60
- </div>
61
- </dl>
62
- </div>
63
-</div>
64
-
65
-<div class="mt-8">
66
- <div class="md:flex md:items-center md:justify-between mb-4">
67
- <h2 class="text-lg font-semibold text-gray-100">Teams</h2>
68
- {% if perms.projects.change_project %}
69
- <a href="{% url 'projects:team_add' slug=project.slug %}"
70
- class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
71
- Add Team
72
- </a>
73
- {% endif %}
74
- </div>
75
-
76
- {% include "projects/partials/project_team_table.html" %}
5
+<div class="flex items-center justify-between mb-6">
6
+ <div>
7
+ <h1 class="text-2xl font-bold text-gray-100">{{ project.name }}</h1>
8
+ <p class="mt-1 text-sm text-gray-400">{{ project.description|default:"No description." }}</p>
9
+ </div>
10
+ <div class="flex gap-3">
11
+ {% if perms.projects.change_project %}
12
+ <a href="{% url 'projects:update' slug=project.slug %}"
13
+ class="rounded-md bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
14
+ Edit
15
+ </a>
16
+ {% endif %}
17
+ </div>
18
+</div>
19
+
20
+{% if repo_stats %}
21
+<!-- Fossil navigation tabs -->
22
+{% with active_tab="overview" %}
23
+{% include "fossil/_project_nav.html" %}
24
+{% endwith %}
25
+{% endif %}
26
+
27
+<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
28
+ <!-- Main content -->
29
+ <div class="lg:col-span-2 space-y-6">
30
+ {% if repo_stats and recent_commits %}
31
+ <!-- Recent activity -->
32
+ <div class="rounded-lg bg-gray-800 border border-gray-700">
33
+ <div class="px-4 py-3 border-b border-gray-700">
34
+ <h3 class="text-sm font-medium text-gray-300">Recent Activity</h3>
35
+ </div>
36
+ <div class="divide-y divide-gray-700">
37
+ {% for commit in recent_commits %}
38
+ <div class="px-4 py-3 flex items-start gap-3">
39
+ <div class="flex-shrink-0 mt-0.5">
40
+ <div class="w-2.5 h-2.5 rounded-full bg-brand"></div>
41
+ </div>
42
+ <div class="flex-1 min-w-0">
43
+ <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
44
+ class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:80 }}</a>
45
+ <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
46
+ <span>{{ commit.user }}</span>
47
+ <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
48
+ class="font-mono text-brand-light hover:text-brand">{{ commit.uuid|truncatechars:10 }}</a>
49
+ <span>{{ commit.timestamp|timesince }} ago</span>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ {% endfor %}
54
+ </div>
55
+ <div class="px-4 py-2 border-t border-gray-700">
56
+ <a href="{% url 'fossil:timeline' slug=project.slug %}" class="text-xs text-brand-light hover:text-brand">View full timeline &rarr;</a>
57
+ </div>
58
+ </div>
59
+ {% endif %}
60
+
61
+ <!-- Teams -->
62
+ <div class="rounded-lg bg-gray-800 border border-gray-700">
63
+ <div class="px-4 py-3 border-b border-gray-700 flex items-center justify-between">
64
+ <h3 class="text-sm font-medium text-gray-300">Teams</h3>
65
+ {% if perms.projects.change_project %}
66
+ <a href="{% url 'projects:team_add' slug=project.slug %}" class="text-xs text-brand-light hover:text-brand">+ Add Team</a>
67
+ {% endif %}
68
+ </div>
69
+ {% include "projects/partials/project_team_table.html" %}
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Sidebar -->
74
+ <div class="space-y-4">
75
+ {% if repo_stats %}
76
+ <!-- Repository stats -->
77
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
78
+ <h3 class="text-sm font-medium text-gray-300 mb-3">Repository</h3>
79
+ <div class="space-y-2">
80
+ <div class="flex items-center justify-between">
81
+ <span class="flex items-center gap-2 text-sm text-gray-400">
82
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
83
+ Checkins
84
+ </span>
85
+ <a href="{% url 'fossil:timeline' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.checkin_count|default:"0" }}</a>
86
+ </div>
87
+ <div class="flex items-center justify-between">
88
+ <span class="flex items-center gap-2 text-sm text-gray-400">
89
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
90
+ Files
91
+ </span>
92
+ <a href="{% url 'fossil:code' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.file_count|default:"—" }}</a>
93
+ </div>
94
+ <div class="flex items-center justify-between">
95
+ <span class="flex items-center gap-2 text-sm text-gray-400">
96
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" /></svg>
97
+ Tickets
98
+ </span>
99
+ <a href="{% url 'fossil:tickets' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.ticket_count|default:"0" }}</a>
100
+ </div>
101
+ <div class="flex items-center justify-between">
102
+ <span class="flex items-center gap-2 text-sm text-gray-400">
103
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" /></svg>
104
+ Wiki Pages
105
+ </span>
106
+ <a href="{% url 'fossil:wiki' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.wiki_page_count|default:"0" }}</a>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ {% endif %}
111
+
112
+ <!-- Project info -->
113
+ <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
114
+ <h3 class="text-sm font-medium text-gray-300 mb-3">About</h3>
115
+ <dl class="space-y-2 text-sm">
116
+ <div class="flex items-center justify-between">
117
+ <dt class="text-gray-500">Visibility</dt>
118
+ <dd>
119
+ {% if project.visibility == "public" %}
120
+ <span class="inline-flex rounded-full bg-green-900/50 px-2 text-xs font-semibold leading-5 text-green-300">Public</span>
121
+ {% elif project.visibility == "internal" %}
122
+ <span class="inline-flex rounded-full bg-yellow-900/50 px-2 text-xs font-semibold leading-5 text-yellow-300">Internal</span>
123
+ {% else %}
124
+ <span class="inline-flex rounded-full bg-gray-700 px-2 text-xs font-semibold leading-5 text-gray-300">Private</span>
125
+ {% endif %}
126
+ </dd>
127
+ </div>
128
+ <div class="flex items-center justify-between">
129
+ <dt class="text-gray-500">Created</dt>
130
+ <dd class="text-gray-300">{{ project.created_at|date:"Y-m-d" }}</dd>
131
+ </div>
132
+ </dl>
133
+ </div>
134
+ </div>
77135
</div>
78136
{% endblock %}
79137
--- templates/projects/project_detail.html
+++ templates/projects/project_detail.html
@@ -1,78 +1,136 @@
1 {% extends "base.html" %}
2 {% block title %}{{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="mb-6">
6 <a href="{% url 'projects:list' %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to Projects</a>
7 </div>
8
9 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
10 <div class="px-6 py-5 sm:flex sm:items-center sm:justify-between">
11 <div>
12 <h1 class="text-2xl font-bold text-gray-100">{{ project.name }}</h1>
13 <p class="mt-1 text-sm text-gray-400">{{ project.slug }}</p>
14 </div>
15 <div class="mt-4 flex gap-3 sm:mt-0">
16 {% if perms.projects.change_project %}
17 <a href="{% url 'projects:update' slug=project.slug %}"
18 class="rounded-md bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
19 Edit
20 </a>
21 {% endif %}
22 {% if perms.projects.delete_project %}
23 <a href="{% url 'projects:delete' slug=project.slug %}"
24 class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500">
25 Delete
26 </a>
27 {% endif %}
28 </div>
29 </div>
30
31 <div class="border-t border-gray-700 px-6 py-5">
32 <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
33 <div>
34 <dt class="text-sm font-medium text-gray-400">Visibility</dt>
35 <dd class="mt-1 text-sm">
36 {% if project.visibility == "public" %}
37 <span class="inline-flex rounded-full bg-green-900/50 px-2 text-xs font-semibold leading-5 text-green-300">Public</span>
38 {% elif project.visibility == "internal" %}
39 <span class="inline-flex rounded-full bg-yellow-900/50 px-2 text-xs font-semibold leading-5 text-yellow-300">Internal</span>
40 {% else %}
41 <span class="inline-flex rounded-full bg-gray-700 px-2 text-xs font-semibold leading-5 text-gray-300">Private</span>
42 {% endif %}
43 </dd>
44 </div>
45 <div>
46 <dt class="text-sm font-medium text-gray-400">GUID</dt>
47 <dd class="mt-1 text-sm text-gray-400 font-mono">{{ project.guid }}</dd>
48 </div>
49 <div class="sm:col-span-2">
50 <dt class="text-sm font-medium text-gray-400">Description</dt>
51 <dd class="mt-1 text-sm text-gray-100">{{ project.description|default:"No description." }}</dd>
52 </div>
53 <div>
54 <dt class="text-sm font-medium text-gray-400">Created</dt>
55 <dd class="mt-1 text-sm text-gray-400">{{ project.created_at|date:"N j, Y g:i a" }} by {{ project.created_by|default:"system" }}</dd>
56 </div>
57 <div>
58 <dt class="text-sm font-medium text-gray-400">Updated</dt>
59 <dd class="mt-1 text-sm text-gray-400">{{ project.updated_at|date:"N j, Y g:i a" }}</dd>
60 </div>
61 </dl>
62 </div>
63 </div>
64
65 <div class="mt-8">
66 <div class="md:flex md:items-center md:justify-between mb-4">
67 <h2 class="text-lg font-semibold text-gray-100">Teams</h2>
68 {% if perms.projects.change_project %}
69 <a href="{% url 'projects:team_add' slug=project.slug %}"
70 class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
71 Add Team
72 </a>
73 {% endif %}
74 </div>
75
76 {% include "projects/partials/project_team_table.html" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77 </div>
78 {% endblock %}
79
--- templates/projects/project_detail.html
+++ templates/projects/project_detail.html
@@ -1,78 +1,136 @@
1 {% extends "base.html" %}
2 {% block title %}{{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 <div class="flex items-center justify-between mb-6">
6 <div>
7 <h1 class="text-2xl font-bold text-gray-100">{{ project.name }}</h1>
8 <p class="mt-1 text-sm text-gray-400">{{ project.description|default:"No description." }}</p>
9 </div>
10 <div class="flex gap-3">
11 {% if perms.projects.change_project %}
12 <a href="{% url 'projects:update' slug=project.slug %}"
13 class="rounded-md bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 hover:bg-gray-600">
14 Edit
15 </a>
16 {% endif %}
17 </div>
18 </div>
19
20 {% if repo_stats %}
21 <!-- Fossil navigation tabs -->
22 {% with active_tab="overview" %}
23 {% include "fossil/_project_nav.html" %}
24 {% endwith %}
25 {% endif %}
26
27 <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
28 <!-- Main content -->
29 <div class="lg:col-span-2 space-y-6">
30 {% if repo_stats and recent_commits %}
31 <!-- Recent activity -->
32 <div class="rounded-lg bg-gray-800 border border-gray-700">
33 <div class="px-4 py-3 border-b border-gray-700">
34 <h3 class="text-sm font-medium text-gray-300">Recent Activity</h3>
35 </div>
36 <div class="divide-y divide-gray-700">
37 {% for commit in recent_commits %}
38 <div class="px-4 py-3 flex items-start gap-3">
39 <div class="flex-shrink-0 mt-0.5">
40 <div class="w-2.5 h-2.5 rounded-full bg-brand"></div>
41 </div>
42 <div class="flex-1 min-w-0">
43 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
44 class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:80 }}</a>
45 <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
46 <span>{{ commit.user }}</span>
47 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
48 class="font-mono text-brand-light hover:text-brand">{{ commit.uuid|truncatechars:10 }}</a>
49 <span>{{ commit.timestamp|timesince }} ago</span>
50 </div>
51 </div>
52 </div>
53 {% endfor %}
54 </div>
55 <div class="px-4 py-2 border-t border-gray-700">
56 <a href="{% url 'fossil:timeline' slug=project.slug %}" class="text-xs text-brand-light hover:text-brand">View full timeline &rarr;</a>
57 </div>
58 </div>
59 {% endif %}
60
61 <!-- Teams -->
62 <div class="rounded-lg bg-gray-800 border border-gray-700">
63 <div class="px-4 py-3 border-b border-gray-700 flex items-center justify-between">
64 <h3 class="text-sm font-medium text-gray-300">Teams</h3>
65 {% if perms.projects.change_project %}
66 <a href="{% url 'projects:team_add' slug=project.slug %}" class="text-xs text-brand-light hover:text-brand">+ Add Team</a>
67 {% endif %}
68 </div>
69 {% include "projects/partials/project_team_table.html" %}
70 </div>
71 </div>
72
73 <!-- Sidebar -->
74 <div class="space-y-4">
75 {% if repo_stats %}
76 <!-- Repository stats -->
77 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
78 <h3 class="text-sm font-medium text-gray-300 mb-3">Repository</h3>
79 <div class="space-y-2">
80 <div class="flex items-center justify-between">
81 <span class="flex items-center gap-2 text-sm text-gray-400">
82 <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
83 Checkins
84 </span>
85 <a href="{% url 'fossil:timeline' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.checkin_count|default:"0" }}</a>
86 </div>
87 <div class="flex items-center justify-between">
88 <span class="flex items-center gap-2 text-sm text-gray-400">
89 <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
90 Files
91 </span>
92 <a href="{% url 'fossil:code' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.file_count|default:"—" }}</a>
93 </div>
94 <div class="flex items-center justify-between">
95 <span class="flex items-center gap-2 text-sm text-gray-400">
96 <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" /></svg>
97 Tickets
98 </span>
99 <a href="{% url 'fossil:tickets' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.ticket_count|default:"0" }}</a>
100 </div>
101 <div class="flex items-center justify-between">
102 <span class="flex items-center gap-2 text-sm text-gray-400">
103 <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" /></svg>
104 Wiki Pages
105 </span>
106 <a href="{% url 'fossil:wiki' slug=project.slug %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ repo_stats.wiki_page_count|default:"0" }}</a>
107 </div>
108 </div>
109 </div>
110 {% endif %}
111
112 <!-- Project info -->
113 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
114 <h3 class="text-sm font-medium text-gray-300 mb-3">About</h3>
115 <dl class="space-y-2 text-sm">
116 <div class="flex items-center justify-between">
117 <dt class="text-gray-500">Visibility</dt>
118 <dd>
119 {% if project.visibility == "public" %}
120 <span class="inline-flex rounded-full bg-green-900/50 px-2 text-xs font-semibold leading-5 text-green-300">Public</span>
121 {% elif project.visibility == "internal" %}
122 <span class="inline-flex rounded-full bg-yellow-900/50 px-2 text-xs font-semibold leading-5 text-yellow-300">Internal</span>
123 {% else %}
124 <span class="inline-flex rounded-full bg-gray-700 px-2 text-xs font-semibold leading-5 text-gray-300">Private</span>
125 {% endif %}
126 </dd>
127 </div>
128 <div class="flex items-center justify-between">
129 <dt class="text-gray-500">Created</dt>
130 <dd class="text-gray-300">{{ project.created_at|date:"Y-m-d" }}</dd>
131 </div>
132 </dl>
133 </div>
134 </div>
135 </div>
136 {% endblock %}
137

Keyboard Shortcuts

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