Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
HTML: Add contribution guidelines in the MR tab
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
import (
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"git.sr.ht/~sircmpwn/go-bare"
)
type commitDisplay struct {
Hash string
Author string
Email string
Date string
Message string
}
// httpHandleRepoIndex provides the front page of a repo using git2d.
func httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, params map[string]any) {
repoName := params["repo_name"].(string)
groupPath := params["group_path"].([]string)
_, repoPath, _, _, _, _, _ := getRepoInfo(req.Context(), groupPath, repoName, "") // TODO: Don't use getRepoInfo
var notes []string
if strings.Contains(repoName, "\n") || sliceContainsNewlines(groupPath) {
notes = append(notes, "Path contains newlines; HTTP Git access impossible")
}
conn, err := net.Dial("unix", config.Git.Socket)
if err != nil {
errorPage500(w, params, "git2d connection failed: "+err.Error())
return
}
defer conn.Close()
writer := bare.NewWriter(conn)
if err := writer.WriteData([]byte(repoPath)); err != nil {
errorPage500(w, params, "sending repo path failed: "+err.Error())
return
}
reader := bare.NewReader(conn)
status, err := reader.ReadUint()
if err != nil {
errorPage500(w, params, "reading status failed: "+err.Error())
return
}
if status != 0 {
errorPage500(w, params, fmt.Sprintf("git2d error: %d", status))
return
}
// README
readmeRaw, err := reader.ReadData()
if err != nil {
readmeRaw = nil
}
readmeFilename, readmeRendered := renderReadme(readmeRaw, "README.md")
// Commits
var commits []commitDisplay
for {
id, err := reader.ReadData()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
errorPage500(w, params, "error reading commit ID: "+err.Error())
return
}
title, _ := reader.ReadData()
authorName, _ := reader.ReadData()
authorEmail, _ := reader.ReadData()
authorDate, _ := reader.ReadData()
commits = append(commits, commitDisplay{
Hash: hex.EncodeToString(id),
Author: string(authorName),
Email: string(authorEmail),
Date: string(authorDate),
Message: string(title),
})
}
params["commits"] = commits
params["readme_filename"] = readmeFilename
params["readme"] = readmeRendered
params["http_clone_url"] = genHTTPRemoteURL(groupPath, repoName) params["ssh_clone_url"] = genSSHRemoteURL(groupPath, repoName)
params["notes"] = notes renderTemplate(w, "repo_index", params) // TODO: Caching }
// 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:] + "@" + 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>
*/}}
{{- define "repo_contrib_index" -}}
{{- $root := . -}}
<!DOCTYPE html>
<html lang="en">
<head>
{{- template "head_common" . -}}
<title>Merge requests – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .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 powxy
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 class="title-row"> <th colspan="3">Merge requests</th>
<tr> <th scope="col">ID</th> <th scope="col">Title</th> <th scope="col">Status</th>
</tr> </thead>
<tr> <th scope="col">ID</th> <th scope="col">Title</th> <th scope="col">Status</th> </tr>
<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 -}}