From bb6bc2387d58c737dbf92e73f8544bf39319dbd8 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 1 Aug 2026 00:00:00 +0000 Subject: Enter git-forge A small self-hosted git forge: HTTP push and pull, cgit browsing, local and OIDC accounts, TOTP, scoped push tokens, per-repo permissions, issues, a wiki, and outbound mail. It does not reimplement git. git-http-backend and cgit run as CGI behind its own listener, which authorises every request before exec'ing either. One binary, one data directory, one listener: no database, no JavaScript, and state is JSON in a git repo with a working tree. --- templates/admin.html | 236 +++++++++++++++++++++++++++++++++++++++++++ templates/base.html | 23 +++++ templates/error.html | 5 + templates/home.html | 16 +++ templates/issue-new.html | 27 +++++ templates/issue.html | 62 ++++++++++++ templates/issues.html | 34 +++++++ templates/login.html | 33 ++++++ templates/nav.html | 112 ++++++++++++++++++++ templates/repo-settings.html | 122 ++++++++++++++++++++++ templates/reset.html | 23 +++++ templates/set-password.html | 20 ++++ templates/settings.html | 126 +++++++++++++++++++++++ templates/setup.html | 33 ++++++ templates/totp-enroll.html | 96 ++++++++++++++++++ templates/totp.html | 16 +++ templates/wiki-edit.html | 27 +++++ templates/wiki.html | 37 +++++++ 18 files changed, 1048 insertions(+) create mode 100644 templates/admin.html create mode 100644 templates/base.html create mode 100644 templates/error.html create mode 100644 templates/home.html create mode 100644 templates/issue-new.html create mode 100644 templates/issue.html create mode 100644 templates/issues.html create mode 100644 templates/login.html create mode 100644 templates/nav.html create mode 100644 templates/repo-settings.html create mode 100644 templates/reset.html create mode 100644 templates/set-password.html create mode 100644 templates/settings.html create mode 100644 templates/setup.html create mode 100644 templates/totp-enroll.html create mode 100644 templates/totp.html create mode 100644 templates/wiki-edit.html create mode 100644 templates/wiki.html (limited to 'templates') diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..871b061 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,236 @@ +{{define "title"}}Admin - {{.Site}}{{end}} +{{define "main"}} +

This instance

+
+ +

+ + +

+

+ + +

+
+Logo +

+{{if .Data.Site.Logo.Name}} +Currently {{.Data.Site.Logo.Name}}. +{{else}} +Currently cgit's own. +{{end}} +

+

+ + + +Drawn in the header at 96 pixels at most, and left at its own size below that. + +

+

+ +

+
+ +
+Favicon +

+{{if .Data.Site.Icon.Name}} +Currently {{.Data.Site.Icon.Name}}. +{{else}} +Currently cgit's own. +{{end}} +

+

+ + + +Served at /favicon.ico, whatever type it is. + +

+

+ +

+
+

+Both take a png, jpeg, gif, webp, or an icon, up to 512 KiB, and are kept in +the config repository, so they are in the backup and in the history with +everything else. SVG is refused: it is XML that can carry script, and this is +served back to browsers. +

+

+
+

+Everything else in forge.json decides how the process starts, and is left to +the file: a listener or a base url typed into a browser is a way to lock +everybody out. +

+ +

Repositories

+ + + + + +{{range .Data.Repos}} + + + + + + + +{{else}} + +{{end}} +
NameVisibilityIssuesWiki
{{.Path}}{{.Repo.Visibility}}{{.Repo.Issues}}{{.Repo.Wiki}} +{{if not .Registered}} +
+ + + +
+{{end}} +
None yet.
+

+A repository with an adopt button has a directory but no entry in repos.json. +It is served private with issues and wiki off until it is adopted or deleted. +

+ +

Create a repository

+
+ +

+ + +

+

+ + +

+

+ + +

+{{template "knobs" .Data.New}} +

+
+ +

Accounts

+{{with .Data.Link}} +
+One time link for {{$.Data.For}}, valid for two days. It both sets the password +and confirms the address, and it dies the moment it is used. +
+
{{.}}
+{{end}} + + + + + + +{{range $name, $u := .Data.Users}} + + + + + + + +{{end}} +
AccountOriginEmailAdminSecond factor
{{$name}}{{$u.Origin}}{{with $u.Email}}{{.}}{{if not $u.EmailConfirmed}} unconfirmed{{end}}{{end}}{{if $u.Admin}}yes{{end}}{{if $u.TOTP}}yes{{else}}none{{end}}
+ +

Create an account

+

+Creating an account issues a one time set-password link rather than a password, +so an admin never handles one. That link is a way in that outlives this session, +so this asks for your own authenticator code. +

+
+ +

+ + +

+

+ + +

+

+ + +

+

+The admin flag is not set here. Handing it out is its own act, below. +

+

+
+ +
+

Changes that hand out power, or cannot be undone

+

+Each of these asks for your own authenticator code as well, so that a session +somebody else is sitting in front of cannot use them. +

+ +

Admin flag

+
+ +

+ + +

+

+ +

+

+ + +

+

+
+ +

Clear an authenticator

+

+The way back in for somebody who has lost both their authenticator and their +recovery codes. It signs every session for that account out. +

+
+ +

+ + +

+

+ + +

+

+
+ +

Delete an account

+

This also drops every grant that pointed at it.

+
+ +

+ + +

+

+ + +

+

+
+
+{{end}} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..0f1751f --- /dev/null +++ b/templates/base.html @@ -0,0 +1,23 @@ + + + + + +{{block "title" .}}{{.Site}}{{end}} + + + + +
+{{template "header" .}} +{{template "tabrow" .}} +
+{{if .Error}}
{{.Error}}
{{end}} +{{if .Notice}}
{{.Notice}}
{{end}} +{{block "main" .}}{{end}} +
+{{/* cgit's footer names cgit, so this one names the forge. */}} + +
+ + diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..9c39077 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,5 @@ +{{define "title"}}{{.Data.Status}} {{.Data.Text}} - {{.Site}}{{end}} +{{define "main"}} +

{{.Data.Status}} {{.Data.Text}}

+

Back to the index

+{{end}} diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..a7460f8 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,16 @@ +{{define "title"}}{{.Site}}{{end}} +{{define "main"}} + + +{{range .Data.Repos}} + + + + +{{else}} + +{{end}} +
NameDescription
{{.Path}}{{if eq .Repo.Visibility "private"}} private{{end}}{{.Repo.Desc}}
+{{if $.User}}Nothing you can read yet.{{else}}Nothing public here. Sign in to see the rest.{{end}} +
+{{end}} diff --git a/templates/issue-new.html b/templates/issue-new.html new file mode 100644 index 0000000..ed102aa --- /dev/null +++ b/templates/issue-new.html @@ -0,0 +1,27 @@ +{{define "title"}}New issue, {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +

Open an issue

+
+ +

+ + +

+

+ + +

+{{with .Data.Labels}} +
+Labels +{{range .}} + +{{end}} +
+{{end}} +

+
+{{end}} diff --git a/templates/issue.html b/templates/issue.html new file mode 100644 index 0000000..e5135a0 --- /dev/null +++ b/templates/issue.html @@ -0,0 +1,62 @@ +{{define "title"}}{{.Data.Issue.Title}} - {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +{{$issue := .Data.Issue}} +

{{$issue.Title}} #{{$issue.ID}}

+

+{{$issue.State}} +opened by {{template "who" $issue.Author}} +on {{$issue.Created.Format "2006-01-02 15:04"}} UTC +{{range $issue.Labels}}{{.}}{{end}} +

+ +{{if .Data.Triage}} +
+
+ + +
+{{with .Data.Labels}} +
+ +{{range .}} + +{{end}} + +
+{{end}} +
+{{end}} + +
+ +
{{.Data.Rendered}}
+
+ +{{range .Data.Comments}} +
+ +
{{.Rendered}}
+
+{{end}} + +{{if .Data.Can}} +

Comment

+
+ +

+ + +

+

+
+{{end}} +{{end}} diff --git a/templates/issues.html b/templates/issues.html new file mode 100644 index 0000000..e0907ce --- /dev/null +++ b/templates/issues.html @@ -0,0 +1,34 @@ +{{define "title"}}Issues, {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +
+
+ + + +
+ +{{if eq .Data.State "open"}}open{{else}}open{{end}} +{{if eq .Data.State "closed"}}closed{{else}}closed{{end}} +history +{{if .Data.Can}}open an issue{{end}} + +
+ + + + + + +{{range .Data.Issues}} + + + + + + + +{{else}} + +{{end}} +
#TitleLabelsAuthorUpdated
{{.ID}}{{.Title}}{{range .Labels}}{{.}} {{end}}{{template "who" .Author}}{{.Updated.Format "2006-01-02"}}
Nothing here.
+{{end}} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..eb9dcd9 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,33 @@ +{{define "title"}}Sign in - {{.Site}}{{end}} +{{define "main"}} +

Sign in

+
+ +

+ + +

+

+ + +

+

+
+{{if .MailOff}} +

+Mail is turned off on this instance, so there is no password reset. An admin +can issue you a one time link instead. +

+{{else}} +

Forgotten your password?

+{{end}} + +{{with .Data.Providers}} +

Or sign in through

+ +{{end}} +{{end}} diff --git a/templates/nav.html b/templates/nav.html new file mode 100644 index 0000000..82f0714 --- /dev/null +++ b/templates/nav.html @@ -0,0 +1,112 @@ +{{/* + cgit's own header markup, rendered in place of the one noheader=1 turns off, + so that both kinds of page carry the same header and it can name the account. +*/}} +{{define "header"}} + + + + + + + + +{{end}} + +{{/* The forge's sections, in cgit's tab markup. */}} +{{define "tabs"}}{{with .Repo}}{{if .Issues}}issues{{end}}{{if .Wiki}}wiki{{end}}{{if .Settings}}settings{{end}}{{end}}{{end}} + +{{/* + cgit's row copied, for the forge's own pages where cgit renders nothing: the + same tabs in the same order, and the same search cell. +*/}} +{{define "tabrow"}}{{with .Repo}} + +
+aboutsummaryrefslogtreecommitdiff{{template "tabs" $}}
+ + + +
+
+{{end}}{{end}} + +{{/* + In the layout because every page is parsed as its own set, and a partial + defined in one page is invisible to the next. +*/}} +{{define "who"}}{{if .}} {{.}}{{else}}nobody{{end}}{{end}} + +{{/* + One fragment for both the create form and the settings form, so that the two + cannot drift apart on what members and users mean. +*/}} +{{define "knobs"}} +
+Who may read it + + +
+ +
+Who may file issues + + + +
+ +
+Who may edit the wiki + + + +
+

+Reading is visibility's job alone. The other two say who may write, so a +repository can take bug reports and documentation fixes from people who will +never push code. users on a private repository is refused rather than coerced. +

+{{end}} diff --git a/templates/repo-settings.html b/templates/repo-settings.html new file mode 100644 index 0000000..d90f744 --- /dev/null +++ b/templates/repo-settings.html @@ -0,0 +1,122 @@ +{{define "title"}}{{.Data.Path}} settings - {{.Site}}{{end}} +{{define "main"}} +{{$repo := .Data.Repo}} +

Settings

+ +{{if not .Data.Enrolled}} +
+This account has no authenticator, so it cannot change who may read or write +this repository, and cannot change grants. +Enrol one and come back. +
+{{end}} + +
+ +

+ + +

+{{template "knobs" $repo}} +

+ + +

+

+ + + +Needed only to move one of the three above: who may read or write outlives this +session, the way a grant does. A description or a label does not. + +

+

+
+ +

Grants

+

+read clones and browses a private repository. write adds pushing to it. admin +adds this page. A global admin holds admin everywhere without appearing here. +A push token can be minted for any repository an account can reach, and carries +the access that account already has there and nothing more. +

+

+Access handed out here outlives the session that hands it out, so changing a +grant asks for your own authenticator code as well. +

+ + +{{range $account, $level := $repo.Grants}} + + + + + +{{else}} + +{{end}} +
AccountLevel
{{template "who" $account}}{{$level}} +{{if $.Data.Enrolled}} +
+ + + + +
+{{end}} +
Nobody, so only global admins can reach it.
+ +{{if .Data.Enrolled}} +
+ +

+ + +

+
+Level + + + +
+

+ + +

+

+
+{{end}} + +{{if .Data.Delete}} +
+

Delete this repository

+

+This removes the repository, its issues, and its wiki together. There is no +undo and the history goes with it. Type {{.Data.Path}} to confirm, +and your own authenticator code, so that a session somebody else is sitting in +front of cannot do it. +

+
+ +

+ + +

+

+ + +

+

+
+
+{{end}} +{{end}} diff --git a/templates/reset.html b/templates/reset.html new file mode 100644 index 0000000..9025f2e --- /dev/null +++ b/templates/reset.html @@ -0,0 +1,23 @@ +{{define "title"}}Forgotten password - {{.Site}}{{end}} +{{define "main"}} +

Forgotten password

+{{if .MailOff}} +
+Mail is turned off on this instance, so no link can be sent. Ask an admin for +a one time link. +
+{{end}} +

+This mails a one time link to the address on file for the account. The answer +below is the same whether or not the account exists, so that this page cannot +be used to find out which names are taken. +

+
+ +

+ + +

+

+
+{{end}} diff --git a/templates/set-password.html b/templates/set-password.html new file mode 100644 index 0000000..6a38413 --- /dev/null +++ b/templates/set-password.html @@ -0,0 +1,20 @@ +{{define "title"}}Set a password - {{.Site}}{{end}} +{{define "main"}} +

Set a password for {{.Data.Name}}

+

This link works once, and setting the password is what spends it.

+
+ + + + +

+ + +

+

+ + +

+

+
+{{end}} diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 0000000..8b2f0f1 --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,126 @@ +{{define "title"}}Settings - {{.Site}}{{end}} +{{define "main"}} +

Account

+

+Signed in as {{template "who" .User}}{{if .Admin}}, a global admin{{end}}. +

+ +

Email and notifications

+{{if .MailOff}} +

+Mail is turned off on this instance, so nothing is sent. The address below is +still what commits made through the web are attributed to. +

+{{else}} +

+An address receives nothing until a link sent to it has been followed. Notices +about your own account ignore the toggle below, because they are security facts +rather than activity. +

+{{end}} +
+ +

+ + +{{if .Data.Email}}{{if not .Data.EmailConfirmed}}unconfirmed{{end}}{{end}} +

+

+ +

+

+
+ +{{if .Data.Local}} +

Change password

+
+ +

+ + +

+

+ + +

+

+ + +

+

+
+{{else}} +

Password

+

This account signs in through an external provider, so there is no password here.

+{{end}} + +

Push tokens

+

+Git over HTTP takes a token, never your password. A token is scoped to one +repository and carries the access you already have there: clone if you can read +it, push if you can write it, the wiki if you may edit it. It carries no second +factor, and it can be revoked on its own without touching anything else. +

+
git clone {{.Base}}/section/repo.git
+Username: {{.User}}
+Password: the token
+{{with .Data.Secret}} +
Token for {{$.User}}, shown once:
+
{{.}}
+{{end}} + + + + + +{{range .Data.Tokens}} + + + + + + + +{{else}} + +{{end}} +
IdRepositoryMadeLast used
{{.ID}}{{.Repo}}{{if .Created}}{{.Created}}{{else}}unknown{{end}}{{if .Used}}{{.Used}}{{else}}never{{end}} +
+ + + +
+
None. Mint one below to push over HTTP.
+

+Dates are days, not times: a write on every request git makes would be a write +on every request git makes. A token that has never been used, or was last used +a long time ago, is one worth revoking. +

+
+ +

+ + +

+

+
+ +

Authenticator

+

+Second factor. An account with the global admin +flag has to have one before it can administer anything. +

+ +

Sessions

+

+Signing out everywhere invalidates every session cookie ever issued for this +account, including this browser's. +

+
+ +

+
+{{end}} diff --git a/templates/setup.html b/templates/setup.html new file mode 100644 index 0000000..9089bd3 --- /dev/null +++ b/templates/setup.html @@ -0,0 +1,33 @@ +{{define "title"}}Set up - {{.Site}}{{end}} +{{define "main"}} +

Set up {{.Site}}

+

+This creates the first account and gives it the global admin flag. The setup +token was printed to the log when git-forge started, and this page is gone once +an account exists. +

+
+ +

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+
+{{end}} diff --git a/templates/totp-enroll.html b/templates/totp-enroll.html new file mode 100644 index 0000000..da3d301 --- /dev/null +++ b/templates/totp-enroll.html @@ -0,0 +1,96 @@ +{{define "title"}}Authenticator - {{.Site}}{{end}} +{{define "main"}} +

Authenticator

+ +{{with .Data.Codes}} +

Recovery codes

+

Each of these works once, in place of a code. They are not shown again.

+
{{range .}}{{.}}
+{{end}}
+{{end}} + +{{if .Data.Enrolled}} +

+This account has an authenticator, and {{.Data.Left}} recovery codes left. +

+ +

New recovery codes

+

Making new ones stops the old ones working.

+
+ +{{if .Data.Local}} +

+ + +

+{{else}} +

+ + +

+{{end}} +

+
+ +

Remove

+{{if .Data.Required}} +

+This account holds the global admin flag, so it must keep an authenticator. +Another admin has to take the flag away first. +

+{{else}} +

Removing it signs out every session for this account.

+
+ +{{if .Data.Local}} +

+ + +

+{{else}} +

+ + +

+{{end}} +

+
+{{end}} + +{{else}} +

+Type this secret into an authenticator, or give it the URI. There is no QR +code: it would be useless in a terminal browser. +

+
{{.Data.Secret}}
+
+{{.Data.URI}}
+

+Then prove it took, which is what enrols it. Enrolling signs out every other +session. +{{if .Data.Local}} +It takes the password as well: a stolen cookie must not be able to put a second +factor on this account and lock its owner out. +{{else}} +This account signs in through an external provider and has no password here, so +enrolling rests on this session alone. The address on file is told, and an admin +can clear an authenticator you did not enrol. +{{end}} +

+
+ + +

+ + +

+{{if .Data.Local}} +

+ + +

+{{end}} +

+
+{{end}} +{{end}} diff --git a/templates/totp.html b/templates/totp.html new file mode 100644 index 0000000..329d6cc --- /dev/null +++ b/templates/totp.html @@ -0,0 +1,16 @@ +{{define "title"}}Authenticator code - {{.Site}}{{end}} +{{define "main"}} +

Authenticator code

+

The password was right. Now the six digit code, or one recovery code.

+
+ + + + +

+ + +

+

+
+{{end}} diff --git a/templates/wiki-edit.html b/templates/wiki-edit.html new file mode 100644 index 0000000..ed896d2 --- /dev/null +++ b/templates/wiki-edit.html @@ -0,0 +1,27 @@ +{{define "title"}}Edit {{.Data.Page}} - {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +

Edit wiki{{with .Data.Page}} {{.}}{{end}}

+ +{{with .Data.Preview}} +

Preview

+
{{.}}
+
+{{end}} + +
+ + +

+ + +

+

+ + +

+
+

+Links between pages are plain relative markdown links, and a subdirectory's +index.md is served at the subdirectory itself. +

+{{end}} diff --git a/templates/wiki.html b/templates/wiki.html new file mode 100644 index 0000000..36305c6 --- /dev/null +++ b/templates/wiki.html @@ -0,0 +1,37 @@ +{{define "title"}}Wiki {{.Data.Page}} - {{.Data.Path}} - {{.Site}}{{end}} +{{define "main"}} +

Wiki{{with .Data.Page}} {{.}}{{end}}

+ +{{with .Data.Pages}} +

+Pages: +{{range .}}{{if .}}{{.}}{{else}}index{{end}} +{{end}} +

+{{end}} + +{{if .Data.Can}} +
+

+ + + +

+
+{{end}} + +{{if .Data.Empty}} +

This wiki has no pages yet.

+{{if .Data.Can}}

Write the first one.

{{end}} +{{else if .Data.Missing}} +

There is no page here yet.

+{{if .Data.Can}}

Write it.

{{end}} +{{else}} +
{{.Data.Rendered}}
+

+{{if .Data.Can}}Edit{{end}} +History +

+{{end}} + +{{end}} -- cgit v1.2.3