Lindenii Project Forge
Remove man pages They're better documented on the Web
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> # # TODO: This Makefile utilizes a lot of GNU extensions. Some of them are # unfortunately difficult to avoid as POSIX Make's pattern rules are not # sufficiently expressive. This needs to be fixed sometime (or we might move to # some other build system). # .PHONY: clean CFLAGS = -Wall -Wextra -pedantic -std=c99 -D_GNU_SOURCE MAN_PAGES = lindenii-forge.5 lindenii-forge-hookc.1 lindenii-forge.1 lindenii-forge-mail.5 VERSION = $(shell git describe --tags --always --dirty) SOURCE_FILES = $(shell git ls-files)
forge: source.tar.gz hookc/hookc git2d/git2d $(MAN_PAGES:%=man/%.html) $(MAN_PAGES:%=man/%.txt) $(SOURCE_FILES)
forge: source.tar.gz hookc/hookc git2d/git2d $(SOURCE_FILES)
CGO_ENABLED=0 go build -o forge -ldflags '-extldflags "-f no-PIC -static" -X "main.VERSION=$(VERSION)"' -tags 'osusergo netgo static_build'
man/%.html: man/% mandoc -Thtml -O style=./mandoc.css $< > $@ man/%.txt: man/% utils/colb mandoc $< | ./utils/colb > $@
utils/colb: hookc/hookc: git2d/git2d: git2d/*.c $(CC) $(CFLAGS) -o git2d/git2d $^ $(shell pkg-config --cflags --libs libgit2) -lpthread clean:
rm -rf forge vendor man/*.html man/*.txt utils/colb hookc/hookc git2d/git2d source.tar.gz */*.o
rm -rf forge vendor utils/colb hookc/hookc git2d/git2d source.tar.gz */*.o
source.tar.gz: $(SOURCE_FILES) rm -f source.tar.gz go mod vendor git ls-files -z | xargs -0 tar -czf source.tar.gz vendor
# Lindenii Forge **Work in progress.** Lindenii Forge aims to be an uncomplicated yet featured software forge, primarily designed for self-hosting by small organizations and individuals. * [Upstream source repository](https://forge.lindenii.runxiyu.org/forge/-/repos/server/) ([backup](https://git.lindenii.runxiyu.org/forge.git/)) * [Website and documentation](https://lindenii.runxiyu.org/forge/)
* [Manual pages](https://forge.lindenii.runxiyu.org/-/man/)
* [Temporary issue tracker](https://todo.sr.ht/~runxiyu/forge) * IRC [`#lindenii`](https://webirc.runxiyu.org/kiwiirc/#lindenii) on [irc.runxiyu.org](https://irc.runxiyu.org)\ and [`#lindenii`](https://web.libera.chat/#lindenii) on [Libera.Chat](https://libera.chat) ## Implemented features * Umambiguously parsable URL * Groups and subgroups * Repo hosting * Push to `contrib/` branches to automatically create merge requests * Basic federated authentication * Converting mailed patches to branches ## Planned features * Further Integration with mailing list workflows * Ticket trackers and discussions * Web interface * Email integration with IMAP archives * SSH API * Email access ## License We are currently using the [GNU Affero General Public License version 3](https://www.gnu.org/licenses/agpl-3.0.html). The forge software serves its own source at `/-/source/`. ## Contribute Please submit patches by pushing to `contrib/...` in the official repo. Alternatively, send email to [`forge/-/repos/server@forge.lindenii.runxiyu.org`](mailto:forge%2F-%2Frepos%2Fserver@forge.lindenii.runxiyu.org). Note that emailing patches is still experimental. ## Mirrors We have several repo mirrors: * [Official repo on our own instance of Lindenii Forge](https://forge.lindenii.runxiyu.org/forge/-/repos/server/) * [The Lindenii Project's backup cgit](https://git.lindenii.runxiyu.org/forge.git/) * [SourceHut](https://git.sr.ht/~runxiyu/forge/) * [Codeberg](https://codeberg.org/lindenii/forge/) * [GitHub](https://github.com/runxiyu/forge/)
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "errors" "net/http" "net/url" "strconv" "strings" "github.com/jackc/pgx/v5" "go.lindenii.runxiyu.org/lindenii-common/clog" ) type forgeHTTPRouter struct{} // ServeHTTP handles all incoming HTTP requests and routes them to the correct // location. // // TODO: This function is way too large. func (router *forgeHTTPRouter) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var remoteAddr string if config.HTTP.ReverseProxy { remoteAddrs, ok := request.Header["X-Forwarded-For"] if ok && len(remoteAddrs) == 1 { remoteAddr = remoteAddrs[0] } else { remoteAddr = request.RemoteAddr } } else { remoteAddr = request.RemoteAddr } clog.Info("Incoming HTTP: " + remoteAddr + " " + request.Method + " " + request.RequestURI) var segments []string var err error var sepIndex int params := make(map[string]any) if segments, _, err = parseReqURI(request.RequestURI); err != nil { errorPage400(writer, params, "Error parsing request URI: "+err.Error()) return } dirMode := false if segments[len(segments)-1] == "" { dirMode = true segments = segments[:len(segments)-1] } params["url_segments"] = segments params["dir_mode"] = dirMode params["global"] = globalData var userID int // 0 for none userID, params["username"], err = getUserFromRequest(request) params["user_id"] = userID if err != nil && !errors.Is(err, http.ErrNoCookie) && !errors.Is(err, pgx.ErrNoRows) { errorPage500(writer, params, "Error getting user info from request: "+err.Error()) return } if userID == 0 { params["user_id_string"] = "" } else { params["user_id_string"] = strconv.Itoa(userID) } for _, v := range segments { if strings.Contains(v, ":") { errorPage400Colon(writer, params) return } } if len(segments) == 0 { httpHandleIndex(writer, request, params) return } if segments[0] == "-" { if len(segments) < 2 { errorPage404(writer, params) return } else if len(segments) == 2 && redirectDir(writer, request) { return } switch segments[1] {
case "man": manHandler.ServeHTTP(writer, request) return
case "static": staticHandler.ServeHTTP(writer, request) return case "source": sourceHandler.ServeHTTP(writer, request) return } } if segments[0] == "-" { switch segments[1] { case "login": httpHandleLogin(writer, request, params) return case "users": httpHandleUsers(writer, request, params) return case "gc": httpHandleGC(writer, request, params) return default: errorPage404(writer, params) return } } sepIndex = -1 for i, part := range segments { if part == "-" { sepIndex = i break } } params["separator_index"] = sepIndex var groupPath []string var moduleType string var moduleName string if sepIndex > 0 { groupPath = segments[:sepIndex] } else { groupPath = segments } params["group_path"] = groupPath switch { case sepIndex == -1: if redirectDir(writer, request) { return } httpHandleGroupIndex(writer, request, params) case len(segments) == sepIndex+1: errorPage404(writer, params) return case len(segments) == sepIndex+2: errorPage404(writer, params) return default: moduleType = segments[sepIndex+1] moduleName = segments[sepIndex+2] switch moduleType { case "repos": params["repo_name"] = moduleName if len(segments) > sepIndex+3 { switch segments[sepIndex+3] { case "info": if err = httpHandleRepoInfo(writer, request, params); err != nil { errorPage500(writer, params, err.Error()) } return case "git-upload-pack": if err = httpHandleUploadPack(writer, request, params); err != nil { errorPage500(writer, params, err.Error()) } return } } if params["ref_type"], params["ref_name"], err = getParamRefTypeName(request); err != nil { if errors.Is(err, errNoRefSpec) { params["ref_type"] = "" } else { errorPage400(writer, params, "Error querying ref type: "+err.Error()) return } } if params["repo"], params["repo_description"], params["repo_id"], _, err = openRepo(request.Context(), groupPath, moduleName); err != nil { errorPage500(writer, params, "Error opening repo: "+err.Error()) return } repoURLRoot := "/" for _, part := range segments[:sepIndex+3] { repoURLRoot = repoURLRoot + url.PathEscape(part) + "/" } params["repo_url_root"] = repoURLRoot params["repo_patch_mailing_list"] = repoURLRoot[1:len(repoURLRoot)-1] + "@" + config.LMTP.Domain params["http_clone_url"] = genHTTPRemoteURL(groupPath, moduleName) params["ssh_clone_url"] = genSSHRemoteURL(groupPath, moduleName) if len(segments) == sepIndex+3 { if redirectDir(writer, request) { return } httpHandleRepoIndex(writer, request, params) return } repoFeature := segments[sepIndex+3] switch repoFeature { case "tree": if anyContain(segments[sepIndex+4:], "/") { errorPage400(writer, params, "Repo tree paths may not contain slashes in any segments") return } if dirMode { params["rest"] = strings.Join(segments[sepIndex+4:], "/") + "/" } else { params["rest"] = strings.Join(segments[sepIndex+4:], "/") } if len(segments) < sepIndex+5 && redirectDir(writer, request) { return } httpHandleRepoTree(writer, request, params) case "branches": if redirectDir(writer, request) { return } httpHandleRepoBranches(writer, request, params) return case "raw": if anyContain(segments[sepIndex+4:], "/") { errorPage400(writer, params, "Repo tree paths may not contain slashes in any segments") return } if dirMode { params["rest"] = strings.Join(segments[sepIndex+4:], "/") + "/" } else { params["rest"] = strings.Join(segments[sepIndex+4:], "/") } if len(segments) < sepIndex+5 && redirectDir(writer, request) { return } httpHandleRepoRaw(writer, request, params) case "log": if len(segments) > sepIndex+4 { errorPage400(writer, params, "Too many parameters") return } if redirectDir(writer, request) { return } httpHandleRepoLog(writer, request, params) case "commit": if len(segments) != sepIndex+5 { errorPage400(writer, params, "Incorrect number of parameters") return } if redirectNoDir(writer, request) { return } params["commit_id"] = segments[sepIndex+4] httpHandleRepoCommit(writer, request, params) case "contrib": if redirectDir(writer, request) { return } switch len(segments) { case sepIndex + 4: httpHandleRepoContribIndex(writer, request, params) case sepIndex + 5: params["mr_id"] = segments[sepIndex+4] httpHandleRepoContribOne(writer, request, params) default: errorPage400(writer, params, "Too many parameters") } default: errorPage404(writer, params) return } default: errorPage404(writer, params) return } } }
.\" SPDX-License-Identifier: AGPL-3.0-only .\" SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> .Dd March 30, 2025 .Dt LINDENII-FORGE-HOOKC 1 .Os Lindenii Forge .Sh NAME .Nm lindenii-forge-hookc .Nd helper binary to delegate Git hook behavior to the forge daemon .Sh SYNOPSIS .Nm .Op Ar argument ... .Sh DESCRIPTION .Nm is a helper binary for Git server-side hooks that relays the hook's context to a persistent daemon via a UNIX domain socket and communicates back any relevant responses. .Pp It is intended to be invoked by .Xr git-receive-pack 1 for hooks such as .Pa pre-receive , .Pa update , and .Pa post-receive . .Sh ENVIRONMENT .Bl -tag -width Ds .It Ev LINDENII_FORGE_HOOKS_SOCKET_PATH Absolute path to the UNIX domain socket on which the daemon is listening. .It Ev LINDENII_FORGE_HOOKS_COOKIE 64-character authentication cookie used to validate the hook client to the daemon. .El .Sh OPERATION .Nm collects the following information and sends it to the daemon: .Bl -bullet .It All command-line arguments .It All .Ev GIT_* environment variables .It The raw hook .Pa stdin (e.g., old/new ref triplets for .Pa pre-receive ) .El .Pp After sending this data, it waits for a one-byte status code from the daemon, which becomes .Nm Ns 's own exit status. .Pp If the daemon sends any output afterward, it is forwarded to standard error and will appear as .Dq remote: output to the user. .Sh BUGS .Bl -bullet .It The status byte from the daemon currently must be sent before any stderr output. .It Currently assumes .Pa stdin and .Pa stderr are pipes, which is not guaranteed in future versions of Git. .El .Sh AUTHORS .An Runxi Yu Aq Mt https://runxiyu.org .An Test_User Aq Mt hax@runxiyu.org .Sh SEE ALSO .Xr git-receive-pack 1 , .Xr lindenii-forge 1
.\" SPDX-License-Identifier: AGPL-3.0-only .\" SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> .Dd March 30, 2025 .Dt LINDENII-FORGE-MAIL 5 .Os Lindenii Forge .Sh NAME .Nm lindenii-forge-mail .Nd configuring Lindenii Forge email integration .Sh DESCRIPTION .Nm is a guide to configuring Lindenii Forge for email integration. .Pp This is currently a stub. Here is a working configuration that works for the Lindenii Project itself, though. .Sh /etc/smtpd/smtpd.conf .Bd -literal table forge file:/etc/smtpd/forge action "FORGE" lmtp "/srv/forge/lmtp.sock" rcpt-to virtual <forge> match from any for domain "forge.lindenii.runxiyu.org" action "FORGE" .Ed .Sh /etc/smtpd/forge .Bd -literal @ forge .Ed .Sh SEE ALSO .Xr lindenii-forge 1 , .Xr lindenii-forge 5 , .Xr smtpd.conf 5 .Sh AUTHORS .An Runxi Yu Aq Mt https://runxiyu.org .An Test_User Aq Mt hax@runxiyu.org
.\" SPDX-License-Identifier: AGPL-3.0-only .\" SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> .Dd March 30, 2025 .Dt LINDENII-FORGE 1 .Os Lindenii Forge .Sh NAME .Nm lindenii-forge .Nd Lindenii Forge server daemon .Sh SYNOPSIS .Nm .Op Fl config Ar path .Sh DESCRIPTION .Nm is the main server daemon for Lindenii Forge. .Pp All configuration is loaded from a configuration file; see .Xr lindenii-forge 5 . .Pp All listeners are long-lived; the process runs until interrupted. .Sh OPTIONS .Bl -tag -width Ds .It Fl config Ar path The path to the configuration file. Defaults to .Pa /etc/lindenii/forge.scfg . .El .Sh FILES .Bl -tag -width Ds .It Pa /etc/lindenii/forge.scfg Default configuration file. .El .Sh SEE ALSO .Xr lindenii-forge 5 .Sh AUTHORS .An Runxi Yu Aq Mt https://runxiyu.org .An Test_User Aq Mt hax@runxiyu.org
.\" SPDX-License-Identifier: AGPL-3.0-only .\" SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> .Dd March 30, 2025 .Dt LINDENII-FORGE 5 .Os Lindenii Forge .Sh NAME .Nm lindenii-forge.scfg .Nd configuration file for Lindenii Forge .Sh DESCRIPTION .Nm describes the configuration for .Xr lindenii-forge 1 instance using the scfg format. .Pp Each directive consists of a name followed by zero or more parameters. Directives may also introduce blocks of subdirectives using braces. .Pp Comments begin with .Sq # and extend to the end of the line. .Sh DIRECTIVES .Bl -tag -width Ds .It Ic http Configures the ingress HTTP server. .Bl -tag -width Ds .It Ic net Network type to listen on (e.g., .Dq tcp , .Dq tcp4 , .Dq unix ) . .It Ic addr Address to listen on (e.g., .Dq :8080 or .Dq /var/run/lindenii/forge/http.sock ) . .It Ic cookie_expiry How long (in seconds) to keep session cookies. .It Ic root Canonical root URL of the web interface (e.g., .Dq https://forge.example.org ) . .It Ic read_timeout , write_timeout , idle_timeout Timeouts, in seconds, for the general HTTP server context. .It Ic reverse_proxy Boolean indicating whether to trust X-Forwarded-For headers. .El .It Ic ssh Configures the SSH server. .Bl -tag -width Ds .It Ic net Network type to listen on .Dq ( tcp is recommended). .It Ic addr Address to listen on (e.g., .Dq :22 ) . .It Ic key Path to the SSH host key (must be passwordless). .It Ic root Canonical SSH URL prefix (e.g., .Dq ssh://forge.example.org ) . .El .It Ic git Configures Git repository storage. .Bl -tag -width Ds .It Ic repo_dir Filesystem path under which new repositories are stored. .It Ic socket Filesystem path for the socket listened on by .Xr lindenii-forge-git2d 1 . .It Ic daemon_path Where .Xr lindenii-forge-git2d 1 should be deployed to and run from. .El .It Ic db Configures database connection. .Bl -tag -width Ds .It Ic type Database type (currently must be .Dq postgres ) . .It Ic conn Connection string, e.g., .Dq postgresql:///lindenii-forge?host=/var/run/postgresql . .El .It Ic general Miscellaneous settings. .Bl -tag -width Ds .It Ic title A user-facing name for the instance. .El .It Ic hooks Configures Git hook communication with the forge daemon. .Bl -tag -width Ds .It Ic socket Path to a UNIX domain socket for receiving hook events. .It Ic execs Directory where Git hook executables are stored. .El .It Ic irc Optional configuration for IRC presence. .Bl -tag -width Ds .It Ic tls Boolean indicating whether to use TLS. .It Ic net , addr Network type and address (e.g., .Dq tcp , .Dq irc.example.org:6697 ) . .It Ic sendq Maximum send queue size. .It Ic nick , user , gecos Identity fields for the IRC connection. .El .It Ic lmtp Configuration for the LMTP/MX component. You may wish to refer to .Xr forge-mail 5 for information on configuring your SMTP server. .Bl -tag -width Ds .It Ic socket The path to the UNIX domain socket to listen on. .It Ic max_size The maximum acceptable ingress message size. .It Ic domain The domain-part of our LMTP server. .It Ic read_timeout , write_timeout General timeouts for LMTP connections. .El .El .Sh FILES .Bl -tag -width Ds .It Pa /etc/lindenii/forge.scfg Default path to the configuration file. .El .Sh SEE ALSO .Xr lindenii-forge 1 , .Xr lindenii-forge-hookc 1 , .Lk https://git.sr.ht/~emersion/scfg scfg .Sh AUTHORS .An Runxi Yu Aq Mt https://runxiyu.org .An Test_User Aq Mt hax@runxiyu.org
/* $OpenBSD: mandoc.css,v 1.41 2025/01/25 03:17:11 schwarze Exp $ */ /* * Standard style sheet for mandoc(1) -Thtml and man.cgi(8). * * Written by Ingo Schwarze <schwarze@openbsd.org>. * I place this file into the public domain. * Permission to use, copy, modify, and distribute it for any purpose * with or without fee is hereby granted, without any conditions. */ /* Global defaults. */ html { max-width: 65em; --bg: #FFFFFF; --fg: #000000; } body { background: var(--bg); color: var(--fg); font-family: Helvetica,Arial,sans-serif; } h1, h2 { font-size: 110%; } table { margin-top: 0em; margin-bottom: 0em; border-collapse: collapse; } /* Some browsers set border-color in a browser style for tbody, * but not for table, resulting in inconsistent border styling. */ tbody { border-color: inherit; } tr { border-color: inherit; } td { vertical-align: top; padding-left: 0.2em; padding-right: 0.2em; border-color: inherit; } ul, ol, dl { margin-top: 0em; margin-bottom: 0em; } li, dt { margin-top: 1em; } pre { font-family: inherit; } .permalink { border-bottom: thin dotted; color: inherit; font: inherit; text-decoration: inherit; } * { clear: both } /* Search form and search results. */ fieldset { border: thin solid silver; border-radius: 1em; text-align: center; } input[name=expr] { width: 25%; } table.results { margin-top: 1em; margin-left: 2em; font-size: smaller; } /* Header and footer lines. */ div[role=doc-pageheader] { display: flex; border-bottom: 1px dotted #808080; margin-bottom: 1em; font-size: smaller; } .head-ltitle { flex: 1; } .head-vol { flex: 0 1 auto; text-align: center; } .head-rtitle { flex: 1; text-align: right; } div[role=doc-pagefooter] { display: flex; justify-content: space-between; border-top: 1px dotted #808080; margin-top: 1em; font-size: smaller; } .foot-left { flex: 1; } .foot-date { flex: 0 1 auto; text-align: center; } .foot-os { flex: 1; text-align: right; } /* Sections and paragraphs. */ main { margin-left: 3.8em; } .Nd { } section.Sh { } h2.Sh { margin-top: 1.2em; margin-bottom: 0.6em; margin-left: -3.2em; } section.Ss { } h3.Ss { margin-top: 1.2em; margin-bottom: 0.6em; margin-left: -1.2em; font-size: 105%; } .Pp { margin: 0.6em 0em; } .Sx { } .Xr { } /* Displays and lists. */ .Bd { } .Bd-indent { margin-left: 3.8em; } .Bl-bullet { list-style-type: disc; padding-left: 1em; } .Bl-bullet > li { } .Bl-dash { list-style-type: none; padding-left: 0em; } .Bl-dash > li:before { content: "\2014 "; } .Bl-item { list-style-type: none; padding-left: 0em; } .Bl-item > li { } .Bl-compact > li { margin-top: 0em; } .Bl-enum { padding-left: 2em; } .Bl-enum > li { } .Bl-compact > li { margin-top: 0em; } .Bl-diag { } .Bl-diag > dt { font-style: normal; font-weight: bold; } .Bl-diag > dd { margin-left: 0em; } .Bl-hang { } .Bl-hang > dt { } .Bl-hang > dd { margin-left: 5.5em; } .Bl-inset { } .Bl-inset > dt { } .Bl-inset > dd { margin-left: 0em; } .Bl-ohang { } .Bl-ohang > dt { } .Bl-ohang > dd { margin-left: 0em; } .Bl-tag { margin-top: 0.6em; margin-left: 5.5em; } .Bl-tag > dt { float: left; margin-top: 0em; margin-left: -5.5em; padding-right: 0.5em; vertical-align: top; } .Bl-tag > dd { clear: right; column-count: 1; /* Force block formatting context. */ width: 100%; margin-top: 0em; margin-left: 0em; margin-bottom: 0.6em; vertical-align: top; } .Bl-compact { margin-top: 0em; } .Bl-compact > dd { margin-bottom: 0em; } .Bl-compact > dt { margin-top: 0em; } .Bl-column { } .Bl-column > tbody > tr { } .Bl-column > tbody > tr > td { margin-top: 1em; } .Bl-compact > tbody > tr > td { margin-top: 0em; } .Rs { font-style: normal; font-weight: normal; } .RsA { } .RsB { font-style: italic; font-weight: normal; } .RsC { } .RsD { } .RsI { font-style: italic; font-weight: normal; } .RsJ { font-style: italic; font-weight: normal; } .RsN { } .RsO { } .RsP { } .RsQ { } .RsR { } .RsT { font-style: normal; font-weight: normal; } .RsU { } .RsV { } .eqn { } .tbl td { vertical-align: middle; } .HP { margin-left: 3.8em; text-indent: -3.8em; } /* Semantic markup for command line utilities. */ table.Nm { } code.Nm { font-style: normal; font-weight: bold; font-family: inherit; } .Fl { font-style: normal; font-weight: bold; font-family: inherit; } .Cm { font-style: normal; font-weight: bold; font-family: inherit; } .Ar { font-style: italic; font-weight: normal; } .Op { display: inline flow; } .Ic { font-style: normal; font-weight: bold; font-family: inherit; } .Ev { font-style: normal; font-weight: normal; font-family: monospace; } .Pa { font-style: italic; font-weight: normal; } /* Semantic markup for function libraries. */ .Lb { } code.In { font-style: normal; font-weight: bold; font-family: inherit; } a.In { } .Fd { font-style: normal; font-weight: bold; font-family: inherit; } .Ft { font-style: italic; font-weight: normal; } .Fn { font-style: normal; font-weight: bold; font-family: inherit; } .Fa { font-style: italic; font-weight: normal; } .Vt { font-style: italic; font-weight: normal; } .Va { font-style: italic; font-weight: normal; } .Dv { font-style: normal; font-weight: normal; font-family: monospace; } .Er { font-style: normal; font-weight: normal; font-family: monospace; } /* Various semantic markup. */ .An { } .Lk { } .Mt { } .Cd { font-style: normal; font-weight: bold; font-family: inherit; } .Ad { font-style: italic; font-weight: normal; } .Ms { font-style: normal; font-weight: bold; } .St { } .Ux { } /* Physical markup. */ .Bf { display: inline flow; } .No { font-style: normal; font-weight: normal; } .Em { font-style: italic; font-weight: normal; } .Sy { font-style: normal; font-weight: bold; } .Li { font-style: normal; font-weight: normal; font-family: monospace; } /* Tooltip support. */ h2.Sh, h3.Ss { position: relative; } .An, .Ar, .Cd, .Cm, .Dv, .Em, .Er, .Ev, .Fa, .Fd, .Fl, .Fn, .Ft, .Ic, code.In, .Lb, .Lk, .Ms, .Mt, .Nd, code.Nm, .Pa, .Rs, .St, .Sx, .Sy, .Va, .Vt, .Xr { display: inline flow; position: relative; } .An::before { content: "An"; } .Ar::before { content: "Ar"; } .Cd::before { content: "Cd"; } .Cm::before { content: "Cm"; } .Dv::before { content: "Dv"; } .Em::before { content: "Em"; } .Er::before { content: "Er"; } .Ev::before { content: "Ev"; } .Fa::before { content: "Fa"; } .Fd::before { content: "Fd"; } .Fl::before { content: "Fl"; } .Fn::before { content: "Fn"; } .Ft::before { content: "Ft"; } .Ic::before { content: "Ic"; } code.In::before { content: "In"; } .Lb::before { content: "Lb"; } .Lk::before { content: "Lk"; } .Ms::before { content: "Ms"; } .Mt::before { content: "Mt"; } .Nd::before { content: "Nd"; } code.Nm::before { content: "Nm"; } .Pa::before { content: "Pa"; } .Rs::before { content: "Rs"; } h2.Sh::before { content: "Sh"; } h3.Ss::before { content: "Ss"; } .St::before { content: "St"; } .Sx::before { content: "Sx"; } .Sy::before { content: "Sy"; } .Va::before { content: "Va"; } .Vt::before { content: "Vt"; } .Xr::before { content: "Xr"; } .An::before, .Ar::before, .Cd::before, .Cm::before, .Dv::before, .Em::before, .Er::before, .Ev::before, .Fa::before, .Fd::before, .Fl::before, .Fn::before, .Ft::before, .Ic::before, code.In::before, .Lb::before, .Lk::before, .Ms::before, .Mt::before, .Nd::before, code.Nm::before, .Pa::before, .Rs::before, h2.Sh::before, h3.Ss::before, .St::before, .Sx::before, .Sy::before, .Va::before, .Vt::before, .Xr::before { opacity: 0; transition: .15s ease opacity; pointer-events: none; position: absolute; bottom: 100%; box-shadow: 0 0 .35em var(--fg); padding: .15em .25em; white-space: nowrap; font-family: Helvetica,Arial,sans-serif; font-style: normal; font-weight: bold; background: var(--bg); color: var(--fg); } .An:hover::before, .Ar:hover::before, .Cd:hover::before, .Cm:hover::before, .Dv:hover::before, .Em:hover::before, .Er:hover::before, .Ev:hover::before, .Fa:hover::before, .Fd:hover::before, .Fl:hover::before, .Fn:hover::before, .Ft:hover::before, .Ic:hover::before, code.In:hover::before, .Lb:hover::before, .Lk:hover::before, .Ms:hover::before, .Mt:hover::before, .Nd:hover::before, code.Nm:hover::before, .Pa:hover::before, .Rs:hover::before, h2.Sh:hover::before, h3.Ss:hover::before, .St:hover::before, .Sx:hover::before, .Sy:hover::before, .Va:hover::before, .Vt:hover::before, .Xr:hover::before { opacity: 1; pointer-events: inherit; } /* Overrides to avoid excessive margins on small devices. */ @media (max-width: 37.5em) { main { margin-left: 0.5em; } h2.Sh, h3.Ss { margin-left: 0em; } .Bd-indent { margin-left: 2em; } .Bl-hang > dd { margin-left: 2em; } .Bl-tag { margin-left: 2em; } .Bl-tag > dt { margin-left: -2em; } .HP { margin-left: 2em; text-indent: -2em; } } /* Overrides for a dark color scheme for accessibility. */ @media (prefers-color-scheme: dark) { html { --bg: #1E1F21; --fg: #EEEFF1; } :link { color: #BAD7FF; } :visited { color: #F6BAFF; } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "embed" "html/template" "io/fs" "net/http" "github.com/tdewolff/minify/v2" "github.com/tdewolff/minify/v2/html" ) //go:embed LICENSE source.tar.gz var sourceFS embed.FS var sourceHandler = http.StripPrefix( "/-/source/", http.FileServer(http.FS(sourceFS)), ) //go:embed templates/* static/*
//go:embed man/*.html man/*.txt man/*.css
//go:embed hookc/hookc git2d/git2d var resourcesFS embed.FS var templates *template.Template // loadTemplates minifies and loads HTML templates. func loadTemplates() (err error) { minifier := minify.New() minifierOptions := html.Minifier{ TemplateDelims: [2]string{"{{", "}}"}, KeepDefaultAttrVals: true, } //exhaustruct:ignore minifier.Add("text/html", &minifierOptions) templates = template.New("templates").Funcs(template.FuncMap{ "first_line": firstLine, "path_escape": pathEscape, "query_escape": queryEscape, "dereference_error": dereferenceOrZero[error], "minus": minus, }) err = fs.WalkDir(resourcesFS, "templates", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { content, err := fs.ReadFile(resourcesFS, path) if err != nil { return err } minified, err := minifier.Bytes("text/html", content) if err != nil { return err } _, err = templates.Parse(bytesToString(minified)) if err != nil { return err } } return nil }) return err } var ( staticHandler http.Handler
manHandler http.Handler
)
// This init sets up static and man handlers. The resulting handlers must be
// This init sets up static handlers. The resulting handlers must be
// used in the HTTP router, and do nothing unless called from elsewhere. func init() { staticFS, err := fs.Sub(resourcesFS, "static") if err != nil { panic(err) } staticHandler = http.StripPrefix("/-/static/", http.FileServer(http.FS(staticFS)))
manFS, err := fs.Sub(resourcesFS, "man") if err != nil { panic(err) } manHandler = http.StripPrefix("/-/man/", http.FileServer(http.FS(manFS)))
}