Lindenii Project Forge
Mandoc more
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-FileContributor: Runxi Yu <https://runxiyu.org> .PHONY: clean version.go man CFLAGS = -Wall -Wextra -Werror -pedantic -std=c99 -D_GNU_SOURCE MAN_PAGES = forge.5 hookc.1 forge: version.go hookc/*.c hookc/hookc man # TODO go mod vendor go build .
man: $(MAN_PAGES:%=man/%.html)
man: $(MAN_PAGES:%=man/%.html) $(MAN_PAGES:%=man/%.txt)
man/%.html: man/%
mandoc -Thtml -O style=static/mandoc.css $< > $@
mandoc -Thtml -O style=./mandoc.css $< > $@ man/%.txt: man/% mandoc $< | col -b > $@
hookc/hookc: version.go: printf 'package main\n\nconst VERSION = "%s"\n' `git describe --tags --always --dirty` > $@ clean: $(RM) forge version.go vendor
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "errors" "net/http" "strconv" "strings" "github.com/jackc/pgx/v5" "go.lindenii.runxiyu.org/lindenii-common/clog" ) type forgeHTTPRouter struct{} 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) } 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 { errorPage500(writer, params, "Error querying ref type: "+err.Error()) return } } // TODO: subgroups 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 } 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 "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 } } }
/*.html
/*.txt
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: 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" ) // We embed all source for easy AGPL compliance. // //go:embed .gitignore .gitattributes //go:embed LICENSE README.md //go:embed *.go go.mod go.sum //go:embed *.scfg //go:embed Makefile
//go:embed static/* templates/* scripts/* sql/*
//go:embed static/* templates/* scripts/* sql/* man/*
//go:embed hookc/*.c //go:embed vendor/* var sourceFS embed.FS var sourceHandler = http.StripPrefix( "/:/source/", http.FileServer(http.FS(sourceFS)), )
//go:embed templates/* static/* hookc/hookc
//go:embed templates/* static/* hookc/hookc man/*.html man/*.txt man/*.css
var resourcesFS embed.FS var templates *template.Template 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, "base_name": baseName, "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
var manHandler http.Handler
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)))
}
/* $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; } }