aboutsummaryrefslogtreecommitdiff
path: root/forged
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-04-06 09:33:11 +0800
committerRunxi Yu <me@runxiyu.org>2025-04-06 09:34:06 +0800
commitc9b4eee4c589b8b40c02d0c96f887ec991580a24 (patch)
tree0ab3f2a22e9c4ca430573559d5712a4a7a6903a3 /forged
parentMove the Go stuff to ./forged/ (diff)
downloadforge-c9b4eee4c589b8b40c02d0c96f887ec991580a24.tar.gz
forge-c9b4eee4c589b8b40c02d0c96f887ec991580a24.tar.zst
forge-c9b4eee4c589b8b40c02d0c96f887ec991580a24.zip
Restructure static/templates into forged
Diffstat (limited to 'forged')
-rw-r--r--forged/.golangci.yaml42
-rw-r--r--forged/cmd/forge/main.go1
-rw-r--r--forged/internal/embed/.gitignore1
-rw-r--r--forged/internal/embed/embed.go2
-rw-r--r--forged/internal/unsorted/resources.go2
-rw-r--r--forged/internal/unsorted/server.go2
-rw-r--r--forged/static/.gitignore2
-rw-r--r--forged/static/chroma.css152
-rw-r--r--forged/static/style.css613
-rw-r--r--forged/templates/400.tmpl25
-rw-r--r--forged/templates/400_colon.tmpl26
-rw-r--r--forged/templates/403.tmpl25
-rw-r--r--forged/templates/404.tmpl24
-rw-r--r--forged/templates/451.tmpl25
-rw-r--r--forged/templates/500.tmpl25
-rw-r--r--forged/templates/501.tmpl24
-rw-r--r--forged/templates/_footer.tmpl12
-rw-r--r--forged/templates/_group_path.tmpl8
-rw-r--r--forged/templates/_group_view.tmpl56
-rw-r--r--forged/templates/_head.tmpl9
-rw-r--r--forged/templates/_header.tmpl35
-rw-r--r--forged/templates/_ref_query.tmpl3
-rw-r--r--forged/templates/group.tmpl80
-rw-r--r--forged/templates/index.tmpl63
-rw-r--r--forged/templates/login.tmpl59
-rw-r--r--forged/templates/repo_branches.tmpl71
-rw-r--r--forged/templates/repo_commit.tmpl117
-rw-r--r--forged/templates/repo_contrib_index.tmpl82
-rw-r--r--forged/templates/repo_contrib_one.tmpl123
-rw-r--r--forged/templates/repo_index.tmpl94
-rw-r--r--forged/templates/repo_log.tmpl90
-rw-r--r--forged/templates/repo_raw_dir.tmpl88
-rw-r--r--forged/templates/repo_tree_dir.tmpl93
-rw-r--r--forged/templates/repo_tree_file.tmpl65
34 files changed, 2136 insertions, 3 deletions
diff --git a/forged/.golangci.yaml b/forged/.golangci.yaml
new file mode 100644
index 0000000..1c8c972
--- /dev/null
+++ b/forged/.golangci.yaml
@@ -0,0 +1,42 @@
+linters:
+ enable-all: true
+ disable:
+ - tenv
+ - depguard
+ - err113 # dynamically defined errors are fine for our purposes
+ - forcetypeassert # type assertion failures are usually programming errors
+ - gochecknoinits # we use inits sparingly for good reasons
+ - godox # they're just used as markers for where needs improvements
+ - ireturn # doesn't work well with how we use generics
+ - lll # long lines are acceptable
+ - mnd # it's a bit ridiculous to replace all of them
+ - nakedret # patterns should be consistent
+ - nonamedreturns # i like named returns
+ - wrapcheck # wrapping all errors is just not necessary
+ - varnamelen # "from" and "to" are very valid
+ - stylecheck
+ - containedctx
+ - godot
+ - dogsled
+ - maintidx # e
+ - nestif # e
+ - gocognit # e
+ - gocyclo # e
+ - dupl # e
+ - cyclop # e
+ - goconst # e
+ - funlen # e
+ - wsl # e
+ - nlreturn # e
+ - unused # e
+ - exhaustruct # e
+
+linters-settings:
+ revive:
+ rules:
+ - name: error-strings
+ disabled: true
+
+issues:
+ max-issues-per-linter: 0
+ max-same-issues: 0
diff --git a/forged/cmd/forge/main.go b/forged/cmd/forge/main.go
index 8785751..fde15d1 100644
--- a/forged/cmd/forge/main.go
+++ b/forged/cmd/forge/main.go
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+// The main entry point to the Lindenii Forge daemon.
package main
import (
diff --git a/forged/internal/embed/.gitignore b/forged/internal/embed/.gitignore
index b4db111..e8708b1 100644
--- a/forged/internal/embed/.gitignore
+++ b/forged/internal/embed/.gitignore
@@ -4,3 +4,4 @@
/static
/templates
/LICENSE*
+/forged
diff --git a/forged/internal/embed/embed.go b/forged/internal/embed/embed.go
index 68af51d..c9eeeb3 100644
--- a/forged/internal/embed/embed.go
+++ b/forged/internal/embed/embed.go
@@ -9,6 +9,6 @@ import "embed"
//go:embed LICENSE* source.tar.gz
var Source embed.FS
-//go:embed templates/* static/*
+//go:embed forged/templates/* forged/static/*
//go:embed hookc/hookc git2d/git2d
var Resources embed.FS
diff --git a/forged/internal/unsorted/resources.go b/forged/internal/unsorted/resources.go
index 9f3a5d0..692b454 100644
--- a/forged/internal/unsorted/resources.go
+++ b/forged/internal/unsorted/resources.go
@@ -30,7 +30,7 @@ func (s *Server) loadTemplates() (err error) {
"minus": misc.Minus,
})
- err = fs.WalkDir(embed.Resources, "templates", func(path string, d fs.DirEntry, err error) error {
+ err = fs.WalkDir(embed.Resources, "forged/templates", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
diff --git a/forged/internal/unsorted/server.go b/forged/internal/unsorted/server.go
index f8978a5..c7caa76 100644
--- a/forged/internal/unsorted/server.go
+++ b/forged/internal/unsorted/server.go
@@ -63,7 +63,7 @@ func NewServer(configPath string) (*Server, error) {
"/-/source/",
http.FileServer(http.FS(embed.Source)),
)
- staticFS, err := fs.Sub(embed.Resources, "static")
+ staticFS, err := fs.Sub(embed.Resources, "forged/static")
if err != nil {
return s, err
}
diff --git a/forged/static/.gitignore b/forged/static/.gitignore
new file mode 100644
index 0000000..812b75f
--- /dev/null
+++ b/forged/static/.gitignore
@@ -0,0 +1,2 @@
+/index.html
+# used for testing css without recompiling the server
diff --git a/forged/static/chroma.css b/forged/static/chroma.css
new file mode 100644
index 0000000..1f7219a
--- /dev/null
+++ b/forged/static/chroma.css
@@ -0,0 +1,152 @@
+/*
+ * SPDX-License-Identifier: MIT AND BSD-2-Clause
+ * SPDX-FileCopyrightText: Copyright (c) 2018-2025 Pygments and Chroma authors
+ */
+
+@media (prefers-color-scheme: light) {
+ /* Background */ .bg { ; }
+ /* PreWrapper */ .chroma { ; }
+ /* Error */ .chroma .err { }
+ /* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
+ /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
+ /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
+ /* LineHighlight */ .chroma .hl { background-color: #e5e5e5 }
+ /* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
+ /* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
+ /* Line */ .chroma .line { display: flex; }
+ /* Keyword */ .chroma .k { color: #008000; font-weight: bold }
+ /* KeywordConstant */ .chroma .kc { color: #008000; font-weight: bold }
+ /* KeywordDeclaration */ .chroma .kd { color: #008000; font-weight: bold }
+ /* KeywordNamespace */ .chroma .kn { color: #008000; font-weight: bold }
+ /* KeywordPseudo */ .chroma .kp { color: #008000 }
+ /* KeywordReserved */ .chroma .kr { color: #008000; font-weight: bold }
+ /* KeywordType */ .chroma .kt { color: #b00040 }
+ /* NameAttribute */ .chroma .na { color: #7d9029 }
+ /* NameBuiltin */ .chroma .nb { color: #008000 }
+ /* NameClass */ .chroma .nc { color: #0000ff; font-weight: bold }
+ /* NameConstant */ .chroma .no { color: #880000 }
+ /* NameDecorator */ .chroma .nd { color: #aa22ff }
+ /* NameEntity */ .chroma .ni { color: #999999; font-weight: bold }
+ /* NameException */ .chroma .ne { color: #d2413a; font-weight: bold }
+ /* NameFunction */ .chroma .nf { color: #0000ff }
+ /* NameLabel */ .chroma .nl { color: #a0a000 }
+ /* NameNamespace */ .chroma .nn { color: #0000ff; font-weight: bold }
+ /* NameTag */ .chroma .nt { color: #008000; font-weight: bold }
+ /* NameVariable */ .chroma .nv { color: #19177c }
+ /* LiteralString */ .chroma .s { color: #ba2121 }
+ /* LiteralStringAffix */ .chroma .sa { color: #ba2121 }
+ /* LiteralStringBacktick */ .chroma .sb { color: #ba2121 }
+ /* LiteralStringChar */ .chroma .sc { color: #ba2121 }
+ /* LiteralStringDelimiter */ .chroma .dl { color: #ba2121 }
+ /* LiteralStringDoc */ .chroma .sd { color: #ba2121; font-style: italic }
+ /* LiteralStringDouble */ .chroma .s2 { color: #ba2121 }
+ /* LiteralStringEscape */ .chroma .se { color: #bb6622; font-weight: bold }
+ /* LiteralStringHeredoc */ .chroma .sh { color: #ba2121 }
+ /* LiteralStringInterpol */ .chroma .si { color: #bb6688; font-weight: bold }
+ /* LiteralStringOther */ .chroma .sx { color: #008000 }
+ /* LiteralStringRegex */ .chroma .sr { color: #bb6688 }
+ /* LiteralStringSingle */ .chroma .s1 { color: #ba2121 }
+ /* LiteralStringSymbol */ .chroma .ss { color: #19177c }
+ /* LiteralNumber */ .chroma .m { color: #666666 }
+ /* LiteralNumberBin */ .chroma .mb { color: #666666 }
+ /* LiteralNumberFloat */ .chroma .mf { color: #666666 }
+ /* LiteralNumberHex */ .chroma .mh { color: #666666 }
+ /* LiteralNumberInteger */ .chroma .mi { color: #666666 }
+ /* LiteralNumberIntegerLong */ .chroma .il { color: #666666 }
+ /* LiteralNumberOct */ .chroma .mo { color: #666666 }
+ /* Operator */ .chroma .o { color: #666666 }
+ /* OperatorWord */ .chroma .ow { color: #aa22ff; font-weight: bold }
+ /* Comment */ .chroma .c { color: #408080; font-style: italic }
+ /* CommentHashbang */ .chroma .ch { color: #408080; font-style: italic }
+ /* CommentMultiline */ .chroma .cm { color: #408080; font-style: italic }
+ /* CommentSingle */ .chroma .c1 { color: #408080; font-style: italic }
+ /* CommentSpecial */ .chroma .cs { color: #408080; font-style: italic }
+ /* CommentPreproc */ .chroma .cp { color: #bc7a00 }
+ /* CommentPreprocFile */ .chroma .cpf { color: #bc7a00 }
+ /* GenericDeleted */ .chroma .gd { color: #a00000 }
+ /* GenericEmph */ .chroma .ge { font-style: italic }
+ /* GenericError */ .chroma .gr { color: #ff0000 }
+ /* GenericHeading */ .chroma .gh { color: #000080; font-weight: bold }
+ /* GenericInserted */ .chroma .gi { color: #00a000 }
+ /* GenericOutput */ .chroma .go { color: #888888 }
+ /* GenericPrompt */ .chroma .gp { color: #000080; font-weight: bold }
+ /* GenericStrong */ .chroma .gs { font-weight: bold }
+ /* GenericSubheading */ .chroma .gu { color: #800080; font-weight: bold }
+ /* GenericTraceback */ .chroma .gt { color: #0044dd }
+ /* GenericUnderline */ .chroma .gl { text-decoration: underline }
+ /* TextWhitespace */ .chroma .w { color: #bbbbbb }
+}
+@media (prefers-color-scheme: dark) {
+ /* Background */ .bg { color: #e6edf3; background-color: #000000; }
+ /* PreWrapper */ .chroma { color: #e6edf3; background-color: #000000; }
+ /* Error */ .chroma .err { color: #f85149 }
+ /* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
+ /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
+ /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
+ /* LineHighlight */ .chroma .hl { background-color: #6e7681 }
+ /* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #737679 }
+ /* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #6e7681 }
+ /* Line */ .chroma .line { display: flex; }
+ /* Keyword */ .chroma .k { color: #ff7b72 }
+ /* KeywordConstant */ .chroma .kc { color: #79c0ff }
+ /* KeywordDeclaration */ .chroma .kd { color: #ff7b72 }
+ /* KeywordNamespace */ .chroma .kn { color: #ff7b72 }
+ /* KeywordPseudo */ .chroma .kp { color: #79c0ff }
+ /* KeywordReserved */ .chroma .kr { color: #ff7b72 }
+ /* KeywordType */ .chroma .kt { color: #ff7b72 }
+ /* NameClass */ .chroma .nc { color: #f0883e; font-weight: bold }
+ /* NameConstant */ .chroma .no { color: #79c0ff; font-weight: bold }
+ /* NameDecorator */ .chroma .nd { color: #d2a8ff; font-weight: bold }
+ /* NameEntity */ .chroma .ni { color: #ffa657 }
+ /* NameException */ .chroma .ne { color: #f0883e; font-weight: bold }
+ /* NameFunction */ .chroma .nf { color: #d2a8ff; font-weight: bold }
+ /* NameLabel */ .chroma .nl { color: #79c0ff; font-weight: bold }
+ /* NameNamespace */ .chroma .nn { color: #ff7b72 }
+ /* NameProperty */ .chroma .py { color: #79c0ff }
+ /* NameTag */ .chroma .nt { color: #7ee787 }
+ /* NameVariable */ .chroma .nv { color: #79c0ff }
+ /* Literal */ .chroma .l { color: #a5d6ff }
+ /* LiteralDate */ .chroma .ld { color: #79c0ff }
+ /* LiteralString */ .chroma .s { color: #a5d6ff }
+ /* LiteralStringAffix */ .chroma .sa { color: #79c0ff }
+ /* LiteralStringBacktick */ .chroma .sb { color: #a5d6ff }
+ /* LiteralStringChar */ .chroma .sc { color: #a5d6ff }
+ /* LiteralStringDelimiter */ .chroma .dl { color: #79c0ff }
+ /* LiteralStringDoc */ .chroma .sd { color: #a5d6ff }
+ /* LiteralStringDouble */ .chroma .s2 { color: #a5d6ff }
+ /* LiteralStringEscape */ .chroma .se { color: #79c0ff }
+ /* LiteralStringHeredoc */ .chroma .sh { color: #79c0ff }
+ /* LiteralStringInterpol */ .chroma .si { color: #a5d6ff }
+ /* LiteralStringOther */ .chroma .sx { color: #a5d6ff }
+ /* LiteralStringRegex */ .chroma .sr { color: #79c0ff }
+ /* LiteralStringSingle */ .chroma .s1 { color: #a5d6ff }
+ /* LiteralStringSymbol */ .chroma .ss { color: #a5d6ff }
+ /* LiteralNumber */ .chroma .m { color: #a5d6ff }
+ /* LiteralNumberBin */ .chroma .mb { color: #a5d6ff }
+ /* LiteralNumberFloat */ .chroma .mf { color: #a5d6ff }
+ /* LiteralNumberHex */ .chroma .mh { color: #a5d6ff }
+ /* LiteralNumberInteger */ .chroma .mi { color: #a5d6ff }
+ /* LiteralNumberIntegerLong */ .chroma .il { color: #a5d6ff }
+ /* LiteralNumberOct */ .chroma .mo { color: #a5d6ff }
+ /* Operator */ .chroma .o { color: #ff7b72; font-weight: bold }
+ /* OperatorWord */ .chroma .ow { color: #ff7b72; font-weight: bold }
+ /* Comment */ .chroma .c { color: #8b949e; font-style: italic }
+ /* CommentHashbang */ .chroma .ch { color: #8b949e; font-style: italic }
+ /* CommentMultiline */ .chroma .cm { color: #8b949e; font-style: italic }
+ /* CommentSingle */ .chroma .c1 { color: #8b949e; font-style: italic }
+ /* CommentSpecial */ .chroma .cs { color: #8b949e; font-weight: bold; font-style: italic }
+ /* CommentPreproc */ .chroma .cp { color: #8b949e; font-weight: bold; font-style: italic }
+ /* CommentPreprocFile */ .chroma .cpf { color: #8b949e; font-weight: bold; font-style: italic }
+ /* GenericDeleted */ .chroma .gd { color: #ffa198; background-color: #490202 }
+ /* GenericEmph */ .chroma .ge { font-style: italic }
+ /* GenericError */ .chroma .gr { color: #ffa198 }
+ /* GenericHeading */ .chroma .gh { color: #79c0ff; font-weight: bold }
+ /* GenericInserted */ .chroma .gi { color: #56d364; background-color: #0f5323 }
+ /* GenericOutput */ .chroma .go { color: #8b949e }
+ /* GenericPrompt */ .chroma .gp { color: #8b949e }
+ /* GenericStrong */ .chroma .gs { font-weight: bold }
+ /* GenericSubheading */ .chroma .gu { color: #79c0ff }
+ /* GenericTraceback */ .chroma .gt { color: #ff7b72 }
+ /* GenericUnderline */ .chroma .gl { text-decoration: underline }
+ /* TextWhitespace */ .chroma .w { color: #6e7681 }
+}
diff --git a/forged/static/style.css b/forged/static/style.css
new file mode 100644
index 0000000..b1e208f
--- /dev/null
+++ b/forged/static/style.css
@@ -0,0 +1,613 @@
+/*
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ * SPDX-FileCopyrightText: Copyright (c) 2025 luk3yx <https://luk3yx.github.io>
+ * SPDX-FileCopyrightText: Copyright (c) 2017-2025 Drew DeVault <https://drewdevault.com>
+ *
+ * Drew did not directly contribute here but we took significant portions of
+ * SourceHut's CSS.
+ */
+
+* {
+ box-sizing: border-box;
+}
+
+/* Base styles and variables */
+html {
+ font-family: sans-serif;
+ background-color: var(--background-color);
+ color: var(--text-color);
+ font-size: 1rem;
+ --background-color: hsl(0, 0%, 100%);
+ --text-color: hsl(0, 0%, 0%);
+ --link-color: hsl(320, 50%, 36%);
+ --light-text-color: hsl(0, 0%, 45%);
+ --darker-border-color: hsl(0, 0%, 72%);
+ --lighter-border-color: hsl(0, 0%, 85%);
+ --text-decoration-color: hsl(0, 0%, 72%);
+ --darker-box-background-color: hsl(0, 0%, 92%);
+ --lighter-box-background-color: hsl(0, 0%, 95%);
+ --primary-color: hsl(320, 50%, 36%);
+ --primary-color-contrast: hsl(320, 0%, 100%);
+ --danger-color: #ff0000;
+ --danger-color-contrast: #ffffff;
+}
+
+/* Dark mode overrides */
+@media (prefers-color-scheme: dark) {
+ html {
+ --background-color: hsl(0, 0%, 0%);
+ --text-color: hsl(0, 0%, 100%);
+ --link-color: hsl(320, 50%, 76%);
+ --light-text-color: hsl(0, 0%, 78%);
+ --darker-border-color: hsl(0, 0%, 35%);
+ --lighter-border-color: hsl(0, 0%, 25%);
+ --text-decoration-color: hsl(0, 0%, 30%);
+ --darker-box-background-color: hsl(0, 0%, 20%);
+ --lighter-box-background-color: hsl(0, 0%, 15%);
+ }
+}
+
+/* Global layout */
+body {
+ margin: 0;
+}
+html, code, pre {
+ font-size: 0.96rem; /* TODO: Not always correct */
+}
+
+/* Toggle table controls */
+.toggle-table-off, .toggle-table-on {
+ opacity: 0;
+ position: absolute;
+}
+.toggle-table-off:focus-visible + table > thead > tr > th > label,
+.toggle-table-on:focus-visible + table > thead > tr > th > label {
+ outline: 1.5px var(--primary-color) solid;
+}
+.toggle-table-off + table > thead > tr > th, .toggle-table-on + table > thead > tr > th {
+ padding: 0;
+}
+.toggle-table-off + table > thead > tr > th > label, .toggle-table-on + table > thead > tr > th > label {
+ width: 100%;
+ display: inline-block;
+ padding: 3px 0;
+ cursor: pointer;
+}
+.toggle-table-off:checked + table > tbody {
+ display: none;
+}
+.toggle-table-on + table > tbody {
+ display: none;
+}
+.toggle-table-on:checked + table > tbody {
+ display: table-row-group;
+}
+
+/* Footer styles */
+footer {
+ margin-top: 1rem;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 1rem;
+ display: block;
+ padding: 0 5px;
+ width: fit-content;
+ text-align: center;
+ color: var(--light-text-color);
+}
+footer a:link, footer a:visited {
+ color: inherit;
+}
+
+.padding {
+ padding: 0 1rem;
+}
+
+/* Link styles */
+a:link, a:visited {
+ text-decoration-color: var(--text-decoration-color);
+ color: var(--link-color);
+}
+
+/* Readme inline code styling */
+#readme code:not(pre > code) {
+ background-color: var(--lighter-box-background-color);
+ border-radius: 2px;
+ padding: 2px;
+}
+
+/* Readme word breaks to avoid overfull hboxes */
+#readme {
+ word-break: break-word;
+ line-height: 1.3;
+}
+
+/* Table styles */
+table {
+ border: var(--lighter-border-color) solid 1px;
+ border-spacing: 0px;
+ border-collapse: collapse;
+}
+table.wide {
+ width: 100%;
+}
+td, th {
+ padding: 3px 5px;
+ border: var(--lighter-border-color) solid 1px;
+}
+.pad {
+ padding: 3px 5px;
+}
+th, thead, tfoot {
+ background-color: var(--lighter-box-background-color);
+}
+th[scope=row] {
+ text-align: left;
+}
+th {
+ font-weight: normal;
+}
+tr.title-row > th, th.title-row, .title-row {
+ background-color: var(--lighter-box-background-color);
+ font-weight: bold;
+}
+td > pre {
+ margin: 0;
+}
+#readme > *:last-child {
+ margin-bottom: 0;
+}
+#readme > *:first-child {
+ margin-top: 0;
+}
+
+/* Table misc and scrolling */
+.commit-id {
+ font-family: monospace;
+ word-break: break-word;
+}
+.scroll {
+ overflow-x: auto;
+}
+
+/* Diff/chunk styles */
+.chunk-unchanged {
+ color: grey;
+}
+.chunk-addition {
+ color: green;
+}
+@media (prefers-color-scheme: dark) {
+ .chunk-addition {
+ color: lime;
+ }
+}
+.chunk-deletion {
+ color: red;
+}
+.chunk-unknown {
+ color: yellow;
+}
+pre.chunk {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+.centering {
+ text-align: center;
+}
+
+/* Toggle content sections */
+.toggle-off-wrapper, .toggle-on-wrapper {
+ border: var(--lighter-border-color) solid 1px;
+}
+.toggle-off-toggle, .toggle-on-toggle {
+ opacity: 0;
+ position: absolute;
+}
+.toggle-off-header, .toggle-on-header {
+ font-weight: bold;
+ cursor: pointer;
+ display: block;
+ width: 100%;
+ background-color: var(--lighter-box-background-color);
+}
+.toggle-off-header > div, .toggle-on-header > div {
+ padding: 3px 5px;
+ display: block;
+}
+.toggle-on-content {
+ display: none;
+}
+.toggle-on-toggle:focus-visible + .toggle-on-header, .toggle-off-toggle:focus-visible + .toggle-off-header {
+ outline: 1.5px var(--primary-color) solid;
+}
+.toggle-on-toggle:checked + .toggle-on-header + .toggle-on-content {
+ display: block;
+}
+.toggle-off-content {
+ display: block;
+}
+.toggle-off-toggle:checked + .toggle-off-header + .toggle-off-content {
+ display: none;
+}
+
+*:focus-visible {
+ outline: 1.5px var(--primary-color) solid;
+}
+
+/* File display styles */
+.file-patch + .file-patch {
+ margin-top: 0.5rem;
+}
+.file-content {
+ padding: 3px 5px;
+}
+.file-header {
+ font-family: monospace;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+.file-header::after {
+ content: "\25b6";
+ font-family: sans-serif;
+ margin-left: auto;
+ line-height: 100%;
+ margin-right: 0.25em;
+}
+.file-toggle:checked + .file-header::after {
+ content: "\25bc";
+}
+
+/* Form elements */
+textarea {
+ box-sizing: border-box;
+ background-color: var(--lighter-box-background-color);
+ resize: vertical;
+}
+textarea,
+input[type=text],
+input[type=password] {
+ font-family: sans-serif;
+ background-color: var(--lighter-box-background-color);
+ color: var(--text-color);
+ border: none;
+ padding: 0.3rem;
+ width: 100%;
+ box-sizing: border-box;
+}
+td.tdinput, th.tdinput {
+ padding: 0;
+ position: relative;
+}
+td.tdinput textarea,
+td.tdinput input[type=text],
+td.tdinput input[type=password],
+th.tdinput textarea,
+th.tdinput input[type=text],
+th.tdinput input[type=password] {
+ background-color: transparent;
+}
+td.tdinput select {
+ position: absolute;
+ background-color: var(--background-color);
+ border: none;
+ /*
+ width: 100%;
+ height: 100%;
+ */
+ box-sizing: border-box;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+select:active {
+ outline: 1.5px var(--primary-color) solid;
+}
+
+
+/* Button styles */
+.btn-primary, a.btn-primary {
+ background: var(--primary-color);
+ color: var(--primary-color-contrast);
+ border: var(--lighter-border-color) 1px solid;
+ font-weight: bold;
+}
+.btn-danger, a.btn-danger {
+ background: var(--danger-color);
+ color: var(--danger-color-contrast);
+ border: var(--lighter-border-color) 1px solid;
+ font-weight: bold;
+}
+.btn-white, a.btn-white {
+ background: var(--primary-color-contrast);
+ color: var(--primary-color);
+ border: var(--lighter-border-color) 1px solid;
+}
+.btn-normal, a.btn-normal,
+input[type=file]::file-selector-button {
+ background: var(--lighter-box-background-color);
+ border: var(--lighter-border-color) 1px solid !important;
+ color: var(--text-color);
+}
+.btn, .btn-white, .btn-danger, .btn-normal, .btn-primary,
+input[type=submit],
+input[type=file]::file-selector-button {
+ display: inline-block;
+ width: auto;
+ min-width: fit-content;
+ padding: .1rem .75rem;
+ transition: background .1s linear;
+ cursor: pointer;
+}
+a.btn, a.btn-white, a.btn-danger, a.btn-normal, a.btn-primary {
+ text-decoration: none;
+}
+
+/* Header layout */
+header#main-header {
+ /* background-color: var(--lighter-box-background-color); */
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ gap: 0.5rem;
+}
+#main-header a, #main-header a:link, main-header a:visited {
+ text-decoration: none;
+ color: inherit;
+}
+#main-header-forge-title {
+ white-space: nowrap;
+}
+#breadcrumb-nav {
+ display: flex;
+ align-items: center;
+ flex: 1 1 auto;
+ min-width: 0;
+ overflow-x: auto;
+ gap: 0.25rem;
+ white-space: nowrap;
+}
+.breadcrumb-separator {
+ margin: 0 0.25rem;
+}
+#main-header-user {
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+}
+@media (max-width: 37.5rem) {
+ header#main-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ #breadcrumb-nav {
+ width: 100%;
+ overflow-x: auto;
+ }
+}
+
+/* Uncategorized */
+table + table {
+ margin-top: 1rem;
+}
+
+td > ul {
+ padding-left: 1.5rem;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+
+
+.complete-error-page hr {
+ border: 0;
+ border-bottom: 1px dashed;
+}
+
+
+
+
+
+
+.key-val-grid {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 0;
+ border: var(--lighter-border-color) 1px solid;
+ overflow: auto;
+}
+
+.key-val-grid > .title-row {
+ grid-column: 1 / -1;
+ background-color: var(--lighter-box-background-color);
+ font-weight: bold;
+ padding: 3px 5px;
+ border-bottom: var(--lighter-border-color) 1px solid;
+}
+
+.key-val-grid > .row-label {
+ background-color: var(--lighter-box-background-color);
+ padding: 3px 5px;
+ border-bottom: var(--lighter-border-color) 1px solid;
+ border-right: var(--lighter-border-color) 1px solid;
+ text-align: left;
+ font-weight: normal;
+}
+
+.key-val-grid > .row-value {
+ padding: 3px 5px;
+ border-bottom: var(--lighter-border-color) 1px solid;
+ word-break: break-word;
+}
+
+.key-val-grid code {
+ font-family: monospace;
+}
+
+.key-val-grid ul {
+ margin: 0;
+ padding-left: 1.5rem;
+}
+
+.key-val-grid > .row-label:nth-last-of-type(2),
+.key-val-grid > .row-value:last-of-type {
+ border-bottom: none;
+}
+
+@media (max-width: 37.5rem) {
+ .key-val-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .key-val-grid > .row-label {
+ border-right: none;
+ }
+}
+.key-val-grid > .title-row {
+ grid-column: 1 / -1;
+ background-color: var(--lighter-box-background-color);
+ font-weight: bold;
+ padding: 3px 5px;
+ border-bottom: var(--lighter-border-color) 1px solid;
+ margin: 0;
+ text-align: center;
+}
+
+.key-val-grid-wrapper {
+ max-width: 100%;
+ width: fit-content;
+}
+
+/* Tab navigation */
+
+.nav-tabs-standalone {
+ border: none;
+ list-style: none;
+ margin: 0;
+ flex-grow: 1;
+ display: inline-flex;
+ flex-wrap: nowrap;
+ padding: 0;
+ border-bottom: 0.25rem var(--darker-box-background-color) solid;
+ width: 100%;
+ max-width: 100%;
+ min-width: 100%;
+}
+
+.nav-tabs-standalone > li {
+ align-self: flex-end;
+}
+.nav-tabs-standalone > li > a {
+ padding: 0 1rem;
+}
+
+.nav-item a.active {
+ background-color: var(--darker-box-background-color);
+}
+
+.nav-item a, .nav-item a:link, .nav-item a:visited {
+ text-decoration: none;
+ color: inherit;
+}
+
+.repo-header-extension {
+ margin-bottom: 1rem;
+ background-color: var(--darker-box-background-color);
+}
+
+.repo-header > h2 {
+ display: inline;
+ margin: 0;
+ padding-right: 1rem;
+}
+
+.repo-header > .nav-tabs-standalone {
+ border: none;
+ margin: 0;
+ flex-grow: 1;
+ display: inline-flex;
+ flex-wrap: nowrap;
+ padding: 0;
+}
+
+.repo-header {
+ display: flex;
+ flex-wrap: nowrap;
+}
+
+.repo-header-extension-content {
+ padding-top: 0.3rem;
+ padding-bottom: 0.2rem;
+}
+
+.repo-header, .padding-wrapper, .repo-header-extension-content, #main-header, .readingwidth, .commit-list-small {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ max-width: 60rem;
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.padding-wrapper {
+ margin-bottom: 1rem;
+}
+
+/* TODO */
+
+.commit-list-small .event {
+ background-color: var(--lighter-box-background-color);
+ padding: 0.5rem;
+ margin-bottom: 1rem;
+ max-width: 30rem;
+}
+
+.commit-list-small .event:last-child {
+ margin-bottom: 1rem;
+}
+
+.commit-list-small a {
+ color: var(--link-color);
+ text-decoration: none;
+ font-weight: 500;
+}
+
+.commit-list-small a:hover {
+ text-decoration: underline;
+ text-decoration-color: var(--text-decoration-color);
+}
+
+.commit-list-small .event > div {
+ font-size: 0.95rem;
+ line-height: 1.4;
+}
+
+.commit-list-small .pull-right {
+ float: right;
+ font-size: 0.85em;
+ color: var(--light-text-color);
+ margin-left: 1rem;
+}
+
+.commit-list-small pre.commit {
+ margin: 0.25rem 0 0 0;
+ padding: 0;
+ font-family: inherit;
+ font-size: 0.95rem;
+ color: var(--text-color);
+ white-space: pre-wrap;
+}
+
+.commit-list-small .commit-error {
+ color: var(--danger-color);
+ font-weight: bold;
+ margin-top: 1rem;
+}
diff --git a/forged/templates/400.tmpl b/forged/templates/400.tmpl
new file mode 100644
index 0000000..58ce768
--- /dev/null
+++ b/forged/templates/400.tmpl
@@ -0,0 +1,25 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "400" -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>400 Bad Request &ndash; {{ .global.forge_title }}</title>
+ </head>
+ <body class="400">
+ {{- template "header" . -}}
+ <div class="padding-wrapper complete-error-page">
+ <h1>400 Bad Request</h1>
+ <p>{{- .complete_error_msg -}}</p>
+ <hr />
+ <address>Lindenii Forge</address>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/400_colon.tmpl b/forged/templates/400_colon.tmpl
new file mode 100644
index 0000000..470a685
--- /dev/null
+++ b/forged/templates/400_colon.tmpl
@@ -0,0 +1,26 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "400_colon" -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>400 Bad Request &ndash; {{ .global.forge_title }}</title>
+ </head>
+ <body class="400-colon">
+ {{- template "header" . -}}
+ <div class="padding-wrapper complete-error-page">
+ <h1>400 Bad Request</h1>
+ <p>We recently switched URL schemes. Previously &ldquo;<code>:</code>&rdquo; was used as our URL group separator, but because OpenSMTPD does not implement local-part address quoting properly, we&rsquo;re unable to include &ldquo;<code>:</code>&rdquo; in URLs properly, hence we use &ldquo;<code>-</code>&rdquo; now.</p>
+ <p>As a precaution in case visitors get confused, this page was set up. <strong>You should probably replace the &ldquo;<code>:</code>&rdquo;s with &ldquo;<code>-</code>&rdquo;s in the URL bar.</strong> If there are colons in the URL that <em>is not</em> the group separator&mdash;that&rsquo;s an edge case that we&rsquo;ll fix later.</p>
+ <hr />
+ <address>Lindenii Forge</address>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/403.tmpl b/forged/templates/403.tmpl
new file mode 100644
index 0000000..86d5518
--- /dev/null
+++ b/forged/templates/403.tmpl
@@ -0,0 +1,25 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "403" -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>403 Forbidden &ndash; {{ .global.forge_title }}</title>
+ </head>
+ <body class="403">
+ {{- template "header" . -}}
+ <div class="padding-wrapper complete-error-page">
+ <h1>403 Forbidden</h1>
+ <p>{{- .complete_error_msg -}}</p>
+ <hr />
+ <address>Lindenii Forge</address>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/404.tmpl b/forged/templates/404.tmpl
new file mode 100644
index 0000000..2eabb06
--- /dev/null
+++ b/forged/templates/404.tmpl
@@ -0,0 +1,24 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "404" -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>404 Not Found &ndash; {{ .global.forge_title }}</title>
+ </head>
+ <body class="404">
+ {{- template "header" . -}}
+ <div class="padding-wrapper complete-error-page">
+ <h1>404 Not Found</h1>
+ <hr />
+ <address>Lindenii Forge</address>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/451.tmpl b/forged/templates/451.tmpl
new file mode 100644
index 0000000..ed6343c
--- /dev/null
+++ b/forged/templates/451.tmpl
@@ -0,0 +1,25 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "451" -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>451 Unavailable For Legal Reasons &ndash; {{ .global.forge_title }}</title>
+ </head>
+ <body class="451">
+ {{- template "header" . -}}
+ <div class="padding-wrapper complete-error-page">
+ <h1>451 Unavailable For Legal Reasons</h1>
+ <p>{{- .complete_error_msg -}}</p>
+ <hr />
+ <address>Lindenii Forge</address>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/500.tmpl b/forged/templates/500.tmpl
new file mode 100644
index 0000000..3a540e6
--- /dev/null
+++ b/forged/templates/500.tmpl
@@ -0,0 +1,25 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "500" -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>500 Internal Server Error &ndash; {{ .global.forge_title }}</title>
+ </head>
+ <body class="500">
+ {{- template "header" . -}}
+ <div class="padding-wrapper complete-error-page">
+ <h1>500 Internal Server Error</h1>
+ <p>{{- .complete_error_msg -}}</p>
+ <hr />
+ <address>Lindenii Forge</address>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/501.tmpl b/forged/templates/501.tmpl
new file mode 100644
index 0000000..b6ab2f0
--- /dev/null
+++ b/forged/templates/501.tmpl
@@ -0,0 +1,24 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "501" -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>501 Not Implemented &ndash; {{ .global.forge_title }}</title>
+ </head>
+ <body class="501">
+ {{- template "header" . -}}
+ <div class="padding-wrapper complete-error-page">
+ <h1>501 Not Implemented</h1>
+ <hr />
+ <address>Lindenii Forge</address>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/_footer.tmpl b/forged/templates/_footer.tmpl
new file mode 100644
index 0000000..f71ea3e
--- /dev/null
+++ b/forged/templates/_footer.tmpl
@@ -0,0 +1,12 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "footer" -}}
+<a href="https://lindenii.runxiyu.org/forge/">Lindenii Forge</a>
+{{ .global.forge_version }}
+(<a href="/-/source/source.tar.gz">source</a>,
+<a href="https://forge.lindenii.runxiyu.org/forge/-/repos/server/">upstream</a>,
+<a href="/-/source/LICENSE">license</a>,
+<a href="https://webirc.runxiyu.org/kiwiirc/#lindenii">support</a>)
+{{- end -}}
diff --git a/forged/templates/_group_path.tmpl b/forged/templates/_group_path.tmpl
new file mode 100644
index 0000000..f5d3bf8
--- /dev/null
+++ b/forged/templates/_group_path.tmpl
@@ -0,0 +1,8 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "group_path_plain" -}}
+{{- $p := . -}}
+{{- range $i, $s := . -}}{{- $s -}}{{- if ne $i (minus (len $p) 1) -}}/{{- end -}}{{- end -}}
+{{- end -}}
diff --git a/forged/templates/_group_view.tmpl b/forged/templates/_group_view.tmpl
new file mode 100644
index 0000000..92b6639
--- /dev/null
+++ b/forged/templates/_group_view.tmpl
@@ -0,0 +1,56 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "group_view" -}}
+{{- if .subgroups -}}
+ <table class="wide">
+ <thead>
+ <tr>
+ <th colspan="2" class="title-row">Subgroups</th>
+ </tr>
+ <tr>
+ <th scope="col">Name</th>
+ <th scope="col">Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{- range .subgroups -}}
+ <tr>
+ <td>
+ <a href="{{- .Name | path_escape -}}/">{{- .Name -}}</a>
+ </td>
+ <td>
+ {{- .Description -}}
+ </td>
+ </tr>
+ {{- end -}}
+ </tbody>
+ </table>
+{{- end -}}
+{{- if .repos -}}
+<table class="wide">
+ <thead>
+ <tr>
+ <th colspan="2" class="title-row">Repos</th>
+ <tr>
+ <th scope="col">Name</th>
+ <th scope="col">Description</th>
+ </tr>
+ </tr>
+ </thead>
+ <tbody>
+ {{- range .repos -}}
+ <tr>
+ <td>
+ <a href="-/repos/{{- .Name | path_escape -}}/">{{- .Name -}}</a>
+ </td>
+ <td>
+ {{- .Description -}}
+ </td>
+ </tr>
+ {{- end -}}
+ </tbody>
+</table>
+{{- end -}}
+{{- end -}}
diff --git a/forged/templates/_head.tmpl b/forged/templates/_head.tmpl
new file mode 100644
index 0000000..d6d6571
--- /dev/null
+++ b/forged/templates/_head.tmpl
@@ -0,0 +1,9 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "head_common" -}}
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<link rel="stylesheet" href="/-/static/style.css" />
+{{- end -}}
diff --git a/forged/templates/_header.tmpl b/forged/templates/_header.tmpl
new file mode 100644
index 0000000..340a2ac
--- /dev/null
+++ b/forged/templates/_header.tmpl
@@ -0,0 +1,35 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "header" -}}
+<header id="main-header">
+ <div id="main-header-forge-title">
+ <a href="/">{{- .global.forge_title -}}</a>
+ </div>
+ <nav id="breadcrumb-nav">
+ {{- $path := "" -}}
+ {{- $url_segments := .url_segments -}}
+ {{- $dir_mode := .dir_mode -}}
+ {{- $ref_type := .ref_type -}}
+ {{- $ref := .ref_name -}}
+ {{- $separator_index := .separator_index -}}
+ {{- if eq $separator_index -1 -}}
+ {{- $separator_index = len $url_segments -}}
+ {{- end -}}
+ {{- range $i := $separator_index -}}
+ {{- $segment := index $url_segments $i -}}
+ {{- $path = printf "%s/%s" $path $segment -}}
+ <span class="breadcrumb-separator">/</span>
+ <a href="{{ $path }}{{ if or (ne $i (minus (len $url_segments) 1)) $dir_mode }}/{{ end }}{{- if $ref_type -}}?{{- $ref_type -}}={{- $ref -}}{{- end -}}">{{ $segment }}</a>
+ {{- end -}}
+ </nav>
+ <div id="main-header-user">
+ {{- if ne .user_id_string "" -}}
+ <a href="/-/users/{{- .user_id_string -}}">{{- .username -}}</a>
+ {{- else -}}
+ <a href="/-/login/">Login</a>
+ {{- end -}}
+ </div>
+</header>
+{{- end -}}
diff --git a/forged/templates/_ref_query.tmpl b/forged/templates/_ref_query.tmpl
new file mode 100644
index 0000000..2f78955
--- /dev/null
+++ b/forged/templates/_ref_query.tmpl
@@ -0,0 +1,3 @@
+{{- define "ref_query" -}}
+{{- if .ref_type -}}?{{- .ref_type -}}={{- .ref_name -}}{{- end -}}
+{{- end -}}
diff --git a/forged/templates/group.tmpl b/forged/templates/group.tmpl
new file mode 100644
index 0000000..b15c316
--- /dev/null
+++ b/forged/templates/group.tmpl
@@ -0,0 +1,80 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "group" -}}
+{{- $group_path := .group_path -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>{{- range $i, $s := .group_path -}}{{- $s -}}{{- if ne $i (len $group_path) -}}/{{- end -}}{{- end }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="group">
+ {{- template "header" . -}}
+ <div class="padding-wrapper">
+ {{- if .description -}}
+ <p>{{- .description -}}</p>
+ {{- end -}}
+ {{- template "group_view" . -}}
+ </div>
+ {{- if .direct_access -}}
+ <div class="padding-wrapper">
+ <form method="POST" enctype="application/x-www-form-urlencoded">
+ <table>
+ <thead>
+ <tr>
+ <th class="title-row" colspan="2">
+ Create repo
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th scope="row">Name</th>
+ <td class="tdinput">
+ <input id="repo-name-input" name="repo_name" type="text" />
+ </td>
+ </tr>
+ <tr>
+ <th scope="row">Description</th>
+ <td class="tdinput">
+ <input id="repo-desc-input" name="repo_desc" type="text" />
+ </td>
+ </tr>
+ <tr>
+ <th scope="row">Contrib</th>
+ <td class="tdinput">
+ <select id="repo-contrib-input" name="repo_contrib">
+ <option value="public">Public</option>
+ <option value="ssh_pubkey">SSH public key</option>
+ <option value="federated">Federated service</option>
+ <option value="registered_user">Registered user</option>
+ <option value="closed">Closed</option>
+ </select>
+ </td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <td class="th-like" colspan="2">
+ <div class="flex-justify">
+ <div class="left">
+ </div>
+ <div class="right">
+ <input class="btn-primary" type="submit" value="Create" />
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ </form>
+ </div>
+ {{- end -}}
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/index.tmpl b/forged/templates/index.tmpl
new file mode 100644
index 0000000..ff7c127
--- /dev/null
+++ b/forged/templates/index.tmpl
@@ -0,0 +1,63 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "index" -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>Index &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="index">
+ {{- template "header" . -}}
+ <div class="padding-wrapper">
+ <table class="wide">
+ <thead>
+ <tr>
+ <th colspan="2" class="title-row">Groups</th>
+ </tr>
+ <tr>
+ <th scope="col">Name</th>
+ <th scope="col">Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{- range .groups -}}
+ <tr>
+ <td>
+ <a href="{{- .Name | path_escape -}}/">{{- .Name -}}</a>
+ </td>
+ <td>
+ {{- .Description -}}
+ </td>
+ </tr>
+ {{- end -}}
+ </tbody>
+ </table>
+ <table class="wide">
+ <thead>
+ <tr>
+ <th colspan="2" class="title-row">
+ Info
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th scope="row">SSH public key</th>
+ <td><code>{{- .global.server_public_key_string -}}</code></td>
+ </tr>
+ <tr>
+ <th scope="row">SSH fingerprint</th>
+ <td><code>{{- .global.server_public_key_fingerprint -}}</code></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/login.tmpl b/forged/templates/login.tmpl
new file mode 100644
index 0000000..1e26c82
--- /dev/null
+++ b/forged/templates/login.tmpl
@@ -0,0 +1,59 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "login" -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>Login &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="index">
+ {{- .login_error -}}
+ <div class="padding-wrapper">
+ <form method="POST" enctype="application/x-www-form-urlencoded">
+ <table>
+ <thead>
+ <tr>
+ <th class="title-row" colspan="2">
+ Password authentication
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th scope="row">Username</th>
+ <td class="tdinput">
+ <input id="usernameinput" name="username" type="text" />
+ </td>
+ </tr>
+ <tr>
+ <th scope="row">Password</th>
+ <td class="tdinput">
+ <input id="passwordinput" name="password" type="password" />
+ </td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <td class="th-like" colspan="2">
+ <div class="flex-justify">
+ <div class="left">
+ </div>
+ <div class="right">
+ <input class="btn-primary" type="submit" value="Submit" />
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ </form>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/repo_branches.tmpl b/forged/templates/repo_branches.tmpl
new file mode 100644
index 0000000..55ea0a6
--- /dev/null
+++ b/forged/templates/repo_branches.tmpl
@@ -0,0 +1,71 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "repo_branches" -}}
+{{- $root := . -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>{{ .repo_name }} &ndash; {{ template "group_path_plain" .group_path }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="repo-branches">
+ {{- template "header" . -}}
+ <div class="repo-header">
+ <h2>{{- .repo_name -}}</h2>
+ <ul class="nav-tabs-standalone">
+ <li class="nav-item">
+ <a class="nav-link" href="../{{- template "ref_query" $root -}}">Summary</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tree/{{- template "ref_query" $root -}}">Tree</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../log/{{- template "ref_query" $root -}}">Log</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link active" href="../branches/">Branches</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tags/">Tags</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../contrib/">Merge requests</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../settings/">Settings</a>
+ </li>
+ </ul>
+ </div>
+ <div class="repo-header-extension">
+ <div class="repo-header-extension-content">
+ {{- .repo_description -}}
+ </div>
+ </div>
+ <div class="padding-wrapper">
+ <p>
+ <strong>
+ Warning: Due to various recent migrations, viewing non-HEAD refs may be broken.
+ </strong>
+ </p>
+ <table id="branches">
+ <thead>
+ <tr class="title-row">
+ <th colspan="1">Branches</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{- range .branches -}}
+ <tr>
+ <td>
+ <a href="../?branch={{ . }}">{{ . }}</a>
+ </td>
+ </tr>
+ {{- end -}}
+ </tbody>
+ </table>
+ </div>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/repo_commit.tmpl b/forged/templates/repo_commit.tmpl
new file mode 100644
index 0000000..470bba9
--- /dev/null
+++ b/forged/templates/repo_commit.tmpl
@@ -0,0 +1,117 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "repo_commit" -}}
+{{- $root := . -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>Commit {{ .commit_id }} &ndash; {{ .repo_name }} &ndash; {{ template "group_path_plain" .group_path }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="repo-commit">
+ {{- template "header" . -}}
+ <div class="repo-header">
+ <h2>{{- .repo_name -}}</h2>
+ <ul class="nav-tabs-standalone">
+ <li class="nav-item">
+ <a class="nav-link" href="../{{- template "ref_query" $root -}}">Summary</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tree/{{- template "ref_query" $root -}}">Tree</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../log/{{- template "ref_query" $root -}}">Log</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../branches/">Branches</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tags/">Tags</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../contrib/">Merge requests</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../settings/">Settings</a>
+ </li>
+ </ul>
+ </div>
+ <div class="repo-header-extension">
+ <div class="repo-header-extension-content">
+ {{- .repo_description -}}
+ </div>
+ </div>
+ <div class="padding-wrapper scroll">
+ <div class="key-val-grid-wrapper">
+ <section id="commit-info" class="key-val-grid">
+ <div class="title-row">Commit info</div>
+ <div class="row-label">ID</div>
+ <div class="row-value">{{- .commit_id -}}</div>
+ <div class="row-label">Author</div>
+ <div class="row-value">
+ <span>{{- .commit_object.Author.Name -}}</span> <span>&lt;<a href="mailto:{{- .commit_object.Author.Email -}}">{{- .commit_object.Author.Email -}}</a>&gt;</span>
+ </div>
+ <div class="row-label">Author date</div>
+ <div class="row-value">{{- .commit_object.Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" -}}</div>
+ <div class="row-label">Committer</div>
+ <div class="row-value">
+ <span>{{- .commit_object.Committer.Name -}}</span> <span>&lt;<a href="mailto:{{- .commit_object.Committer.Email -}}">{{- .commit_object.Committer.Email -}}</a>&gt;</span>
+ </div>
+ <div class="row-label">Committer date</div>
+ <div class="row-value">{{- .commit_object.Committer.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" -}}</div>
+ <div class="row-label">Actions</div>
+ <div class="row-value">
+ <a href="{{- .commit_object.Hash -}}.patch">Get patch</a>
+ </div>
+ </section>
+ </div>
+ </div>
+
+ <div class="padding-wrapper scroll" id="this-commit-message">
+ <pre>{{- .commit_object.Message -}}</pre>
+ </div>
+ <div class="padding-wrapper">
+ {{- $parent_commit_hash := .parent_commit_hash -}}
+ {{- $commit_object := .commit_object -}}
+ {{- range .file_patches -}}
+ <div class="file-patch toggle-on-wrapper">
+ <input type="checkbox" id="toggle-{{- .From.Hash -}}{{- .To.Hash -}}" class="file-toggle toggle-on-toggle">
+ <label for="toggle-{{- .From.Hash -}}{{- .To.Hash -}}" class="file-header toggle-on-header">
+ <div>
+ {{- if eq .From.Path "" -}}
+ --- /dev/null
+ {{- else -}}
+ --- a/<a href="../tree/{{- .From.Path -}}?commit={{- $parent_commit_hash -}}">{{- .From.Path -}}</a> {{ .From.Mode -}}
+ {{- end -}}
+ <br />
+ {{- if eq .To.Path "" -}}
+ +++ /dev/null
+ {{- else -}}
+ +++ b/<a href="../tree/{{- .To.Path -}}?commit={{- $commit_object.Hash -}}">{{- .To.Path -}}</a> {{ .To.Mode -}}
+ {{- end -}}
+ </div>
+ </label>
+ <div class="file-content toggle-on-content scroll">
+ {{- range .Chunks -}}
+ {{- if eq .Operation 0 -}}
+ <pre class="chunk chunk-unchanged">{{ .Content }}</pre>
+ {{- else if eq .Operation 1 -}}
+ <pre class="chunk chunk-addition">{{ .Content }}</pre>
+ {{- else if eq .Operation 2 -}}
+ <pre class="chunk chunk-deletion">{{ .Content }}</pre>
+ {{- else -}}
+ <pre class="chunk chunk-unknown">{{ .Content }}</pre>
+ {{- end -}}
+ {{- end -}}
+ </div>
+ </div>
+ {{- end -}}
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/repo_contrib_index.tmpl b/forged/templates/repo_contrib_index.tmpl
new file mode 100644
index 0000000..172a079
--- /dev/null
+++ b/forged/templates/repo_contrib_index.tmpl
@@ -0,0 +1,82 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "repo_contrib_index" -}}
+{{- $root := . -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>Merge requests &ndash; {{ .repo_name }} &ndash; {{ template "group_path_plain" .group_path }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="repo-contrib-index">
+ {{- template "header" . -}}
+ <div class="repo-header">
+ <h2>{{- .repo_name -}}</h2>
+ <ul class="nav-tabs-standalone">
+ <li class="nav-item">
+ <a class="nav-link" href="../{{- template "ref_query" $root -}}">Summary</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tree/{{- template "ref_query" $root -}}">Tree</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../log/{{- template "ref_query" $root -}}">Log</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../branches/">Branches</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tags/">Tags</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link active" href="../contrib/">Merge requests</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../settings/">Settings</a>
+ </li>
+ </ul>
+ </div>
+ <div class="repo-header-extension">
+ <div class="repo-header-extension-content">
+ {{- .repo_description -}}
+ </div>
+ </div>
+ <div class="padding-wrapper">
+ <h2>How to submit a merge request</h2>
+ <pre>git clone {{ .ssh_clone_url }}
+cd {{ .repo_name }}
+git checkout -b contrib/name_of_your_contribution
+# edit and commit stuff
+git push -u origin HEAD</pre>
+ <p>Pushes that update branches in other namespaces, or pushes to existing contribution branches belonging to other SSH keys, will be automatically
+rejected, unless you are an authenticated maintainer. Otherwise, a merge request is automatically opened, and the maintainers are notified via IRC.</p>
+ <p>Alternatively, you may <a href="https://git-send-email.io">email patches</a> to <a href="mailto:{{ .repo_patch_mailing_list }}">{{ .repo_patch_mailing_list }}</a>.</p>
+ </div>
+ <div class="padding-wrapper">
+ <table id="recent-merge_requests" class="wide">
+ <thead>
+ <tr>
+ <th scope="col">ID</th>
+ <th scope="col">Title</th>
+ <th scope="col">Status</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{- range .merge_requests -}}
+ <tr>
+ <td class="merge_request-id">{{- .ID -}}</td>
+ <td class="merge_request-title"><a href="{{- .ID -}}/">{{- .Title -}}</a></td>
+ <td class="merge_request-status">{{- .Status -}}</td>
+ </tr>
+ {{- end -}}
+ </tbody>
+ </table>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/repo_contrib_one.tmpl b/forged/templates/repo_contrib_one.tmpl
new file mode 100644
index 0000000..a5f35d3
--- /dev/null
+++ b/forged/templates/repo_contrib_one.tmpl
@@ -0,0 +1,123 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "repo_contrib_one" -}}
+{{- $root := . -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>Merge requests &ndash; {{ .repo_name }} &ndash; {{ template "group_path_plain" .group_path }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="repo-contrib-one">
+ {{- template "header" . -}}
+ <div class="repo-header">
+ <h2>{{- .repo_name -}}</h2>
+ <ul class="nav-tabs-standalone">
+ <li class="nav-item">
+ <a class="nav-link" href="../{{- template "ref_query" $root -}}">Summary</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tree/{{- template "ref_query" $root -}}">Tree</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../log/{{- template "ref_query" $root -}}">Log</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../branches/">Branches</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tags/">Tags</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link active" href="../contrib/">Merge requests</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../settings/">Settings</a>
+ </li>
+ </ul>
+ </div>
+ <div class="repo-header-extension">
+ <div class="repo-header-extension-content">
+ {{- .repo_description -}}
+ </div>
+ </div>
+ <div class="padding-wrapper">
+ <table id="mr-info-table">
+ <thead>
+ <tr class="title-row">
+ <th colspan="2">Merge request info</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th scope="row">ID</th>
+ <td>{{- .mr_id -}}</td>
+ </tr>
+ <tr>
+ <th scope="row">Status</th>
+ <td>{{- .mr_status -}}</td>
+ </tr>
+ <tr>
+ <th scope="row">Title</th>
+ <td>{{- .mr_title -}}</td>
+ </tr>
+ <tr>
+ <th scope="row">Source ref</th>
+ <td>{{- .mr_source_ref -}}</td>
+ </tr>
+ <tr>
+ <th scope="row">Destination branch</th>
+ <td>{{- .mr_destination_branch -}}</td>
+ </tr>
+ <tr>
+ <th scope="row">Merge base</th>
+ <td>{{- .merge_base.Hash.String -}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="padding-wrapper">
+ {{- $merge_base := .merge_base -}}
+ {{- $source_commit := .source_commit -}}
+ {{- range .file_patches -}}
+ <div class="file-patch toggle-on-wrapper">
+ <input type="checkbox" id="toggle-{{- .From.Hash -}}{{- .To.Hash -}}" class="file-toggle toggle-on-toggle">
+ <label for="toggle-{{- .From.Hash -}}{{- .To.Hash -}}" class="file-header toggle-on-header">
+ <div>
+ {{- if eq .From.Path "" -}}
+ --- /dev/null
+ {{- else -}}
+ --- a/<a href="../../tree/{{- .From.Path -}}?commit={{- $merge_base.Hash -}}">{{- .From.Path -}}</a> {{ .From.Mode -}}
+ {{- end -}}
+ <br />
+ {{- if eq .To.Path "" -}}
+ +++ /dev/null
+ {{- else -}}
+ +++ b/<a href="../../tree/{{- .To.Path -}}?commit={{- $source_commit.Hash -}}">{{- .To.Path -}}</a> {{ .To.Mode -}}
+ {{- end -}}
+ </div>
+ </label>
+ <div class="file-content toggle-on-content scroll">
+ {{- range .Chunks -}}
+ {{- if eq .Operation 0 -}}
+ <pre class="chunk chunk-unchanged">{{ .Content }}</pre>
+ {{- else if eq .Operation 1 -}}
+ <pre class="chunk chunk-addition">{{ .Content }}</pre>
+ {{- else if eq .Operation 2 -}}
+ <pre class="chunk chunk-deletion">{{ .Content }}</pre>
+ {{- else -}}
+ <pre class="chunk chunk-unknown">{{ .Content }}</pre>
+ {{- end -}}
+ {{- end -}}
+ </div>
+ </div>
+ {{- end -}}
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/repo_index.tmpl b/forged/templates/repo_index.tmpl
new file mode 100644
index 0000000..d040f3a
--- /dev/null
+++ b/forged/templates/repo_index.tmpl
@@ -0,0 +1,94 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "repo_index" -}}
+{{- $root := . -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>{{ .repo_name }} &ndash; {{ template "group_path_plain" .group_path }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="repo-index">
+ {{- template "header" . -}}
+ <div class="repo-header">
+ <h2>{{- .repo_name -}}</h2>
+ <ul class="nav-tabs-standalone">
+ <li class="nav-item">
+ <a class="nav-link active" href="./{{- template "ref_query" $root -}}">Summary</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="tree/{{- template "ref_query" $root -}}">Tree</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="log/{{- template "ref_query" $root -}}">Log</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="branches/">Branches</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="tags/">Tags</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="contrib/">Merge requests</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="settings/">Settings</a>
+ </li>
+ </ul>
+ </div>
+ <div class="repo-header-extension">
+ <div class="repo-header-extension-content">
+ {{- .repo_description -}}
+ </div>
+ </div>
+ {{- if .notes -}}
+ <div id="notes">Notes</div>
+ <ul>
+ {{- range .notes -}}<li>{{- . -}}</li>{{- end -}}
+ </ul>
+ </div>
+ {{- end -}}
+ <p class="readingwidth"><code>{{- .ssh_clone_url -}}</code></p>
+ {{- if .ref_name -}}
+ <p class="readingwidth">
+ <strong>
+ Warning: Due to various recent migrations, viewing non-HEAD refs may be broken.
+ </strong>
+ </p>
+ {{- end -}}
+ {{- if .commits -}}
+ <div class="commit-list-small">
+ {{- range .commits -}}
+ <div class="event">
+ <div>
+ <a href="commit/{{- .Hash -}}" title="{{- .Hash -}}" rel="nofollow">
+ {{- .Hash | printf "%.8s" -}}
+ </a>
+ &nbsp;&mdash;&nbsp;<a href="mailto:{{- .Email -}}">{{- .Author -}}</a>
+ <small class="pull-right">
+ <span title="{{- .Date -}}">{{- .Date -}}</span>
+ </small>
+ </div>
+ <pre class="commit">{{- .Message | first_line -}}</pre>
+ </div>
+ {{- end -}}
+ {{- if dereference_error .commits_err -}}
+ <div class="commit-error">
+ Error while obtaining commit log: {{ .commits_err }}
+ </div>
+ {{- end -}}
+ </div>
+ {{- end -}}
+ {{- if .readme -}}
+ <div class="padding-wrapper" id="readme">
+ {{- .readme -}}
+ </div>
+ {{- end -}}
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/repo_log.tmpl b/forged/templates/repo_log.tmpl
new file mode 100644
index 0000000..2262902
--- /dev/null
+++ b/forged/templates/repo_log.tmpl
@@ -0,0 +1,90 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "repo_log" -}}
+{{- $root := . -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>Log &ndash; {{ .repo_name }} &ndash; {{ template "group_path_plain" .group_path }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="repo-log">
+ {{- template "header" . -}}
+ <div class="repo-header">
+ <h2>{{- .repo_name -}}</h2>
+ <ul class="nav-tabs-standalone">
+ <li class="nav-item">
+ <a class="nav-link" href="../{{- template "ref_query" $root -}}">Summary</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tree/{{- template "ref_query" $root -}}">Tree</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link active" href="../log/{{- template "ref_query" $root -}}">Log</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../branches/">Branches</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../tags/">Tags</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../contrib/">Merge requests</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="../settings/">Settings</a>
+ </li>
+ </ul>
+ </div>
+ <div class="repo-header-extension">
+ <div class="repo-header-extension-content">
+ {{- .repo_description -}}
+ </div>
+ </div>
+ <div class="scroll">
+ {{- if .ref_name -}}
+ <p>
+ <strong>
+ Warning: Due to various recent migrations, viewing non-HEAD refs may be broken.
+ </strong>
+ </p>
+ {{- end -}}
+ <table id="commits" class="wide">
+ <thead>
+ <tr class="title-row">
+ <th colspan="4">Commits {{ if .ref_name }} on {{ .ref_name }}{{ end -}}</th>
+ </tr>
+ <tr>
+ <th scope="col">ID</th>
+ <th scope="col">Title</th>
+ <th scope="col">Author</th>
+ <th scope="col">Author date</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{- range .commits -}}
+ <tr>
+ <td class="commit-id"><a href="../commit/{{- .Hash -}}">{{- .Hash -}}</a></td>
+ <td class="commit-title">{{- .Message | first_line -}}</td>
+ <td class="commit-author">
+ <a class="email-name" href="mailto:{{- .Author.Email -}}">{{- .Author.Name -}}</a>
+ </td>
+ <td class="commit-time">
+ {{- .Author.When.Format "2006-01-02 15:04:05 -0700" -}}
+ </td>
+ </tr>
+ {{- end -}}
+ {{- if dereference_error .commits_err -}}
+ Error while obtaining commit log: {{ .commits_err }}
+ {{- end -}}
+ </tbody>
+ </table>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/repo_raw_dir.tmpl b/forged/templates/repo_raw_dir.tmpl
new file mode 100644
index 0000000..a33da4a
--- /dev/null
+++ b/forged/templates/repo_raw_dir.tmpl
@@ -0,0 +1,88 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "repo_raw_dir" -}}
+{{- $root := . -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>/{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }} &ndash; {{ .repo_name }} &ndash; {{ template "group_path_plain" .group_path }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="repo-raw-dir">
+ {{- template "header" . -}}
+ <div class="repo-header">
+ <h2>{{- .repo_name -}}</h2>
+ <ul class="nav-tabs-standalone">
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}{{- template "ref_query" $root -}}">Summary</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link active" href="{{- .repo_url_root -}}tree/{{- template "ref_query" $root -}}">Tree</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}log/{{- template "ref_query" $root -}}">Log</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}branches/">Branches</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}tags/">Tags</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}contrib/">Merge requests</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}settings/">Settings</a>
+ </li>
+ </ul>
+ </div>
+ <div class="repo-header-extension">
+ <div class="repo-header-extension-content">
+ {{- .repo_description -}}
+ </div>
+ </div>
+ <div class="padding-wrapper scroll">
+ {{- if .ref_name -}}
+ <p>
+ <strong>
+ Warning: Due to various recent migrations, viewing non-HEAD refs may be broken.
+ </strong>
+ </p>
+ {{- end -}}
+ <table id="file-tree" class="wide">
+ <thead>
+ <tr class="title-row">
+ <th colspan="3">
+ (Raw) /{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }}{{ if .ref_name }} on {{ .ref_name }}{{ end -}}
+ </th>
+ </tr>
+ <tr>
+ <th scope="col">Mode</th>
+ <th scope="col">Filename</th>
+ <th scope="col">Size</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{- $path_spec := .path_spec -}}
+ {{- range .files -}}
+ <tr>
+ <td class="file-mode">{{- .Mode -}}</td>
+ <td class="file-name"><a href="{{- .Name -}}{{- if not .IsFile -}}/{{- end -}}{{- template "ref_query" $root -}}">{{- .Name -}}</a>{{- if not .IsFile -}}/{{- end -}}</td>
+ <td class="file-size">{{- .Size -}}</td>
+ </tr>
+ {{- end -}}
+ </tbody>
+ </table>
+ </div>
+ <div class="padding-wrapper">
+ <div id="refs">
+ </div>
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/repo_tree_dir.tmpl b/forged/templates/repo_tree_dir.tmpl
new file mode 100644
index 0000000..fc06646
--- /dev/null
+++ b/forged/templates/repo_tree_dir.tmpl
@@ -0,0 +1,93 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "repo_tree_dir" -}}
+{{- $root := . -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <title>/{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }} &ndash; {{ .repo_name }} &ndash; {{ template "group_path_plain" .group_path }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="repo-tree-dir">
+ {{- template "header" . -}}
+ <div class="repo-header">
+ <h2>{{- .repo_name -}}</h2>
+ <ul class="nav-tabs-standalone">
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}{{- template "ref_query" $root -}}">Summary</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link active" href="{{- .repo_url_root -}}tree/{{- template "ref_query" $root -}}">Tree</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}log/{{- template "ref_query" $root -}}">Log</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}branches/">Branches</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}tags/">Tags</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}contrib/">Merge requests</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}settings/">Settings</a>
+ </li>
+ </ul>
+ </div>
+ <div class="repo-header-extension">
+ <div class="repo-header-extension-content">
+ {{- .repo_description -}}
+ </div>
+ </div>
+ <div class="padding-wrapper scroll">
+ {{- if .ref_name -}}
+ <p>
+ <strong>
+ Warning: Due to various recent migrations, viewing non-HEAD refs may be broken.
+ </strong>
+ </p>
+ {{- end -}}
+ <table id="file-tree" class="wide">
+ <thead>
+ <tr class="title-row">
+ <th colspan="3">
+ /{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }}{{ if .ref_name }} on {{ .ref_name }}{{ end -}}
+ </th>
+ <tr>
+ <th scope="col">Mode</th>
+ <th scope="col">Filename</th>
+ <th scope="col">Size</th>
+ </tr>
+ </tr>
+ </thead>
+ <tbody>
+ {{- $path_spec := .path_spec -}}
+ {{- range .files -}}
+ <tr>
+ <td class="file-mode">{{- .Mode -}}</td>
+ <td class="file-name"><a href="{{- .Name -}}{{- if not .IsFile -}}/{{- end -}}{{- template "ref_query" $root -}}">{{- .Name -}}</a>{{- if not .IsFile -}}/{{- end -}}</td>
+ <td class="file-size">{{- .Size -}}</td>
+ </tr>
+ {{- end -}}
+ </tbody>
+ </table>
+ </div>
+ <div class="padding-wrapper">
+ <div id="refs">
+ </div>
+ </div>
+ {{- if .readme -}}
+ <div class="padding-wrapper" id="readme">
+ {{- .readme -}}
+ </div>
+ {{- end -}}
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}
diff --git a/forged/templates/repo_tree_file.tmpl b/forged/templates/repo_tree_file.tmpl
new file mode 100644
index 0000000..76404a9
--- /dev/null
+++ b/forged/templates/repo_tree_file.tmpl
@@ -0,0 +1,65 @@
+{{/*
+ SPDX-License-Identifier: AGPL-3.0-only
+ SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+*/}}
+{{- define "repo_tree_file" -}}
+{{- $root := . -}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ {{- template "head_common" . -}}
+ <link rel="stylesheet" href="/-/static/chroma.css" />
+ <title>/{{ .path_spec }} &ndash; {{ .repo_name }} &ndash; {{ template "group_path_plain" .group_path }} &ndash; {{ .global.forge_title -}}</title>
+ </head>
+ <body class="repo-tree-file">
+ {{- template "header" . -}}
+ <div class="repo-header">
+ <h2>{{- .repo_name -}}</h2>
+ <ul class="nav-tabs-standalone">
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}{{- template "ref_query" $root -}}">Summary</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link active" href="{{- .repo_url_root -}}tree/{{- template "ref_query" $root -}}">Tree</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}log/{{- template "ref_query" $root -}}">Log</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}branches/">Branches</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}tags/">Tags</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}contrib/">Merge requests</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{- .repo_url_root -}}settings/">Settings</a>
+ </li>
+ </ul>
+ </div>
+ <div class="repo-header-extension">
+ <div class="repo-header-extension-content">
+ {{- .repo_description -}}
+ </div>
+ </div>
+ <div class="padding">
+ {{- if .ref_name -}}
+ <p>
+ <strong>
+ Warning: Due to various recent migrations, viewing non-HEAD refs may be broken.
+ </strong>
+ </p>
+ {{- end -}}
+ <p>
+ /{{ .path_spec }} (<a href="/{{ template "group_path_plain" .group_path }}/-/repos/{{ .repo_name }}/raw/{{ .path_spec }}{{- template "ref_query" $root -}}">raw</a>)
+ </p>
+ {{- .file_contents -}}
+ </div>
+ <footer>
+ {{- template "footer" . -}}
+ </footer>
+ </body>
+</html>
+{{- end -}}