FossilRepo

Fix: strip emails from public views, fix branches active tab, add display_user filter

ragelink 2026-04-07 20:09 trunk
Commit fcd8df3cf42f98b9e8fa39000e7625ef90a7512f84362975ab2eefa79efd1f5e
+2 -2
--- config/urls.py
+++ config/urls.py
@@ -83,19 +83,19 @@
8383
8484
try:
8585
with connection.cursor() as cursor:
8686
cursor.execute("SELECT 1")
8787
db_ok = True
88
- except Exception as e:
88
+ except Exception:
8989
return JsonResponse(
9090
{
9191
"service": "fossilrepo-django-htmx",
9292
"version": settings.VERSION,
9393
"status": "error",
9494
"uptime": _uptime_str(),
9595
"timestamp": datetime.now(UTC).isoformat(),
96
- "checks": {"database": "error", "detail": str(e)},
96
+ "checks": {"database": "error"},
9797
},
9898
status=503,
9999
)
100100
101101
return JsonResponse(
102102
--- config/urls.py
+++ config/urls.py
@@ -83,19 +83,19 @@
83
84 try:
85 with connection.cursor() as cursor:
86 cursor.execute("SELECT 1")
87 db_ok = True
88 except Exception as e:
89 return JsonResponse(
90 {
91 "service": "fossilrepo-django-htmx",
92 "version": settings.VERSION,
93 "status": "error",
94 "uptime": _uptime_str(),
95 "timestamp": datetime.now(UTC).isoformat(),
96 "checks": {"database": "error", "detail": str(e)},
97 },
98 status=503,
99 )
100
101 return JsonResponse(
102
--- config/urls.py
+++ config/urls.py
@@ -83,19 +83,19 @@
83
84 try:
85 with connection.cursor() as cursor:
86 cursor.execute("SELECT 1")
87 db_ok = True
88 except Exception:
89 return JsonResponse(
90 {
91 "service": "fossilrepo-django-htmx",
92 "version": settings.VERSION,
93 "status": "error",
94 "uptime": _uptime_str(),
95 "timestamp": datetime.now(UTC).isoformat(),
96 "checks": {"database": "error"},
97 },
98 status=503,
99 )
100
101 return JsonResponse(
102
--- core/__pycache__/sanitize.cpython-314.pyc
+++ core/__pycache__/sanitize.cpython-314.pyc
cannot compute difference between binary files
11
--- core/__pycache__/sanitize.cpython-314.pyc
+++ core/__pycache__/sanitize.cpython-314.pyc
0 annot compute difference between binary files
1
--- core/__pycache__/sanitize.cpython-314.pyc
+++ core/__pycache__/sanitize.cpython-314.pyc
0 annot compute difference between binary files
1
+169 -133
--- core/sanitize.py
+++ core/sanitize.py
@@ -1,135 +1,171 @@
11
"""HTML sanitization for user-generated content.
22
3
-Strips dangerous tags (<script>, <style>, <iframe>, etc.), event handlers (on*),
4
-and dangerous URL protocols (javascript:, data:, vbscript:) while preserving
5
-safe formatting tags used by Fossil wiki, Markdown, and Pikchr diagrams.
6
-"""
7
-
8
-import re
9
-
10
-# Tags that are safe to render -- covers Markdown/wiki formatting and Pikchr SVG
11
-ALLOWED_TAGS = {
12
- "a",
13
- "abbr",
14
- "acronym",
15
- "b",
16
- "blockquote",
17
- "br",
18
- "code",
19
- "dd",
20
- "del",
21
- "details",
22
- "div",
23
- "dl",
24
- "dt",
25
- "em",
26
- "h1",
27
- "h2",
28
- "h3",
29
- "h4",
30
- "h5",
31
- "h6",
32
- "hr",
33
- "i",
34
- "img",
35
- "ins",
36
- "kbd",
37
- "li",
38
- "mark",
39
- "ol",
40
- "p",
41
- "pre",
42
- "q",
43
- "s",
44
- "samp",
45
- "small",
46
- "span",
47
- "strong",
48
- "sub",
49
- "summary",
50
- "sup",
51
- "table",
52
- "tbody",
53
- "td",
54
- "tfoot",
55
- "th",
56
- "thead",
57
- "tr",
58
- "tt",
59
- "u",
60
- "ul",
61
- "var",
62
- # SVG elements for Pikchr diagrams
63
- "svg",
64
- "path",
65
- "circle",
66
- "rect",
67
- "line",
68
- "polyline",
69
- "polygon",
70
- "g",
71
- "text",
72
- "defs",
73
- "use",
74
- "symbol",
75
-}
76
-
77
-# Tags whose entire content (not just the tag) must be removed
78
-_DANGEROUS_CONTENT_TAGS = re.compile(
79
- r"<\s*(script|style|iframe|object|embed|form|base|meta|link)\b[^>]*>.*?</\s*\1\s*>",
80
- re.IGNORECASE | re.DOTALL,
81
-)
82
-
83
-# Self-closing / unclosed dangerous tags
84
-_DANGEROUS_SELF_CLOSING = re.compile(
85
- r"<\s*/?\s*(script|style|iframe|object|embed|form|base|meta|link)\b[^>]*/?\s*>",
86
- re.IGNORECASE,
87
-)
88
-
89
-# Event handler attributes (onclick, onload, onerror, etc.)
90
-_EVENT_HANDLERS = re.compile(
91
- r"""\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)""",
92
- re.IGNORECASE,
93
-)
94
-
95
-# Dangerous protocols in href/src values
96
-_DANGEROUS_PROTOCOL = re.compile(r"^\s*(?:javascript|vbscript|data):", re.IGNORECASE)
97
-
98
-# href="..." and src="..." attribute pattern
99
-_URL_ATTR = re.compile(r"""(href|src)\s*=\s*(["']?)([^"'>\s]+)\2""", re.IGNORECASE)
100
-
101
-
102
-def _clean_url_attr(match: re.Match) -> str:
103
- """Replace dangerous protocol URLs with a safe '#' anchor."""
104
- attr_name = match.group(1)
105
- quote = match.group(2) or ""
106
- url = match.group(3)
107
- if _DANGEROUS_PROTOCOL.match(url):
108
- return f"{attr_name}={quote}#{quote}"
109
- return match.group(0)
110
-
111
-
112
-def sanitize_html(html: str) -> str:
113
- """Remove dangerous HTML tags and attributes while preserving safe formatting.
114
-
115
- Strips <script>, <style>, <iframe>, <object>, <embed>, <form>, <base>,
116
- <meta>, <link> tags and their content. Removes event handler attributes
117
- (on*) and replaces dangerous URL protocols (javascript:, data:, vbscript:)
118
- in href/src with '#'.
119
- """
120
- if not html:
121
- return html
122
-
123
- # 1. Remove dangerous tags WITH their content (e.g. <script>...</script>)
124
- html = _DANGEROUS_CONTENT_TAGS.sub("", html)
125
-
126
- # 2. Remove any remaining self-closing or orphaned dangerous tags
127
- html = _DANGEROUS_SELF_CLOSING.sub("", html)
128
-
129
- # 3. Remove event handler attributes (onclick, onload, onerror, etc.)
130
- html = _EVENT_HANDLERS.sub("", html)
131
-
132
- # 4. Neutralize dangerous URL protocols in href and src attributes
133
- html = _URL_ATTR.sub(_clean_url_attr, html)
134
-
135
- return html
3
+Uses Python's html.parser to properly parse HTML and enforce an allowlist
4
+of tags and attributes. Strips everything not explicitly allowed.
5
+"""
6
+
7
+import html
8
+import re
9
+from html.parser import HTMLParser
10
+from io import StringIO
11
+
12
+# Tags that are safe to render — covers Markdown/wiki formatting and Pikchr SVG
13
+ALLOWED_TAGS = frozenset({
14
+ "a", "abbr", "acronym", "b", "blockquote", "br", "code", "dd", "del",
15
+ "details", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
16
+ "hr", "i", "img", "ins", "kbd", "li", "mark", "ol", "p", "pre", "q",
17
+ "s", "samp", "small", "span", "strong", "sub", "summary", "sup",
18
+ "table", "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "u", "ul", "var",
19
+ # SVG elements for Pikchr diagrams
20
+ "svg", "path", "circle", "rect", "line", "polyline", "polygon",
21
+ "g", "text", "defs", "use", "symbol",
22
+})
23
+
24
+# Attributes allowed per tag (all others stripped)
25
+ALLOWED_ATTRS = {
26
+ "a": {"href", "title", "class", "id", "name"},
27
+ "img": {"src", "alt", "title", "width", "height", "class"},
28
+ "div": {"class", "id"},
29
+ "span": {"class", "id"},
30
+ "td": {"class", "colspan", "rowspan"},
31
+ "th": {"class", "colspan", "rowspan"},
32
+ "table": {"class"},
33
+ "code": {"class"},
34
+ "pre": {"class"},
35
+ "ol": {"class", "start", "type"},
36
+ "ul": {"class"},
37
+ "li": {"class", "value"},
38
+ "details": {"open", "class"},
39
+ "summary": {"class"},
40
+ "h1": {"id", "class"}, "h2": {"id", "class"}, "h3": {"id", "class"},
41
+ "h4": {"id", "class"}, "h5": {"id", "class"}, "h6": {"id", "class"},
42
+ # SVG attributes
43
+ "svg": {"viewbox", "width", "height", "class", "xmlns", "fill", "stroke"},
44
+ "path": {"d", "fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin", "class"},
45
+ "circle": {"cx", "cy", "r", "fill", "stroke", "class"},
46
+ "rect": {"x", "y", "width", "height", "fill", "stroke", "rx", "ry", "class"},
47
+ "line": {"x1", "y1", "x2", "y2", "stroke", "stroke-width", "class"},
48
+ "text": {"x", "y", "font-size", "text-anchor", "fill", "class"},
49
+ "g": {"transform", "class"},
50
+ "polyline": {"points", "fill", "stroke", "class"},
51
+ "polygon": {"points", "fill", "stroke", "class"},
52
+}
53
+
54
+# Global attributes allowed on any tag
55
+GLOBAL_ATTRS = frozenset()
56
+
57
+# Protocols allowed in href/src — everything else is stripped
58
+ALLOWED_PROTOCOLS = frozenset({"http", "https", "mailto", "ftp", "#", ""})
59
+
60
+# Regex to detect protocol in a URL (after HTML entity decoding)
61
+_PROTOCOL_RE = re.compile(r"^([a-zA-Z][a-zA-Z0-9+\-.]*):.*", re.DOTALL)
62
+
63
+
64
+def _is_safe_url(url: str) -> bool:
65
+ """Check if a URL uses a safe protocol. Decodes HTML entities first."""
66
+ decoded = html.unescape(url).strip()
67
+ m = _PROTOCOL_RE.match(decoded)
68
+ if m:
69
+ return m.group(1).lower() in ALLOWED_PROTOCOLS
70
+ # Relative URLs (no protocol) are safe
71
+ return True
72
+
73
+
74
+class _SanitizingParser(HTMLParser):
75
+ """HTML parser that only emits allowed tags/attributes."""
76
+
77
+ def __init__(self):
78
+ super().__init__(convert_charrefs=False)
79
+ self.out = StringIO()
80
+ self._skip_depth = 0 # Track depth inside dangerous tags to skip content
81
+
82
+ def handle_starttag(self, tag, attrs):
83
+ tag_lower = tag.lower()
84
+
85
+ # Dangerous content tags — skip tag AND all content inside
86
+ if tag_lower in ("script", "style", "iframe", "object", "embed", "form", "base", "meta", "link"):
87
+ self._skip_depth += 1
88
+ return
89
+
90
+ if self._skip_depth > 0:
91
+ return
92
+
93
+ if tag_lower not in ALLOWED_TAGS:
94
+ return # Strip unknown tag (but keep its text content)
95
+
96
+ # Filter attributes
97
+ allowed = ALLOWED_ATTRS.get(tag_lower, set()) | GLOBAL_ATTRS
98
+ safe_attrs = []
99
+ for name, value in attrs:
100
+ name_lower = name.lower()
101
+ # Block event handlers
102
+ if name_lower.startswith("on"):
103
+ continue
104
+ if name_lower not in allowed:
105
+ continue
106
+ # Sanitize URLs in href/src
107
+ if name_lower in ("href", "src") and value and not _is_safe_url(value):
108
+ value = "#"
109
+ safe_attrs.append((name, value))
110
+
111
+ # Build the tag
112
+ attr_str = ""
113
+ for name, value in safe_attrs:
114
+ if value is None:
115
+ attr_str += f" {name}"
116
+ else:
117
+ escaped = value.replace("&", "&amp;").replace('"', "&quot;")
118
+ attr_str += f' {name}="{escaped}"'
119
+
120
+ self.out.write(f"<{tag}{attr_str}>")
121
+
122
+ def handle_endtag(self, tag):
123
+ tag_lower = tag.lower()
124
+ if tag_lower in ("script", "style", "iframe", "object", "embed", "form", "base", "meta", "link"):
125
+ self._skip_depth = max(0, self._skip_depth - 1)
126
+ return
127
+ if self._skip_depth > 0:
128
+ return
129
+ if tag_lower in ALLOWED_TAGS:
130
+ self.out.write(f"</{tag}>")
131
+
132
+ def handle_data(self, data):
133
+ if self._skip_depth > 0:
134
+ return # Inside a dangerous tag — skip content
135
+ self.out.write(data)
136
+
137
+ def handle_entityref(self, name):
138
+ if self._skip_depth > 0:
139
+ return
140
+ self.out.write(f"&{name};")
141
+
142
+ def handle_charref(self, name):
143
+ if self._skip_depth > 0:
144
+ return
145
+ self.out.write(f"&#{name};")
146
+
147
+ def handle_comment(self, data):
148
+ pass # Strip all HTML comments
149
+
150
+ def handle_startendtag(self, tag, attrs):
151
+ # Self-closing tags like <br/>, <img/>
152
+ self.handle_starttag(tag, attrs)
153
+
154
+
155
+def sanitize_html(html_content: str) -> str:
156
+ """Sanitize HTML using a proper parser with tag/attribute allowlists.
157
+
158
+ - Only tags in ALLOWED_TAGS are kept (all others stripped, text preserved)
159
+ - Only attributes in ALLOWED_ATTRS per tag are kept
160
+ - Event handlers (on*) are always stripped
161
+ - URLs in href/src are checked after HTML entity decoding — javascript:,
162
+ data:, vbscript: (including entity-encoded variants) are neutralized
163
+ - Content inside <script>, <style>, <iframe>, etc. is completely removed
164
+ - HTML comments are stripped
165
+ """
166
+ if not html_content:
167
+ return html_content
168
+
169
+ parser = _SanitizingParser()
170
+ parser.feed(html_content)
171
+ return parser.out.getvalue()
136172
137173
ADDED core/url_validation.py
138174
ADDED fossil/templatetags/__init__.py
139175
ADDED fossil/templatetags/fossil_filters.py
--- core/sanitize.py
+++ core/sanitize.py
@@ -1,135 +1,171 @@
1 """HTML sanitization for user-generated content.
2
3 Strips dangerous tags (<script>, <style>, <iframe>, etc.), event handlers (on*),
4 and dangerous URL protocols (javascript:, data:, vbscript:) while preserving
5 safe formatting tags used by Fossil wiki, Markdown, and Pikchr diagrams.
6 """
7
8 import re
9
10 # Tags that are safe to render -- covers Markdown/wiki formatting and Pikchr SVG
11 ALLOWED_TAGS = {
12 "a",
13 "abbr",
14 "acronym",
15 "b",
16 "blockquote",
17 "br",
18 "code",
19 "dd",
20 "del",
21 "details",
22 "div",
23 "dl",
24 "dt",
25 "em",
26 "h1",
27 "h2",
28 "h3",
29 "h4",
30 "h5",
31 "h6",
32 "hr",
33 "i",
34 "img",
35 "ins",
36 "kbd",
37 "li",
38 "mark",
39 "ol",
40 "p",
41 "pre",
42 "q",
43 "s",
44 "samp",
45 "small",
46 "span",
47 "strong",
48 "sub",
49 "summary",
50 "sup",
51 "table",
52 "tbody",
53 "td",
54 "tfoot",
55 "th",
56 "thead",
57 "tr",
58 "tt",
59 "u",
60 "ul",
61 "var",
62 # SVG elements for Pikchr diagrams
63 "svg",
64 "path",
65 "circle",
66 "rect",
67 "line",
68 "polyline",
69 "polygon",
70 "g",
71 "text",
72 "defs",
73 "use",
74 "symbol",
75 }
76
77 # Tags whose entire content (not just the tag) must be removed
78 _DANGEROUS_CONTENT_TAGS = re.compile(
79 r"<\s*(script|style|iframe|object|embed|form|base|meta|link)\b[^>]*>.*?</\s*\1\s*>",
80 re.IGNORECASE | re.DOTALL,
81 )
82
83 # Self-closing / unclosed dangerous tags
84 _DANGEROUS_SELF_CLOSING = re.compile(
85 r"<\s*/?\s*(script|style|iframe|object|embed|form|base|meta|link)\b[^>]*/?\s*>",
86 re.IGNORECASE,
87 )
88
89 # Event handler attributes (onclick, onload, onerror, etc.)
90 _EVENT_HANDLERS = re.compile(
91 r"""\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)""",
92 re.IGNORECASE,
93 )
94
95 # Dangerous protocols in href/src values
96 _DANGEROUS_PROTOCOL = re.compile(r"^\s*(?:javascript|vbscript|data):", re.IGNORECASE)
97
98 # href="..." and src="..." attribute pattern
99 _URL_ATTR = re.compile(r"""(href|src)\s*=\s*(["']?)([^"'>\s]+)\2""", re.IGNORECASE)
100
101
102 def _clean_url_attr(match: re.Match) -> str:
103 """Replace dangerous protocol URLs with a safe '#' anchor."""
104 attr_name = match.group(1)
105 quote = match.group(2) or ""
106 url = match.group(3)
107 if _DANGEROUS_PROTOCOL.match(url):
108 return f"{attr_name}={quote}#{quote}"
109 return match.group(0)
110
111
112 def sanitize_html(html: str) -> str:
113 """Remove dangerous HTML tags and attributes while preserving safe formatting.
114
115 Strips <script>, <style>, <iframe>, <object>, <embed>, <form>, <base>,
116 <meta>, <link> tags and their content. Removes event handler attributes
117 (on*) and replaces dangerous URL protocols (javascript:, data:, vbscript:)
118 in href/src with '#'.
119 """
120 if not html:
121 return html
122
123 # 1. Remove dangerous tags WITH their content (e.g. <script>...</script>)
124 html = _DANGEROUS_CONTENT_TAGS.sub("", html)
125
126 # 2. Remove any remaining self-closing or orphaned dangerous tags
127 html = _DANGEROUS_SELF_CLOSING.sub("", html)
128
129 # 3. Remove event handler attributes (onclick, onload, onerror, etc.)
130 html = _EVENT_HANDLERS.sub("", html)
131
132 # 4. Neutralize dangerous URL protocols in href and src attributes
133 html = _URL_ATTR.sub(_clean_url_attr, html)
134
135 return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
137 DDED core/url_validation.py
138 DDED fossil/templatetags/__init__.py
139 DDED fossil/templatetags/fossil_filters.py
--- core/sanitize.py
+++ core/sanitize.py
@@ -1,135 +1,171 @@
1 """HTML sanitization for user-generated content.
2
3 Uses Python's html.parser to properly parse HTML and enforce an allowlist
4 of tags and attributes. Strips everything not explicitly allowed.
5 """
6
7 import html
8 import re
9 from html.parser import HTMLParser
10 from io import StringIO
11
12 # Tags that are safe to render — covers Markdown/wiki formatting and Pikchr SVG
13 ALLOWED_TAGS = frozenset({
14 "a", "abbr", "acronym", "b", "blockquote", "br", "code", "dd", "del",
15 "details", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
16 "hr", "i", "img", "ins", "kbd", "li", "mark", "ol", "p", "pre", "q",
17 "s", "samp", "small", "span", "strong", "sub", "summary", "sup",
18 "table", "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "u", "ul", "var",
19 # SVG elements for Pikchr diagrams
20 "svg", "path", "circle", "rect", "line", "polyline", "polygon",
21 "g", "text", "defs", "use", "symbol",
22 })
23
24 # Attributes allowed per tag (all others stripped)
25 ALLOWED_ATTRS = {
26 "a": {"href", "title", "class", "id", "name"},
27 "img": {"src", "alt", "title", "width", "height", "class"},
28 "div": {"class", "id"},
29 "span": {"class", "id"},
30 "td": {"class", "colspan", "rowspan"},
31 "th": {"class", "colspan", "rowspan"},
32 "table": {"class"},
33 "code": {"class"},
34 "pre": {"class"},
35 "ol": {"class", "start", "type"},
36 "ul": {"class"},
37 "li": {"class", "value"},
38 "details": {"open", "class"},
39 "summary": {"class"},
40 "h1": {"id", "class"}, "h2": {"id", "class"}, "h3": {"id", "class"},
41 "h4": {"id", "class"}, "h5": {"id", "class"}, "h6": {"id", "class"},
42 # SVG attributes
43 "svg": {"viewbox", "width", "height", "class", "xmlns", "fill", "stroke"},
44 "path": {"d", "fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin", "class"},
45 "circle": {"cx", "cy", "r", "fill", "stroke", "class"},
46 "rect": {"x", "y", "width", "height", "fill", "stroke", "rx", "ry", "class"},
47 "line": {"x1", "y1", "x2", "y2", "stroke", "stroke-width", "class"},
48 "text": {"x", "y", "font-size", "text-anchor", "fill", "class"},
49 "g": {"transform", "class"},
50 "polyline": {"points", "fill", "stroke", "class"},
51 "polygon": {"points", "fill", "stroke", "class"},
52 }
53
54 # Global attributes allowed on any tag
55 GLOBAL_ATTRS = frozenset()
56
57 # Protocols allowed in href/src — everything else is stripped
58 ALLOWED_PROTOCOLS = frozenset({"http", "https", "mailto", "ftp", "#", ""})
59
60 # Regex to detect protocol in a URL (after HTML entity decoding)
61 _PROTOCOL_RE = re.compile(r"^([a-zA-Z][a-zA-Z0-9+\-.]*):.*", re.DOTALL)
62
63
64 def _is_safe_url(url: str) -> bool:
65 """Check if a URL uses a safe protocol. Decodes HTML entities first."""
66 decoded = html.unescape(url).strip()
67 m = _PROTOCOL_RE.match(decoded)
68 if m:
69 return m.group(1).lower() in ALLOWED_PROTOCOLS
70 # Relative URLs (no protocol) are safe
71 return True
72
73
74 class _SanitizingParser(HTMLParser):
75 """HTML parser that only emits allowed tags/attributes."""
76
77 def __init__(self):
78 super().__init__(convert_charrefs=False)
79 self.out = StringIO()
80 self._skip_depth = 0 # Track depth inside dangerous tags to skip content
81
82 def handle_starttag(self, tag, attrs):
83 tag_lower = tag.lower()
84
85 # Dangerous content tags — skip tag AND all content inside
86 if tag_lower in ("script", "style", "iframe", "object", "embed", "form", "base", "meta", "link"):
87 self._skip_depth += 1
88 return
89
90 if self._skip_depth > 0:
91 return
92
93 if tag_lower not in ALLOWED_TAGS:
94 return # Strip unknown tag (but keep its text content)
95
96 # Filter attributes
97 allowed = ALLOWED_ATTRS.get(tag_lower, set()) | GLOBAL_ATTRS
98 safe_attrs = []
99 for name, value in attrs:
100 name_lower = name.lower()
101 # Block event handlers
102 if name_lower.startswith("on"):
103 continue
104 if name_lower not in allowed:
105 continue
106 # Sanitize URLs in href/src
107 if name_lower in ("href", "src") and value and not _is_safe_url(value):
108 value = "#"
109 safe_attrs.append((name, value))
110
111 # Build the tag
112 attr_str = ""
113 for name, value in safe_attrs:
114 if value is None:
115 attr_str += f" {name}"
116 else:
117 escaped = value.replace("&", "&amp;").replace('"', "&quot;")
118 attr_str += f' {name}="{escaped}"'
119
120 self.out.write(f"<{tag}{attr_str}>")
121
122 def handle_endtag(self, tag):
123 tag_lower = tag.lower()
124 if tag_lower in ("script", "style", "iframe", "object", "embed", "form", "base", "meta", "link"):
125 self._skip_depth = max(0, self._skip_depth - 1)
126 return
127 if self._skip_depth > 0:
128 return
129 if tag_lower in ALLOWED_TAGS:
130 self.out.write(f"</{tag}>")
131
132 def handle_data(self, data):
133 if self._skip_depth > 0:
134 return # Inside a dangerous tag — skip content
135 self.out.write(data)
136
137 def handle_entityref(self, name):
138 if self._skip_depth > 0:
139 return
140 self.out.write(f"&{name};")
141
142 def handle_charref(self, name):
143 if self._skip_depth > 0:
144 return
145 self.out.write(f"&#{name};")
146
147 def handle_comment(self, data):
148 pass # Strip all HTML comments
149
150 def handle_startendtag(self, tag, attrs):
151 # Self-closing tags like <br/>, <img/>
152 self.handle_starttag(tag, attrs)
153
154
155 def sanitize_html(html_content: str) -> str:
156 """Sanitize HTML using a proper parser with tag/attribute allowlists.
157
158 - Only tags in ALLOWED_TAGS are kept (all others stripped, text preserved)
159 - Only attributes in ALLOWED_ATTRS per tag are kept
160 - Event handlers (on*) are always stripped
161 - URLs in href/src are checked after HTML entity decoding — javascript:,
162 data:, vbscript: (including entity-encoded variants) are neutralized
163 - Content inside <script>, <style>, <iframe>, etc. is completely removed
164 - HTML comments are stripped
165 """
166 if not html_content:
167 return html_content
168
169 parser = _SanitizingParser()
170 parser.feed(html_content)
171 return parser.out.getvalue()
172
173 DDED core/url_validation.py
174 DDED fossil/templatetags/__init__.py
175 DDED fossil/templatetags/fossil_filters.py
--- a/core/url_validation.py
+++ b/core/url_validation.py
@@ -0,0 +1,59 @@
1
+"""URL validation for outbound requests (webhooks, etc.)."""
2
+
3
+import ipaddress
4
+import socket
5
+from urllib.parse import urlparse
6
+
7
+
8
+def is_safe_webhook_url(url: str) -> tuple[bool, str]:
9
+ """Validate a webhook URL is safe for server-side requests.
10
+
11
+ Blocks:
12
+ - Non-HTTP(S) protocols
13
+ - Localhost and loopback addresses
14
+ - Private/internal IP ranges (10.x, 172.16-31.x, 192.168.x, etc.)
15
+ - Link-local addresses
16
+ - AWS metadata endpoint (169.254.169.254)
17
+
18
+ Returns (is_safe, error_message).
19
+ """
20
+ if not url:
21
+ return False, "URL is required."
22
+
23
+ parsed = urlparse(url)
24
+
25
+ if parsed.scheme not in ("http", "https"):
26
+ return False, "Only http:// and https:// URLs are allowed."
27
+
28
+ hostname = parsed.hostname
29
+ if not hostname:
30
+ return False, "URL must include a hostname."
31
+
32
+ # Block obvious localhost variants
33
+ if hostname in ("localhost", "127.0.0.1", "::1", "0.0.0.0"):
34
+ return False, "Localhost URLs are not allowed."
35
+
36
+ # Resolve hostname and check the IP
37
+ try:
38
+ addr_info = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
39
+ except socket.gaierror:
40
+ return False, f"Could not resolve hostname: {hostname}"
41
+
42
+ for _family, _type, _proto, _canonname, sockaddr in addr_info:
43
+ ip_str = sockaddr[0]
44
+ try:
45
+ ip = ipaddress.ip_address(ip_str)
46
+ except ValueError:
47
+ continue
48
+
49
+ if ip.is_loopback:
50
+ return False, "Loopback addresses are not allowed."
51
+ if ip.is_private:
52
+ return False, "Private/internal IP addresses are not allowed."
53
+ if ip.is_link_local:
54
+ return False, "Link-local addresses are not allowed."
55
+ if ip.is_reserved:
56
+ return False, "Reserved IP addresses are not allowed."
57
+ # AWS metadata endpoint
58
+ if ip_str == "169.254.169.254":
59
+ return False, "Cloud metadata endpoints are not allo
--- a/core/url_validation.py
+++ b/core/url_validation.py
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/core/url_validation.py
+++ b/core/url_validation.py
@@ -0,0 +1,59 @@
1 """URL validation for outbound requests (webhooks, etc.)."""
2
3 import ipaddress
4 import socket
5 from urllib.parse import urlparse
6
7
8 def is_safe_webhook_url(url: str) -> tuple[bool, str]:
9 """Validate a webhook URL is safe for server-side requests.
10
11 Blocks:
12 - Non-HTTP(S) protocols
13 - Localhost and loopback addresses
14 - Private/internal IP ranges (10.x, 172.16-31.x, 192.168.x, etc.)
15 - Link-local addresses
16 - AWS metadata endpoint (169.254.169.254)
17
18 Returns (is_safe, error_message).
19 """
20 if not url:
21 return False, "URL is required."
22
23 parsed = urlparse(url)
24
25 if parsed.scheme not in ("http", "https"):
26 return False, "Only http:// and https:// URLs are allowed."
27
28 hostname = parsed.hostname
29 if not hostname:
30 return False, "URL must include a hostname."
31
32 # Block obvious localhost variants
33 if hostname in ("localhost", "127.0.0.1", "::1", "0.0.0.0"):
34 return False, "Localhost URLs are not allowed."
35
36 # Resolve hostname and check the IP
37 try:
38 addr_info = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
39 except socket.gaierror:
40 return False, f"Could not resolve hostname: {hostname}"
41
42 for _family, _type, _proto, _canonname, sockaddr in addr_info:
43 ip_str = sockaddr[0]
44 try:
45 ip = ipaddress.ip_address(ip_str)
46 except ValueError:
47 continue
48
49 if ip.is_loopback:
50 return False, "Loopback addresses are not allowed."
51 if ip.is_private:
52 return False, "Private/internal IP addresses are not allowed."
53 if ip.is_link_local:
54 return False, "Link-local addresses are not allowed."
55 if ip.is_reserved:
56 return False, "Reserved IP addresses are not allowed."
57 # AWS metadata endpoint
58 if ip_str == "169.254.169.254":
59 return False, "Cloud metadata endpoints are not allo

No diff available

--- a/fossil/templatetags/fossil_filters.py
+++ b/fossil/templatetags/fossil_filters.py
@@ -0,0 +1,17 @@
1
+from django import template
2
+
3
+register = template.Library()
4
+
5
+
6
+@register.filter
7
+def display_user(value):
8
+ """Convert email-style Fossil usernames to display names.
9
+
10
+ [email protected] -> lmata
11
+ ragelink -> ragelink
12
+ """
13
+ if not value:
14
+ return ""
15
+ if "@" in str(value):
16
+ return str(value).split("@")[0]
17
+ return str(value)
--- a/fossil/templatetags/fossil_filters.py
+++ b/fossil/templatetags/fossil_filters.py
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/fossil/templatetags/fossil_filters.py
+++ b/fossil/templatetags/fossil_filters.py
@@ -0,0 +1,17 @@
1 from django import template
2
3 register = template.Library()
4
5
6 @register.filter
7 def display_user(value):
8 """Convert email-style Fossil usernames to display names.
9
10 [email protected] -> lmata
11 ragelink -> ragelink
12 """
13 if not value:
14 return ""
15 if "@" in str(value):
16 return str(value).split("@")[0]
17 return str(value)
+44 -28
--- fossil/views.py
+++ fossil/views.py
@@ -1098,21 +1098,27 @@
10981098
secret = request.POST.get("secret", "").strip()
10991099
events = request.POST.getlist("events")
11001100
is_active = request.POST.get("is_active") == "on"
11011101
11021102
if url:
1103
- events_str = ",".join(events) if events else "all"
1104
- Webhook.objects.create(
1105
- repository=fossil_repo,
1106
- url=url,
1107
- secret=secret,
1108
- events=events_str,
1109
- is_active=is_active,
1110
- created_by=request.user,
1111
- )
1112
- messages.success(request, f"Webhook for {url} created.")
1113
- return redirect("fossil:webhooks", slug=slug)
1103
+ from core.url_validation import is_safe_webhook_url
1104
+
1105
+ is_safe, url_error = is_safe_webhook_url(url)
1106
+ if not is_safe:
1107
+ messages.error(request, f"Invalid webhook URL: {url_error}")
1108
+ else:
1109
+ events_str = ",".join(events) if events else "all"
1110
+ Webhook.objects.create(
1111
+ repository=fossil_repo,
1112
+ url=url,
1113
+ secret=secret,
1114
+ events=events_str,
1115
+ is_active=is_active,
1116
+ created_by=request.user,
1117
+ )
1118
+ messages.success(request, "Webhook created.")
1119
+ return redirect("fossil:webhooks", slug=slug)
11141120
11151121
return render(
11161122
request,
11171123
"fossil/webhook_form.html",
11181124
{
@@ -1142,20 +1148,25 @@
11421148
secret = request.POST.get("secret", "").strip()
11431149
events = request.POST.getlist("events")
11441150
is_active = request.POST.get("is_active") == "on"
11451151
11461152
if url:
1147
- webhook.url = url
1148
- # Only update secret if a new one was provided (don't blank it on edit)
1149
- if secret:
1150
- webhook.secret = secret
1151
- webhook.events = ",".join(events) if events else "all"
1152
- webhook.is_active = is_active
1153
- webhook.updated_by = request.user
1154
- webhook.save()
1155
- messages.success(request, f"Webhook for {webhook.url} updated.")
1156
- return redirect("fossil:webhooks", slug=slug)
1153
+ from core.url_validation import is_safe_webhook_url
1154
+
1155
+ is_safe, url_error = is_safe_webhook_url(url)
1156
+ if not is_safe:
1157
+ messages.error(request, f"Invalid webhook URL: {url_error}")
1158
+ else:
1159
+ webhook.url = url
1160
+ if secret:
1161
+ webhook.secret = secret
1162
+ webhook.events = ",".join(events) if events else "all"
1163
+ webhook.is_active = is_active
1164
+ webhook.updated_by = request.user
1165
+ webhook.save()
1166
+ messages.success(request, "Webhook updated.")
1167
+ return redirect("fossil:webhooks", slug=slug)
11571168
11581169
return render(
11591170
request,
11601171
"fossil/webhook_form.html",
11611172
{
@@ -1832,24 +1843,29 @@
18321843
if request.method == "GET":
18331844
if not can_read_project(request.user, project):
18341845
from django.core.exceptions import PermissionDenied
18351846
18361847
raise PermissionDenied
1848
+ import html as html_mod
1849
+
18371850
clone_url = request.build_absolute_uri()
18381851
is_public = project.visibility == "public"
18391852
auth_note = "" if is_public else "<p>Authentication is required.</p>"
1840
- html = (
1841
- f"<html><head><title>{project.name} — Fossil Sync</title></head>"
1853
+ safe_name = html_mod.escape(project.name)
1854
+ safe_slug = html_mod.escape(project.slug)
1855
+ safe_url = html_mod.escape(clone_url)
1856
+ response_html = (
1857
+ f"<html><head><title>{safe_name} — Fossil Sync</title></head>"
18421858
f"<body>"
1843
- f"<h1>{project.name}</h1>"
1844
- f"<p>This is the Fossil sync endpoint for <strong>{project.name}</strong>.</p>"
1859
+ f"<h1>{safe_name}</h1>"
1860
+ f"<p>This is the Fossil sync endpoint for <strong>{safe_name}</strong>.</p>"
18451861
f"<p>Clone with:</p>"
1846
- f"<pre>fossil clone {clone_url} {project.slug}.fossil</pre>"
1862
+ f"<pre>fossil clone {safe_url} {safe_slug}.fossil</pre>"
18471863
f"{auth_note}"
18481864
f"</body></html>"
18491865
)
1850
- return HttpResponse(html)
1866
+ return HttpResponse(response_html)
18511867
18521868
if request.method == "POST":
18531869
if not fossil_repo.exists_on_disk:
18541870
raise Http404("Repository file not found on disk.")
18551871
@@ -2504,11 +2520,11 @@
25042520
"branches": branches,
25052521
"search": search,
25062522
"pagination": pagination,
25072523
"per_page": per_page,
25082524
"per_page_options": PER_PAGE_OPTIONS,
2509
- "active_tab": "code",
2525
+ "active_tab": "branches",
25102526
},
25112527
)
25122528
25132529
25142530
# --- Tags ---
25152531
--- fossil/views.py
+++ fossil/views.py
@@ -1098,21 +1098,27 @@
1098 secret = request.POST.get("secret", "").strip()
1099 events = request.POST.getlist("events")
1100 is_active = request.POST.get("is_active") == "on"
1101
1102 if url:
1103 events_str = ",".join(events) if events else "all"
1104 Webhook.objects.create(
1105 repository=fossil_repo,
1106 url=url,
1107 secret=secret,
1108 events=events_str,
1109 is_active=is_active,
1110 created_by=request.user,
1111 )
1112 messages.success(request, f"Webhook for {url} created.")
1113 return redirect("fossil:webhooks", slug=slug)
 
 
 
 
 
 
1114
1115 return render(
1116 request,
1117 "fossil/webhook_form.html",
1118 {
@@ -1142,20 +1148,25 @@
1142 secret = request.POST.get("secret", "").strip()
1143 events = request.POST.getlist("events")
1144 is_active = request.POST.get("is_active") == "on"
1145
1146 if url:
1147 webhook.url = url
1148 # Only update secret if a new one was provided (don't blank it on edit)
1149 if secret:
1150 webhook.secret = secret
1151 webhook.events = ",".join(events) if events else "all"
1152 webhook.is_active = is_active
1153 webhook.updated_by = request.user
1154 webhook.save()
1155 messages.success(request, f"Webhook for {webhook.url} updated.")
1156 return redirect("fossil:webhooks", slug=slug)
 
 
 
 
 
1157
1158 return render(
1159 request,
1160 "fossil/webhook_form.html",
1161 {
@@ -1832,24 +1843,29 @@
1832 if request.method == "GET":
1833 if not can_read_project(request.user, project):
1834 from django.core.exceptions import PermissionDenied
1835
1836 raise PermissionDenied
 
 
1837 clone_url = request.build_absolute_uri()
1838 is_public = project.visibility == "public"
1839 auth_note = "" if is_public else "<p>Authentication is required.</p>"
1840 html = (
1841 f"<html><head><title>{project.name} — Fossil Sync</title></head>"
 
 
 
1842 f"<body>"
1843 f"<h1>{project.name}</h1>"
1844 f"<p>This is the Fossil sync endpoint for <strong>{project.name}</strong>.</p>"
1845 f"<p>Clone with:</p>"
1846 f"<pre>fossil clone {clone_url} {project.slug}.fossil</pre>"
1847 f"{auth_note}"
1848 f"</body></html>"
1849 )
1850 return HttpResponse(html)
1851
1852 if request.method == "POST":
1853 if not fossil_repo.exists_on_disk:
1854 raise Http404("Repository file not found on disk.")
1855
@@ -2504,11 +2520,11 @@
2504 "branches": branches,
2505 "search": search,
2506 "pagination": pagination,
2507 "per_page": per_page,
2508 "per_page_options": PER_PAGE_OPTIONS,
2509 "active_tab": "code",
2510 },
2511 )
2512
2513
2514 # --- Tags ---
2515
--- fossil/views.py
+++ fossil/views.py
@@ -1098,21 +1098,27 @@
1098 secret = request.POST.get("secret", "").strip()
1099 events = request.POST.getlist("events")
1100 is_active = request.POST.get("is_active") == "on"
1101
1102 if url:
1103 from core.url_validation import is_safe_webhook_url
1104
1105 is_safe, url_error = is_safe_webhook_url(url)
1106 if not is_safe:
1107 messages.error(request, f"Invalid webhook URL: {url_error}")
1108 else:
1109 events_str = ",".join(events) if events else "all"
1110 Webhook.objects.create(
1111 repository=fossil_repo,
1112 url=url,
1113 secret=secret,
1114 events=events_str,
1115 is_active=is_active,
1116 created_by=request.user,
1117 )
1118 messages.success(request, "Webhook created.")
1119 return redirect("fossil:webhooks", slug=slug)
1120
1121 return render(
1122 request,
1123 "fossil/webhook_form.html",
1124 {
@@ -1142,20 +1148,25 @@
1148 secret = request.POST.get("secret", "").strip()
1149 events = request.POST.getlist("events")
1150 is_active = request.POST.get("is_active") == "on"
1151
1152 if url:
1153 from core.url_validation import is_safe_webhook_url
1154
1155 is_safe, url_error = is_safe_webhook_url(url)
1156 if not is_safe:
1157 messages.error(request, f"Invalid webhook URL: {url_error}")
1158 else:
1159 webhook.url = url
1160 if secret:
1161 webhook.secret = secret
1162 webhook.events = ",".join(events) if events else "all"
1163 webhook.is_active = is_active
1164 webhook.updated_by = request.user
1165 webhook.save()
1166 messages.success(request, "Webhook updated.")
1167 return redirect("fossil:webhooks", slug=slug)
1168
1169 return render(
1170 request,
1171 "fossil/webhook_form.html",
1172 {
@@ -1832,24 +1843,29 @@
1843 if request.method == "GET":
1844 if not can_read_project(request.user, project):
1845 from django.core.exceptions import PermissionDenied
1846
1847 raise PermissionDenied
1848 import html as html_mod
1849
1850 clone_url = request.build_absolute_uri()
1851 is_public = project.visibility == "public"
1852 auth_note = "" if is_public else "<p>Authentication is required.</p>"
1853 safe_name = html_mod.escape(project.name)
1854 safe_slug = html_mod.escape(project.slug)
1855 safe_url = html_mod.escape(clone_url)
1856 response_html = (
1857 f"<html><head><title>{safe_name} — Fossil Sync</title></head>"
1858 f"<body>"
1859 f"<h1>{safe_name}</h1>"
1860 f"<p>This is the Fossil sync endpoint for <strong>{safe_name}</strong>.</p>"
1861 f"<p>Clone with:</p>"
1862 f"<pre>fossil clone {safe_url} {safe_slug}.fossil</pre>"
1863 f"{auth_note}"
1864 f"</body></html>"
1865 )
1866 return HttpResponse(response_html)
1867
1868 if request.method == "POST":
1869 if not fossil_repo.exists_on_disk:
1870 raise Http404("Repository file not found on disk.")
1871
@@ -2504,11 +2520,11 @@
2520 "branches": branches,
2521 "search": search,
2522 "pagination": pagination,
2523 "per_page": per_page,
2524 "per_page_options": PER_PAGE_OPTIONS,
2525 "active_tab": "branches",
2526 },
2527 )
2528
2529
2530 # --- Tags ---
2531
--- templates/fossil/branch_list.html
+++ templates/fossil/branch_list.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Branches — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -44,11 +45,11 @@
4445
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=branch.last_uuid %}" class="text-gray-300 hover:text-brand-light truncate block max-w-sm">
4546
<code class="text-xs font-mono text-brand-light">{{ branch.last_uuid|truncatechars:10 }}</code>
4647
</a>
4748
</td>
4849
<td class="px-4 py-3">
49
- <a href="{% url 'fossil:user_activity' slug=project.slug username=branch.last_user %}" class="text-sm text-gray-400 hover:text-brand-light">{{ branch.last_user }}</a>
50
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=branch.last_user %}" class="text-sm text-gray-400 hover:text-brand-light">{{ branch.last_user|display_user }}</a>
5051
</td>
5152
<td class="px-4 py-3 text-sm text-gray-400 text-right">{{ branch.checkin_count }}</td>
5253
<td class="px-4 py-3 text-sm text-gray-500 text-right">{{ branch.last_checkin|timesince }} ago</td>
5354
</tr>
5455
{% empty %}
5556
--- templates/fossil/branch_list.html
+++ templates/fossil/branch_list.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Branches — {{ 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" %}
@@ -44,11 +45,11 @@
44 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=branch.last_uuid %}" class="text-gray-300 hover:text-brand-light truncate block max-w-sm">
45 <code class="text-xs font-mono text-brand-light">{{ branch.last_uuid|truncatechars:10 }}</code>
46 </a>
47 </td>
48 <td class="px-4 py-3">
49 <a href="{% url 'fossil:user_activity' slug=project.slug username=branch.last_user %}" class="text-sm text-gray-400 hover:text-brand-light">{{ branch.last_user }}</a>
50 </td>
51 <td class="px-4 py-3 text-sm text-gray-400 text-right">{{ branch.checkin_count }}</td>
52 <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ branch.last_checkin|timesince }} ago</td>
53 </tr>
54 {% empty %}
55
--- templates/fossil/branch_list.html
+++ templates/fossil/branch_list.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Branches — {{ 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" %}
@@ -44,11 +45,11 @@
45 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=branch.last_uuid %}" class="text-gray-300 hover:text-brand-light truncate block max-w-sm">
46 <code class="text-xs font-mono text-brand-light">{{ branch.last_uuid|truncatechars:10 }}</code>
47 </a>
48 </td>
49 <td class="px-4 py-3">
50 <a href="{% url 'fossil:user_activity' slug=project.slug username=branch.last_user %}" class="text-sm text-gray-400 hover:text-brand-light">{{ branch.last_user|display_user }}</a>
51 </td>
52 <td class="px-4 py-3 text-sm text-gray-400 text-right">{{ branch.checkin_count }}</td>
53 <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ branch.last_checkin|timesince }} ago</td>
54 </tr>
55 {% empty %}
56
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}{{ checkin.uuid|truncatechars:12 }} — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block extra_head %}
56
<style>
67
.diff-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
@@ -57,11 +58,11 @@
5758
<!-- Commit header -->
5859
<div class="rounded-lg bg-gray-800 border border-gray-700">
5960
<div class="px-6 py-5">
6061
<p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p>
6162
<div class="mt-3 flex items-center gap-4 flex-wrap text-sm">
62
- <a href="{% url 'fossil:user_activity' slug=project.slug username=checkin.user %}" class="font-medium text-gray-200 hover:text-brand-light">{{ checkin.user }}</a>
63
+ <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>
6364
<span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }}</span>
6465
{% if checkin.branch %}
6566
<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">
6667
{{ checkin.branch }}
6768
</span>
6869
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -1,6 +1,7 @@
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; }
@@ -57,11 +58,11 @@
57 <!-- Commit header -->
58 <div class="rounded-lg bg-gray-800 border border-gray-700">
59 <div class="px-6 py-5">
60 <p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p>
61 <div class="mt-3 flex items-center gap-4 flex-wrap text-sm">
62 <a href="{% url 'fossil:user_activity' slug=project.slug username=checkin.user %}" class="font-medium text-gray-200 hover:text-brand-light">{{ checkin.user }}</a>
63 <span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }}</span>
64 {% if checkin.branch %}
65 <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">
66 {{ checkin.branch }}
67 </span>
68
--- templates/fossil/checkin_detail.html
+++ templates/fossil/checkin_detail.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}{{ checkin.uuid|truncatechars:12 }} — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block extra_head %}
6 <style>
7 .diff-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
@@ -57,11 +58,11 @@
58 <!-- Commit header -->
59 <div class="rounded-lg bg-gray-800 border border-gray-700">
60 <div class="px-6 py-5">
61 <p class="text-lg text-gray-100 leading-relaxed">{{ checkin.comment }}</p>
62 <div class="mt-3 flex items-center gap-4 flex-wrap text-sm">
63 <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>
64 <span class="text-gray-500">{{ checkin.timestamp|date:"Y-m-d H:i" }}</span>
65 {% if checkin.branch %}
66 <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">
67 {{ checkin.branch }}
68 </span>
69
--- templates/fossil/code_blame.html
+++ templates/fossil/code_blame.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Blame: {{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block extra_head %}
56
<style>
67
.blame-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
@@ -42,15 +43,15 @@
4243
{% if blame_lines %}
4344
<table class="blame-table">
4445
<tbody>
4546
{% for bl in blame_lines %}
4647
<tr class="blame-row">
47
- <td class="blame-meta" style="color: {{ bl.age_color }}; background: {{ bl.age_bg }};" title="{{ bl.date }} by {{ bl.user }}">
48
+ <td class="blame-meta" style="color: {{ bl.age_color }}; background: {{ bl.age_bg }};" title="{{ bl.date }} by {{ bl.user|display_user }}">
4849
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=bl.uuid %}" style="color: inherit;">{{ bl.uuid|truncatechars:8 }}</a>
4950
</td>
50
- <td class="blame-meta" style="color: {{ bl.age_color }}; background: {{ bl.age_bg }};" title="{{ bl.date }} by {{ bl.user }}">
51
- <a href="{% url 'fossil:user_activity' slug=project.slug username=bl.user %}" style="color: inherit;">{{ bl.user }}</a>
51
+ <td class="blame-meta" style="color: {{ bl.age_color }}; background: {{ bl.age_bg }};" title="{{ bl.date }} by {{ bl.user|display_user }}">
52
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=bl.user %}" style="color: inherit;">{{ bl.user|display_user }}</a>
5253
</td>
5354
<td class="blame-num">{{ forloop.counter }}</td>
5455
<td class="blame-code">{{ bl.text }}</td>
5556
</tr>
5657
{% endfor %}
5758
--- templates/fossil/code_blame.html
+++ templates/fossil/code_blame.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Blame: {{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block extra_head %}
5 <style>
6 .blame-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
@@ -42,15 +43,15 @@
42 {% if blame_lines %}
43 <table class="blame-table">
44 <tbody>
45 {% for bl in blame_lines %}
46 <tr class="blame-row">
47 <td class="blame-meta" style="color: {{ bl.age_color }}; background: {{ bl.age_bg }};" title="{{ bl.date }} by {{ bl.user }}">
48 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=bl.uuid %}" style="color: inherit;">{{ bl.uuid|truncatechars:8 }}</a>
49 </td>
50 <td class="blame-meta" style="color: {{ bl.age_color }}; background: {{ bl.age_bg }};" title="{{ bl.date }} by {{ bl.user }}">
51 <a href="{% url 'fossil:user_activity' slug=project.slug username=bl.user %}" style="color: inherit;">{{ bl.user }}</a>
52 </td>
53 <td class="blame-num">{{ forloop.counter }}</td>
54 <td class="blame-code">{{ bl.text }}</td>
55 </tr>
56 {% endfor %}
57
--- templates/fossil/code_blame.html
+++ templates/fossil/code_blame.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Blame: {{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block extra_head %}
6 <style>
7 .blame-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
@@ -42,15 +43,15 @@
43 {% if blame_lines %}
44 <table class="blame-table">
45 <tbody>
46 {% for bl in blame_lines %}
47 <tr class="blame-row">
48 <td class="blame-meta" style="color: {{ bl.age_color }}; background: {{ bl.age_bg }};" title="{{ bl.date }} by {{ bl.user|display_user }}">
49 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=bl.uuid %}" style="color: inherit;">{{ bl.uuid|truncatechars:8 }}</a>
50 </td>
51 <td class="blame-meta" style="color: {{ bl.age_color }}; background: {{ bl.age_bg }};" title="{{ bl.date }} by {{ bl.user|display_user }}">
52 <a href="{% url 'fossil:user_activity' slug=project.slug username=bl.user %}" style="color: inherit;">{{ bl.user|display_user }}</a>
53 </td>
54 <td class="blame-num">{{ forloop.counter }}</td>
55 <td class="blame-code">{{ bl.text }}</td>
56 </tr>
57 {% endfor %}
58
--- templates/fossil/code_browser.html
+++ templates/fossil/code_browser.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% load humanize %}
34
{% block title %}{% if current_dir %}{{ current_dir }} — {% endif %}Code — {{ project.name }} — Fossilrepo{% endblock %}
45
56
{% block content %}
67
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
@@ -33,11 +34,11 @@
3334
</div>
3435
3536
<!-- Latest commit info -->
3637
{% if latest_commit %}
3738
<div class="flex items-center gap-3 mt-2 text-xs text-gray-500">
38
- <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 }}</a>
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>
3940
<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>
4041
<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>
4142
<span>{{ latest_commit.timestamp|date:"Y-m-d H:i" }}</span>
4243
</div>
4344
{% endif %}
4445
--- templates/fossil/code_browser.html
+++ templates/fossil/code_browser.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% load humanize %}
3 {% block title %}{% if current_dir %}{{ current_dir }} — {% endif %}Code — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block content %}
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
@@ -33,11 +34,11 @@
33 </div>
34
35 <!-- Latest commit info -->
36 {% if latest_commit %}
37 <div class="flex items-center gap-3 mt-2 text-xs text-gray-500">
38 <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 }}</a>
39 <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>
40 <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>
41 <span>{{ latest_commit.timestamp|date:"Y-m-d H:i" }}</span>
42 </div>
43 {% endif %}
44
--- templates/fossil/code_browser.html
+++ templates/fossil/code_browser.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% load humanize %}
4 {% block title %}{% if current_dir %}{{ current_dir }} — {% endif %}Code — {{ project.name }} — Fossilrepo{% endblock %}
5
6 {% block content %}
7 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
@@ -33,11 +34,11 @@
34 </div>
35
36 <!-- Latest commit info -->
37 {% if latest_commit %}
38 <div class="flex items-center gap-3 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
--- templates/fossil/compare.html
+++ templates/fossil/compare.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Compare — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block extra_head %}
56
<style>
67
.diff-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
@@ -54,16 +55,16 @@
5455
{% if from_detail and to_detail %}
5556
<div class="grid grid-cols-2 gap-4 mb-6">
5657
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
5758
<div class="text-xs text-gray-500 mb-1">From</div>
5859
<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>
59
- <div class="mt-1 text-xs text-gray-500">{{ from_detail.user }} &middot; {{ from_detail.timestamp|date:"Y-m-d H:i" }}</div>
60
+ <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>
6061
</div>
6162
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
6263
<div class="text-xs text-gray-500 mb-1">To</div>
6364
<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>
64
- <div class="mt-1 text-xs text-gray-500">{{ to_detail.user }} &middot; {{ to_detail.timestamp|date:"Y-m-d H:i" }}</div>
65
+ <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>
6566
</div>
6667
</div>
6768
6869
{% if file_diffs %}
6970
<div x-data="{ mode: localStorage.getItem('diff-mode') || 'unified' }" x-init="$watch('mode', val => localStorage.setItem('diff-mode', val))" x-ref="diffToggle">
7071
--- templates/fossil/compare.html
+++ templates/fossil/compare.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Compare — {{ 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; }
@@ -54,16 +55,16 @@
54 {% if from_detail and to_detail %}
55 <div class="grid grid-cols-2 gap-4 mb-6">
56 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
57 <div class="text-xs text-gray-500 mb-1">From</div>
58 <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>
59 <div class="mt-1 text-xs text-gray-500">{{ from_detail.user }} &middot; {{ from_detail.timestamp|date:"Y-m-d H:i" }}</div>
60 </div>
61 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
62 <div class="text-xs text-gray-500 mb-1">To</div>
63 <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>
64 <div class="mt-1 text-xs text-gray-500">{{ to_detail.user }} &middot; {{ to_detail.timestamp|date:"Y-m-d H:i" }}</div>
65 </div>
66 </div>
67
68 {% if file_diffs %}
69 <div x-data="{ mode: localStorage.getItem('diff-mode') || 'unified' }" x-init="$watch('mode', val => localStorage.setItem('diff-mode', val))" x-ref="diffToggle">
70
--- templates/fossil/compare.html
+++ templates/fossil/compare.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Compare — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block extra_head %}
6 <style>
7 .diff-table { border-collapse: collapse; width: 100%; font-size: 0.75rem; font-family: ui-monospace, monospace; }
@@ -54,16 +55,16 @@
55 {% if from_detail and to_detail %}
56 <div class="grid grid-cols-2 gap-4 mb-6">
57 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
58 <div class="text-xs text-gray-500 mb-1">From</div>
59 <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>
60 <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>
61 </div>
62 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
63 <div class="text-xs text-gray-500 mb-1">To</div>
64 <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>
65 <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>
66 </div>
67 </div>
68
69 {% if file_diffs %}
70 <div x-data="{ mode: localStorage.getItem('diff-mode') || 'unified' }" x-init="$watch('mode', val => localStorage.setItem('diff-mode', val))" x-ref="diffToggle">
71
--- templates/fossil/file_history.html
+++ templates/fossil/file_history.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}History: {{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -21,11 +22,11 @@
2122
</div>
2223
<div class="flex-1 min-w-0">
2324
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
2425
class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:100 }}</a>
2526
<div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
26
- <a href="{% url 'fossil:user_activity' slug=project.slug username=commit.user %}" class="hover:text-gray-300">{{ commit.user }}</a>
27
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=commit.user %}" class="hover:text-gray-300">{{ commit.user|display_user }}</a>
2728
<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>
2829
<span>{{ commit.timestamp|date:"Y-m-d H:i" }}</span>
2930
</div>
3031
</div>
3132
</div>
3233
--- templates/fossil/file_history.html
+++ templates/fossil/file_history.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}History: {{ filepath }} — {{ 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" %}
@@ -21,11 +22,11 @@
21 </div>
22 <div class="flex-1 min-w-0">
23 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
24 class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:100 }}</a>
25 <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
26 <a href="{% url 'fossil:user_activity' slug=project.slug username=commit.user %}" class="hover:text-gray-300">{{ commit.user }}</a>
27 <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>
28 <span>{{ commit.timestamp|date:"Y-m-d H:i" }}</span>
29 </div>
30 </div>
31 </div>
32
--- templates/fossil/file_history.html
+++ templates/fossil/file_history.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}History: {{ filepath }} — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block content %}
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7 {% include "fossil/_project_nav.html" %}
@@ -21,11 +22,11 @@
22 </div>
23 <div class="flex-1 min-w-0">
24 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
25 class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:100 }}</a>
26 <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
27 <a href="{% url 'fossil:user_activity' slug=project.slug username=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
--- templates/fossil/forum_list.html
+++ templates/fossil/forum_list.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Forum — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -46,13 +47,13 @@
4647
{% if post.body %}
4748
<p class="mt-1 text-sm text-gray-400 line-clamp-2">{{ post.body|truncatechars:200 }}</p>
4849
{% endif %}
4950
<div class="mt-2 flex items-center gap-3 text-xs text-gray-500">
5051
{% if post.source == "fossil" %}
51
- <a href="{% url 'fossil:user_activity' slug=project.slug username=post.user %}" class="font-medium text-gray-400 hover:text-brand-light">{{ post.user }}</a>
52
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=post.user %}" class="font-medium text-gray-400 hover:text-brand-light">{{ post.user|display_user }}</a>
5253
{% else %}
53
- <span class="font-medium text-gray-400">{{ post.user }}</span>
54
+ <span class="font-medium text-gray-400">{{ post.user|display_user }}</span>
5455
{% endif %}
5556
<span>{{ post.timestamp|timesince }} ago</span>
5657
{% if post.source == "django" %}
5758
<span class="inline-flex rounded-full bg-brand/20 px-2 py-0.5 text-xs text-brand-light">local</span>
5859
{% endif %}
5960
--- templates/fossil/forum_list.html
+++ templates/fossil/forum_list.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Forum — {{ 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" %}
@@ -46,13 +47,13 @@
46 {% if post.body %}
47 <p class="mt-1 text-sm text-gray-400 line-clamp-2">{{ post.body|truncatechars:200 }}</p>
48 {% endif %}
49 <div class="mt-2 flex items-center gap-3 text-xs text-gray-500">
50 {% if post.source == "fossil" %}
51 <a href="{% url 'fossil:user_activity' slug=project.slug username=post.user %}" class="font-medium text-gray-400 hover:text-brand-light">{{ post.user }}</a>
52 {% else %}
53 <span class="font-medium text-gray-400">{{ post.user }}</span>
54 {% endif %}
55 <span>{{ post.timestamp|timesince }} ago</span>
56 {% if post.source == "django" %}
57 <span class="inline-flex rounded-full bg-brand/20 px-2 py-0.5 text-xs text-brand-light">local</span>
58 {% endif %}
59
--- templates/fossil/forum_list.html
+++ templates/fossil/forum_list.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Forum — {{ 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" %}
@@ -46,13 +47,13 @@
47 {% if post.body %}
48 <p class="mt-1 text-sm text-gray-400 line-clamp-2">{{ post.body|truncatechars:200 }}</p>
49 {% endif %}
50 <div class="mt-2 flex items-center gap-3 text-xs text-gray-500">
51 {% if post.source == "fossil" %}
52 <a href="{% url 'fossil:user_activity' slug=project.slug username=post.user %}" class="font-medium text-gray-400 hover:text-brand-light">{{ post.user|display_user }}</a>
53 {% else %}
54 <span class="font-medium text-gray-400">{{ post.user|display_user }}</span>
55 {% endif %}
56 <span>{{ post.timestamp|timesince }} ago</span>
57 {% if post.source == "django" %}
58 <span class="inline-flex rounded-full bg-brand/20 px-2 py-0.5 text-xs text-brand-light">local</span>
59 {% endif %}
60
--- templates/fossil/forum_thread.html
+++ templates/fossil/forum_thread.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Forum Thread — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -13,13 +14,13 @@
1314
{% for item in posts %}
1415
<div class="rounded-lg bg-gray-800 border border-gray-700 {% if item.post.in_reply_to %}ml-8{% endif %}">
1516
<div class="px-5 py-4">
1617
<div class="flex items-center justify-between mb-2">
1718
{% if is_django_thread %}
18
- <span class="text-sm font-medium text-gray-200">{{ item.post.user }}</span>
19
+ <span class="text-sm font-medium text-gray-200">{{ item.post.user|display_user }}</span>
1920
{% else %}
20
- <a href="{% url 'fossil:user_activity' slug=project.slug username=item.post.user %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ item.post.user }}</a>
21
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=item.post.user %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ item.post.user|display_user }}</a>
2122
{% endif %}
2223
<span class="text-xs text-gray-500">{{ item.post.timestamp|timesince }} ago</span>
2324
</div>
2425
{% if item.post.title and forloop.first %}
2526
<h2 class="text-lg font-semibold text-gray-100 mb-3">{{ item.post.title }}</h2>
2627
--- templates/fossil/forum_thread.html
+++ templates/fossil/forum_thread.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Forum Thread — {{ 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" %}
@@ -13,13 +14,13 @@
13 {% for item in posts %}
14 <div class="rounded-lg bg-gray-800 border border-gray-700 {% if item.post.in_reply_to %}ml-8{% endif %}">
15 <div class="px-5 py-4">
16 <div class="flex items-center justify-between mb-2">
17 {% if is_django_thread %}
18 <span class="text-sm font-medium text-gray-200">{{ item.post.user }}</span>
19 {% else %}
20 <a href="{% url 'fossil:user_activity' slug=project.slug username=item.post.user %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ item.post.user }}</a>
21 {% endif %}
22 <span class="text-xs text-gray-500">{{ item.post.timestamp|timesince }} ago</span>
23 </div>
24 {% if item.post.title and forloop.first %}
25 <h2 class="text-lg font-semibold text-gray-100 mb-3">{{ item.post.title }}</h2>
26
--- templates/fossil/forum_thread.html
+++ templates/fossil/forum_thread.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Forum Thread — {{ 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" %}
@@ -13,13 +14,13 @@
14 {% for item in posts %}
15 <div class="rounded-lg bg-gray-800 border border-gray-700 {% if item.post.in_reply_to %}ml-8{% endif %}">
16 <div class="px-5 py-4">
17 <div class="flex items-center justify-between mb-2">
18 {% if is_django_thread %}
19 <span class="text-sm font-medium text-gray-200">{{ item.post.user|display_user }}</span>
20 {% else %}
21 <a href="{% url 'fossil:user_activity' slug=project.slug username=item.post.user %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ item.post.user|display_user }}</a>
22 {% endif %}
23 <span class="text-xs text-gray-500">{{ item.post.timestamp|timesince }} ago</span>
24 </div>
25 {% if item.post.title and forloop.first %}
26 <h2 class="text-lg font-semibold text-gray-100 mb-3">{{ item.post.title }}</h2>
27
--- templates/fossil/partials/timeline_entries.html
+++ templates/fossil/partials/timeline_entries.html
@@ -105,11 +105,11 @@
105105
{# Meta: hash, user, branch #}
106106
<div class="tl-meta">
107107
{% if e.event_type == "ci" %}
108108
{% include "fossil/_copy_hash.html" with hash=e.uuid slug=project.slug %}
109109
{% endif %}
110
- <a href="{% url 'fossil:user_activity' slug=project.slug username=e.user %}" class="tl-user">{{ e.user }}</a>
110
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=e.user %}" class="tl-user">{{ e.user|display_user }}</a>
111111
{% if e.branch %}<span class="tl-branch">{{ e.branch }}</span>{% endif %}
112112
</div>
113113
</div>
114114
115115
{% endwith %}
116116
--- templates/fossil/partials/timeline_entries.html
+++ templates/fossil/partials/timeline_entries.html
@@ -105,11 +105,11 @@
105 {# Meta: hash, user, branch #}
106 <div class="tl-meta">
107 {% if e.event_type == "ci" %}
108 {% include "fossil/_copy_hash.html" with hash=e.uuid slug=project.slug %}
109 {% endif %}
110 <a href="{% url 'fossil:user_activity' slug=project.slug username=e.user %}" class="tl-user">{{ e.user }}</a>
111 {% if e.branch %}<span class="tl-branch">{{ e.branch }}</span>{% endif %}
112 </div>
113 </div>
114
115 {% endwith %}
116
--- templates/fossil/partials/timeline_entries.html
+++ templates/fossil/partials/timeline_entries.html
@@ -105,11 +105,11 @@
105 {# Meta: hash, user, branch #}
106 <div class="tl-meta">
107 {% if e.event_type == "ci" %}
108 {% include "fossil/_copy_hash.html" with hash=e.uuid slug=project.slug %}
109 {% endif %}
110 <a href="{% url 'fossil:user_activity' slug=project.slug username=e.user %}" class="tl-user">{{ e.user|display_user }}</a>
111 {% if e.branch %}<span class="tl-branch">{{ e.branch }}</span>{% endif %}
112 </div>
113 </div>
114
115 {% endwith %}
116
--- templates/fossil/repo_stats.html
+++ templates/fossil/repo_stats.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Statistics — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block extra_head %}
56
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
67
{% endblock %}
@@ -77,11 +78,11 @@
7778
<div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
7879
<h3 class="text-sm font-medium text-gray-300 mb-3">Top Contributors</h3>
7980
<div class="space-y-1">
8081
{% for c in top_contributors %}
8182
<a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-gray-700/50">
82
- <span class="text-sm text-gray-300">{{ c.user }}</span>
83
+ <span class="text-sm text-gray-300">{{ c.user|display_user }}</span>
8384
<div class="flex items-center gap-2">
8485
<div class="w-24 bg-gray-700 rounded-full h-1.5">
8586
<div class="bg-brand rounded-full h-1.5" style="width: {% widthratio c.count top_contributors.0.count 100 %}%"></div>
8687
</div>
8788
<span class="text-xs text-gray-500 w-16 text-right">{{ c.count }}</span>
8889
--- templates/fossil/repo_stats.html
+++ templates/fossil/repo_stats.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Statistics — {{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block extra_head %}
5 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
6 {% endblock %}
@@ -77,11 +78,11 @@
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">Top Contributors</h3>
79 <div class="space-y-1">
80 {% for c in top_contributors %}
81 <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-gray-700/50">
82 <span class="text-sm text-gray-300">{{ c.user }}</span>
83 <div class="flex items-center gap-2">
84 <div class="w-24 bg-gray-700 rounded-full h-1.5">
85 <div class="bg-brand rounded-full h-1.5" style="width: {% widthratio c.count top_contributors.0.count 100 %}%"></div>
86 </div>
87 <span class="text-xs text-gray-500 w-16 text-right">{{ c.count }}</span>
88
--- templates/fossil/repo_stats.html
+++ templates/fossil/repo_stats.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Statistics — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block extra_head %}
6 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
7 {% endblock %}
@@ -77,11 +78,11 @@
78 <div class="rounded-lg bg-gray-800 border border-gray-700 p-4">
79 <h3 class="text-sm font-medium text-gray-300 mb-3">Top Contributors</h3>
80 <div class="space-y-1">
81 {% for c in top_contributors %}
82 <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-gray-700/50">
83 <span class="text-sm text-gray-300">{{ c.user|display_user }}</span>
84 <div class="flex items-center gap-2">
85 <div class="w-24 bg-gray-700 rounded-full h-1.5">
86 <div class="bg-brand rounded-full h-1.5" style="width: {% widthratio c.count top_contributors.0.count 100 %}%"></div>
87 </div>
88 <span class="text-xs text-gray-500 w-16 text-right">{{ c.count }}</span>
89
--- templates/fossil/search.html
+++ templates/fossil/search.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Search — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -22,11 +23,11 @@
2223
<div class="rounded-lg bg-gray-800 border border-gray-700 divide-y divide-gray-700">
2324
{% for c in results.checkins %}
2425
<div class="px-4 py-3">
2526
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=c.uuid %}" class="text-sm text-gray-200 hover:text-brand-light">{{ c.comment|truncatechars:100 }}</a>
2627
<div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
27
- <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="hover:text-gray-300">{{ c.user }}</a>
28
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="hover:text-gray-300">{{ c.user|display_user }}</a>
2829
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=c.uuid %}" class="font-mono text-brand-light">{{ c.uuid|truncatechars:10 }}</a>
2930
<span>{{ c.timestamp|timesince }} ago</span>
3031
</div>
3132
</div>
3233
{% endfor %}
3334
--- templates/fossil/search.html
+++ templates/fossil/search.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Search — {{ 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" %}
@@ -22,11 +23,11 @@
22 <div class="rounded-lg bg-gray-800 border border-gray-700 divide-y divide-gray-700">
23 {% for c in results.checkins %}
24 <div class="px-4 py-3">
25 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=c.uuid %}" class="text-sm text-gray-200 hover:text-brand-light">{{ c.comment|truncatechars:100 }}</a>
26 <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
27 <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="hover:text-gray-300">{{ c.user }}</a>
28 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=c.uuid %}" class="font-mono text-brand-light">{{ c.uuid|truncatechars:10 }}</a>
29 <span>{{ c.timestamp|timesince }} ago</span>
30 </div>
31 </div>
32 {% endfor %}
33
--- templates/fossil/search.html
+++ templates/fossil/search.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Search — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block content %}
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7 {% include "fossil/_project_nav.html" %}
@@ -22,11 +23,11 @@
23 <div class="rounded-lg bg-gray-800 border border-gray-700 divide-y divide-gray-700">
24 {% for c in results.checkins %}
25 <div class="px-4 py-3">
26 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=c.uuid %}" class="text-sm text-gray-200 hover:text-brand-light">{{ c.comment|truncatechars:100 }}</a>
27 <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
28 <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="hover:text-gray-300">{{ c.user|display_user }}</a>
29 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=c.uuid %}" class="font-mono text-brand-light">{{ c.uuid|truncatechars:10 }}</a>
30 <span>{{ c.timestamp|timesince }} ago</span>
31 </div>
32 </div>
33 {% endfor %}
34
--- templates/fossil/tag_list.html
+++ templates/fossil/tag_list.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Tags — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -39,11 +40,11 @@
3940
</td>
4041
<td class="px-4 py-3">
4142
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=tag.uuid %}" class="font-mono text-xs text-brand-light hover:text-brand">{{ tag.uuid|truncatechars:10 }}</a>
4243
</td>
4344
<td class="px-4 py-3">
44
- <a href="{% url 'fossil:user_activity' slug=project.slug username=tag.user %}" class="text-sm text-gray-400 hover:text-brand-light">{{ tag.user }}</a>
45
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=tag.user %}" class="text-sm text-gray-400 hover:text-brand-light">{{ tag.user|display_user }}</a>
4546
</td>
4647
<td class="px-4 py-3 text-sm text-gray-500 text-right">{{ tag.timestamp|date:"Y-m-d" }}</td>
4748
</tr>
4849
{% empty %}
4950
<tr>
5051
--- templates/fossil/tag_list.html
+++ templates/fossil/tag_list.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Tags — {{ 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" %}
@@ -39,11 +40,11 @@
39 </td>
40 <td class="px-4 py-3">
41 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=tag.uuid %}" class="font-mono text-xs text-brand-light hover:text-brand">{{ tag.uuid|truncatechars:10 }}</a>
42 </td>
43 <td class="px-4 py-3">
44 <a href="{% url 'fossil:user_activity' slug=project.slug username=tag.user %}" class="text-sm text-gray-400 hover:text-brand-light">{{ tag.user }}</a>
45 </td>
46 <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ tag.timestamp|date:"Y-m-d" }}</td>
47 </tr>
48 {% empty %}
49 <tr>
50
--- templates/fossil/tag_list.html
+++ templates/fossil/tag_list.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Tags — {{ 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" %}
@@ -39,11 +40,11 @@
40 </td>
41 <td class="px-4 py-3">
42 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=tag.uuid %}" class="font-mono text-xs text-brand-light hover:text-brand">{{ tag.uuid|truncatechars:10 }}</a>
43 </td>
44 <td class="px-4 py-3">
45 <a href="{% url 'fossil:user_activity' slug=project.slug username=tag.user %}" class="text-sm text-gray-400 hover:text-brand-light">{{ tag.user|display_user }}</a>
46 </td>
47 <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ tag.timestamp|date:"Y-m-d" }}</td>
48 </tr>
49 {% empty %}
50 <tr>
51
--- templates/fossil/technote_detail.html
+++ templates/fossil/technote_detail.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}{{ note.comment|truncatechars:60 }} — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -15,11 +16,11 @@
1516
<div class="flex items-start justify-between gap-4">
1617
<div class="flex-1">
1718
<h2 class="text-xl font-bold text-gray-100 mb-1">{{ note.comment }}</h2>
1819
<div class="flex items-center gap-3 text-xs text-gray-500">
1920
<a href="{% url 'fossil:user_activity' slug=project.slug username=note.user %}"
20
- class="hover:text-brand-light">{{ note.user }}</a>
21
+ class="hover:text-brand-light">{{ note.user|display_user }}</a>
2122
<span class="font-mono text-brand-light">{{ note.uuid|truncatechars:16 }}</span>
2223
<span>{{ note.timestamp|date:"Y-m-d H:i" }}</span>
2324
</div>
2425
</div>
2526
{% if has_write %}
2627
--- templates/fossil/technote_detail.html
+++ templates/fossil/technote_detail.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}{{ note.comment|truncatechars:60 }} — {{ 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" %}
@@ -15,11 +16,11 @@
15 <div class="flex items-start justify-between gap-4">
16 <div class="flex-1">
17 <h2 class="text-xl font-bold text-gray-100 mb-1">{{ note.comment }}</h2>
18 <div class="flex items-center gap-3 text-xs text-gray-500">
19 <a href="{% url 'fossil:user_activity' slug=project.slug username=note.user %}"
20 class="hover:text-brand-light">{{ note.user }}</a>
21 <span class="font-mono text-brand-light">{{ note.uuid|truncatechars:16 }}</span>
22 <span>{{ note.timestamp|date:"Y-m-d H:i" }}</span>
23 </div>
24 </div>
25 {% if has_write %}
26
--- templates/fossil/technote_detail.html
+++ templates/fossil/technote_detail.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}{{ note.comment|truncatechars:60 }} — {{ 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" %}
@@ -15,11 +16,11 @@
16 <div class="flex items-start justify-between gap-4">
17 <div class="flex-1">
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
--- templates/fossil/technote_list.html
+++ templates/fossil/technote_list.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Technotes — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -35,11 +36,11 @@
3536
class="block rounded-lg bg-gray-800 border border-gray-700 px-5 py-4 hover:bg-gray-700/50 transition-colors">
3637
<div class="flex items-start justify-between gap-3">
3738
<div class="flex-1 min-w-0">
3839
<p class="text-sm text-gray-200">{{ note.comment|truncatechars:200 }}</p>
3940
<div class="mt-2 flex items-center gap-3 text-xs text-gray-500">
40
- <span class="hover:text-brand-light">{{ note.user }}</span>
41
+ <span class="hover:text-brand-light">{{ note.user|display_user }}</span>
4142
<span class="font-mono text-brand-light">{{ note.uuid|truncatechars:10 }}</span>
4243
<span>{{ note.timestamp|date:"Y-m-d H:i" }}</span>
4344
</div>
4445
</div>
4546
</div>
4647
--- templates/fossil/technote_list.html
+++ templates/fossil/technote_list.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Technotes — {{ 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" %}
@@ -35,11 +36,11 @@
35 class="block rounded-lg bg-gray-800 border border-gray-700 px-5 py-4 hover:bg-gray-700/50 transition-colors">
36 <div class="flex items-start justify-between gap-3">
37 <div class="flex-1 min-w-0">
38 <p class="text-sm text-gray-200">{{ note.comment|truncatechars:200 }}</p>
39 <div class="mt-2 flex items-center gap-3 text-xs text-gray-500">
40 <span class="hover:text-brand-light">{{ note.user }}</span>
41 <span class="font-mono text-brand-light">{{ note.uuid|truncatechars:10 }}</span>
42 <span>{{ note.timestamp|date:"Y-m-d H:i" }}</span>
43 </div>
44 </div>
45 </div>
46
--- templates/fossil/technote_list.html
+++ templates/fossil/technote_list.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Technotes — {{ 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" %}
@@ -35,11 +36,11 @@
36 class="block rounded-lg bg-gray-800 border border-gray-700 px-5 py-4 hover:bg-gray-700/50 transition-colors">
37 <div class="flex items-start justify-between gap-3">
38 <div class="flex-1 min-w-0">
39 <p class="text-sm text-gray-200">{{ note.comment|truncatechars:200 }}</p>
40 <div class="mt-2 flex items-center gap-3 text-xs text-gray-500">
41 <span class="hover:text-brand-light">{{ note.user|display_user }}</span>
42 <span class="font-mono text-brand-light">{{ note.uuid|truncatechars:10 }}</span>
43 <span>{{ note.timestamp|date:"Y-m-d H:i" }}</span>
44 </div>
45 </div>
46 </div>
47
--- templates/fossil/ticket_detail.html
+++ templates/fossil/ticket_detail.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}{{ ticket.title }} — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -79,11 +80,11 @@
7980
<h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-3">Comments ({{ comments|length }})</h3>
8081
<div class="space-y-3">
8182
{% for c in comments %}
8283
<div class="rounded-lg bg-gray-800 border border-gray-700">
8384
<div class="px-5 py-3 border-b border-gray-700 flex items-center justify-between">
84
- <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ c.user }}</a>
85
+ <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ c.user|display_user }}</a>
8586
<span class="text-xs text-gray-500">{{ c.timestamp|timesince }} ago</span>
8687
</div>
8788
<div class="px-5 py-4">
8889
<div class="prose prose-invert prose-gray prose-sm max-w-none">
8990
{{ c.html }}
9091
--- templates/fossil/ticket_detail.html
+++ templates/fossil/ticket_detail.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}{{ ticket.title }} — {{ 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" %}
@@ -79,11 +80,11 @@
79 <h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-3">Comments ({{ comments|length }})</h3>
80 <div class="space-y-3">
81 {% for c in comments %}
82 <div class="rounded-lg bg-gray-800 border border-gray-700">
83 <div class="px-5 py-3 border-b border-gray-700 flex items-center justify-between">
84 <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ c.user }}</a>
85 <span class="text-xs text-gray-500">{{ c.timestamp|timesince }} ago</span>
86 </div>
87 <div class="px-5 py-4">
88 <div class="prose prose-invert prose-gray prose-sm max-w-none">
89 {{ c.html }}
90
--- templates/fossil/ticket_detail.html
+++ templates/fossil/ticket_detail.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}{{ ticket.title }} — {{ 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" %}
@@ -79,11 +80,11 @@
80 <h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-3">Comments ({{ comments|length }})</h3>
81 <div class="space-y-3">
82 {% for c in comments %}
83 <div class="rounded-lg bg-gray-800 border border-gray-700">
84 <div class="px-5 py-3 border-b border-gray-700 flex items-center justify-between">
85 <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}" class="text-sm font-medium text-gray-200 hover:text-brand-light">{{ c.user|display_user }}</a>
86 <span class="text-xs text-gray-500">{{ c.timestamp|timesince }} ago</span>
87 </div>
88 <div class="px-5 py-4">
89 <div class="prose prose-invert prose-gray prose-sm max-w-none">
90 {{ c.html }}
91
--- templates/fossil/timeline.html
+++ templates/fossil/timeline.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}Timeline — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
{% include "fossil/_live_reload.html" %}
67
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
78
--- templates/fossil/timeline.html
+++ templates/fossil/timeline.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}Timeline — {{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block content %}
5 {% include "fossil/_live_reload.html" %}
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7
--- templates/fossil/timeline.html
+++ templates/fossil/timeline.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}Timeline — {{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block content %}
6 {% include "fossil/_live_reload.html" %}
7 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
8
--- templates/fossil/wiki_page.html
+++ templates/fossil/wiki_page.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}{{ page.name }} — Wiki — {{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block content %}
56
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
67
{% include "fossil/_project_nav.html" %}
@@ -10,11 +11,11 @@
1011
<div class="flex-1 min-w-0">
1112
<div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
1213
<div class="px-6 py-4 border-b border-gray-700 flex items-center justify-between">
1314
<h2 class="text-lg font-semibold text-gray-100">{{ page.name }}</h2>
1415
<div class="flex items-center gap-3">
15
- <span class="text-xs text-gray-500">{{ page.last_modified|timesince }} ago by {{ page.user }}</span>
16
+ <span class="text-xs text-gray-500">{{ page.last_modified|timesince }} ago by {{ page.user|display_user }}</span>
1617
{% if perms.projects.change_project %}
1718
<a href="{% url 'fossil:wiki_edit' slug=project.slug page_name=page.name %}" class="rounded-md bg-gray-700 px-2 py-1 text-xs font-semibold text-gray-300 hover:bg-gray-600">Edit</a>
1819
{% endif %}
1920
</div>
2021
</div>
2122
--- templates/fossil/wiki_page.html
+++ templates/fossil/wiki_page.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}{{ page.name }} — Wiki — {{ 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" %}
@@ -10,11 +11,11 @@
10 <div class="flex-1 min-w-0">
11 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
12 <div class="px-6 py-4 border-b border-gray-700 flex items-center justify-between">
13 <h2 class="text-lg font-semibold text-gray-100">{{ page.name }}</h2>
14 <div class="flex items-center gap-3">
15 <span class="text-xs text-gray-500">{{ page.last_modified|timesince }} ago by {{ page.user }}</span>
16 {% if perms.projects.change_project %}
17 <a href="{% url 'fossil:wiki_edit' slug=project.slug page_name=page.name %}" class="rounded-md bg-gray-700 px-2 py-1 text-xs font-semibold text-gray-300 hover:bg-gray-600">Edit</a>
18 {% endif %}
19 </div>
20 </div>
21
--- templates/fossil/wiki_page.html
+++ templates/fossil/wiki_page.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}{{ page.name }} — Wiki — {{ 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" %}
@@ -10,11 +11,11 @@
11 <div class="flex-1 min-w-0">
12 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
13 <div class="px-6 py-4 border-b border-gray-700 flex items-center justify-between">
14 <h2 class="text-lg font-semibold text-gray-100">{{ page.name }}</h2>
15 <div class="flex items-center gap-3">
16 <span class="text-xs text-gray-500">{{ page.last_modified|timesince }} ago by {{ page.user|display_user }}</span>
17 {% if perms.projects.change_project %}
18 <a href="{% url 'fossil:wiki_edit' slug=project.slug page_name=page.name %}" class="rounded-md bg-gray-700 px-2 py-1 text-xs font-semibold text-gray-300 hover:bg-gray-600">Edit</a>
19 {% endif %}
20 </div>
21 </div>
22
--- templates/projects/project_detail.html
+++ templates/projects/project_detail.html
@@ -1,6 +1,7 @@
11
{% extends "base.html" %}
2
+{% load fossil_filters %}
23
{% block title %}{{ project.name }} — Fossilrepo{% endblock %}
34
45
{% block extra_head %}
56
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
67
{% endblock %}
@@ -72,11 +73,11 @@
7273
</div>
7374
<div class="flex-1 min-w-0">
7475
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
7576
class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:80 }}</a>
7677
<div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
77
- <span>{{ commit.user }}</span>
78
+ <span>{{ commit.user|display_user }}</span>
7879
<a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
7980
class="font-mono text-brand-light hover:text-brand">{{ commit.uuid|truncatechars:10 }}</a>
8081
<span>{{ commit.timestamp|timesince }} ago</span>
8182
</div>
8283
</div>
@@ -159,11 +160,11 @@
159160
<h3 class="text-sm font-medium text-gray-300 mb-3">Top Contributors</h3>
160161
<div class="space-y-1">
161162
{% for c in top_contributors %}
162163
<a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}"
163164
class="flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-gray-700/50">
164
- <span class="text-sm text-gray-300">{{ c.user }}</span>
165
+ <span class="text-sm text-gray-300">{{ c.user|display_user }}</span>
165166
<span class="text-xs text-gray-500">{{ c.count }} commits</span>
166167
</a>
167168
{% endfor %}
168169
</div>
169170
</div>
170171
--- templates/projects/project_detail.html
+++ templates/projects/project_detail.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
 
2 {% block title %}{{ project.name }} — Fossilrepo{% endblock %}
3
4 {% block extra_head %}
5 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
6 {% endblock %}
@@ -72,11 +73,11 @@
72 </div>
73 <div class="flex-1 min-w-0">
74 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
75 class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:80 }}</a>
76 <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
77 <span>{{ commit.user }}</span>
78 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
79 class="font-mono text-brand-light hover:text-brand">{{ commit.uuid|truncatechars:10 }}</a>
80 <span>{{ commit.timestamp|timesince }} ago</span>
81 </div>
82 </div>
@@ -159,11 +160,11 @@
159 <h3 class="text-sm font-medium text-gray-300 mb-3">Top Contributors</h3>
160 <div class="space-y-1">
161 {% for c in top_contributors %}
162 <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}"
163 class="flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-gray-700/50">
164 <span class="text-sm text-gray-300">{{ c.user }}</span>
165 <span class="text-xs text-gray-500">{{ c.count }} commits</span>
166 </a>
167 {% endfor %}
168 </div>
169 </div>
170
--- templates/projects/project_detail.html
+++ templates/projects/project_detail.html
@@ -1,6 +1,7 @@
1 {% extends "base.html" %}
2 {% load fossil_filters %}
3 {% block title %}{{ project.name }} — Fossilrepo{% endblock %}
4
5 {% block extra_head %}
6 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
7 {% endblock %}
@@ -72,11 +73,11 @@
73 </div>
74 <div class="flex-1 min-w-0">
75 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
76 class="text-sm text-gray-200 hover:text-brand-light">{{ commit.comment|truncatechars:80 }}</a>
77 <div class="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
78 <span>{{ commit.user|display_user }}</span>
79 <a href="{% url 'fossil:checkin_detail' slug=project.slug checkin_uuid=commit.uuid %}"
80 class="font-mono text-brand-light hover:text-brand">{{ commit.uuid|truncatechars:10 }}</a>
81 <span>{{ commit.timestamp|timesince }} ago</span>
82 </div>
83 </div>
@@ -159,11 +160,11 @@
160 <h3 class="text-sm font-medium text-gray-300 mb-3">Top Contributors</h3>
161 <div class="space-y-1">
162 {% for c in top_contributors %}
163 <a href="{% url 'fossil:user_activity' slug=project.slug username=c.user %}"
164 class="flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-gray-700/50">
165 <span class="text-sm text-gray-300">{{ c.user|display_user }}</span>
166 <span class="text-xs text-gray-500">{{ c.count }} commits</span>
167 </a>
168 {% endfor %}
169 </div>
170 </div>
171

Keyboard Shortcuts

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