Lindenii Project Forge
Add more documentation comments
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // Package database provides stubs and wrappers for databases. package database import ( "context" "github.com/jackc/pgx/v5/pgxpool" )
// Database is a wrapper around pgxpool.Pool to provide a common interface for // other packages in the forge.
type Database struct { *pgxpool.Pool }
// Open opens a new database connection pool using the provided connection // string. It returns a Database instance and an error if any occurs. // It is run indefinitely in the background.
func Open(connString string) (Database, error) { db, err := pgxpool.New(context.Background(), connString) return Database{db}, err }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // Package embed provides embedded filesystems created in build-time. package embed import "embed"
// Source contains the licenses and source tarballs collected at build time. // It is intended to be served to the user. //
//go:embed LICENSE* source.tar.gz var Source embed.FS
// Resources contains the templates and static files used by the web interface, // as well as the git backend daemon and the hookc helper. //
//go:embed forged/templates/* forged/static/* //go:embed hookc/hookc git2d/git2d var Resources embed.FS
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // Package git2c provides routines to interact with the git2d backend daemon. package git2c import ( "fmt" "net" "git.sr.ht/~sircmpwn/go-bare" )
// Client represents a connection to the git2d backend daemon.
type Client struct {
SocketPath string
socketPath string
conn net.Conn writer *bare.Writer reader *bare.Reader }
// NewClient establishes a connection to a git2d socket and returns a new Client.
func NewClient(socketPath string) (*Client, error) { conn, err := net.Dial("unix", socketPath) if err != nil { return nil, fmt.Errorf("git2d connection failed: %w", err) } writer := bare.NewWriter(conn) reader := bare.NewReader(conn) return &Client{
SocketPath: socketPath,
socketPath: socketPath,
conn: conn, writer: writer, reader: reader, }, nil }
// Close terminates the underlying socket connection.
func (c *Client) Close() error { if c.conn != nil { return c.conn.Close() } return nil }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package git2c import ( "encoding/hex" "errors" "fmt" "io" )
// CmdIndex requests a repository index from git2d and returns the list of commits // and the contents of a README file if available.
func (c *Client) CmdIndex(repoPath string) ([]Commit, *FilenameContents, error) { if err := c.writer.WriteData([]byte(repoPath)); err != nil { return nil, nil, fmt.Errorf("sending repo path failed: %w", err) } if err := c.writer.WriteUint(1); err != nil { return nil, nil, fmt.Errorf("sending command failed: %w", err) } status, err := c.reader.ReadUint() if err != nil { return nil, nil, fmt.Errorf("reading status failed: %w", err) } if status != 0 { return nil, nil, fmt.Errorf("git2d error: %d", status) } // README readmeRaw, err := c.reader.ReadData() if err != nil { readmeRaw = nil } readmeFilename := "README.md" // TODO readme := &FilenameContents{Filename: readmeFilename, Content: readmeRaw} // Commits var commits []Commit for { id, err := c.reader.ReadData() if err != nil { if errors.Is(err, io.EOF) { break } return nil, nil, fmt.Errorf("reading commit ID failed: %w", err) } title, _ := c.reader.ReadData() authorName, _ := c.reader.ReadData() authorEmail, _ := c.reader.ReadData() authorDate, _ := c.reader.ReadData() commits = append(commits, Commit{ Hash: hex.EncodeToString(id), Author: string(authorName), Email: string(authorEmail), Date: string(authorDate), Message: string(title), }) } return commits, readme, nil }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package git2c import ( "errors" "fmt" "io" )
// CmdTreeRaw queries git2d for a tree or blob object at the given path within the repository. // It returns either a directory listing or the contents of a file.
func (c *Client) CmdTreeRaw(repoPath, pathSpec string) ([]TreeEntry, string, error) { if err := c.writer.WriteData([]byte(repoPath)); err != nil { return nil, "", fmt.Errorf("sending repo path failed: %w", err) } if err := c.writer.WriteUint(2); err != nil { return nil, "", fmt.Errorf("sending command failed: %w", err) } if err := c.writer.WriteData([]byte(pathSpec)); err != nil { return nil, "", fmt.Errorf("sending path failed: %w", err) } status, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("reading status failed: %w", err) } switch status { case 0: kind, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("reading object kind failed: %w", err) } switch kind { case 1: // Tree count, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("reading entry count failed: %w", err) } var files []TreeEntry for range count { typeCode, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("error reading entry type: %w", err) } mode, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("error reading entry mode: %w", err) } size, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("error reading entry size: %w", err) } name, err := c.reader.ReadData() if err != nil { return nil, "", fmt.Errorf("error reading entry name: %w", err) } files = append(files, TreeEntry{ Name: string(name), Mode: fmt.Sprintf("%06o", mode), Size: size, IsFile: typeCode == 2, IsSubtree: typeCode == 1, }) } return files, "", nil case 2: // Blob content, err := c.reader.ReadData() if err != nil && !errors.Is(err, io.EOF) { return nil, "", fmt.Errorf("error reading file content: %w", err) } return nil, string(content), nil default: return nil, "", fmt.Errorf("unknown kind: %d", kind) } case 3: return nil, "", fmt.Errorf("path not found: %s", pathSpec) default: return nil, "", fmt.Errorf("unknown status code: %d", status) } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package git2c
// Commit represents a single commit object retrieved from the git2d daemon.
type Commit struct { Hash string Author string Email string Date string Message string }
// FilenameContents holds the filename and byte contents of a file, such as a README.
type FilenameContents struct { Filename string Content []byte }
// TreeEntry represents a file or directory entry within a Git tree object.
type TreeEntry struct { Name string Mode string Size uint64 IsFile bool IsSubtree bool }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> // Package irc provides basic IRC bot functionality. package irc import ( "crypto/tls" "log/slog" "net" "go.lindenii.runxiyu.org/forge/forged/internal/misc" irc "go.lindenii.runxiyu.org/lindenii-irc" )
// Config contains IRC connection and identity settings for the bot. // This should usually be a part of the primary config struct.
type Config struct { Net string `scfg:"net"` Addr string `scfg:"addr"` TLS bool `scfg:"tls"` SendQ uint `scfg:"sendq"` Nick string `scfg:"nick"` User string `scfg:"user"` Gecos string `scfg:"gecos"` }
// Bot represents an IRC bot client that handles events and allows for sending messages.
type Bot struct { config *Config ircSendBuffered chan string ircSendDirectChan chan misc.ErrorBack[string] }
// NewBot creates a new Bot instance using the provided configuration.
func NewBot(c *Config) (b *Bot) { b = &Bot{ config: c, } return }
// Connect establishes a new IRC session and starts handling incoming and outgoing messages. // This method blocks until an error occurs or the connection is closed.
func (b *Bot) Connect() error { var err error var underlyingConn net.Conn if b.config.TLS { underlyingConn, err = tls.Dial(b.config.Net, b.config.Addr, nil) } else { underlyingConn, err = net.Dial(b.config.Net, b.config.Addr) } if err != nil { return err } defer underlyingConn.Close() conn := irc.NewConn(underlyingConn) logAndWriteLn := func(s string) (n int, err error) { slog.Debug("irc tx", "line", s) return conn.WriteString(s + "\r\n") } _, err = logAndWriteLn("NICK " + b.config.Nick) if err != nil { return err } _, err = logAndWriteLn("USER " + b.config.User + " 0 * :" + b.config.Gecos) if err != nil { return err } readLoopError := make(chan error) writeLoopAbort := make(chan struct{}) go func() { for { select { case <-writeLoopAbort: return default: } msg, line, err := conn.ReadMessage() if err != nil { readLoopError <- err return } slog.Debug("irc rx", "line", line) switch msg.Command { case "001": _, err = logAndWriteLn("JOIN #chat") if err != nil { readLoopError <- err return } case "PING": _, err = logAndWriteLn("PONG :" + msg.Args[0]) if err != nil { readLoopError <- err return } case "JOIN": c, ok := msg.Source.(irc.Client) if !ok { slog.Error("unable to convert source of JOIN to client") } if c.Nick != b.config.Nick { continue } default: } } }() for { select { case err = <-readLoopError: return err case line := <-b.ircSendBuffered: _, err = logAndWriteLn(line) if err != nil { select { case b.ircSendBuffered <- line: default: slog.Error("unable to requeue message", "line", line) } writeLoopAbort <- struct{}{} return err } case lineErrorBack := <-b.ircSendDirectChan: _, err = logAndWriteLn(lineErrorBack.Content) lineErrorBack.ErrorChan <- err if err != nil { writeLoopAbort <- struct{}{} return err } } } } // SendDirect sends an IRC message directly to the connection and bypasses // the buffering system. func (b *Bot) SendDirect(line string) error { ech := make(chan error, 1) b.ircSendDirectChan <- misc.ErrorBack[string]{ Content: line, ErrorChan: ech, } return <-ech }
// Send queues a message to be sent asynchronously via the buffered send queue. // If the queue is full, the message is dropped and an error is logged.
func (b *Bot) Send(line string) { select { case b.ircSendBuffered <- line: default: slog.Error("irc sendq full", "line", line) } }
// TODO: Delay and warnings?
// ConnectLoop continuously attempts to maintain an IRC session. // If the connection drops, it automatically retries with no delay.
func (b *Bot) ConnectLoop() { b.ircSendBuffered = make(chan string, b.config.SendQ) b.ircSendDirectChan = make(chan misc.ErrorBack[string]) for { err := b.Connect() slog.Error("irc session error", "error", err) } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package misc
// ErrorBack wraps a value and a channel for communicating an associated error. // Typically used to get an error response after sending data across a channel.
type ErrorBack[T any] struct { Content T ErrorChan chan error }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package misc import ( "io" "io/fs" "os" )
// DeployBinary copies the contents of a binary file to the target destination path. // The destination file is created with executable permissions.
func DeployBinary(src fs.File, dst string) (err error) { var dstFile *os.File if dstFile, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755); err != nil { return err } defer dstFile.Close() _, err = io.Copy(dstFile, src) return err }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package misc
// FirstOrPanic returns the value or panics if the error is non-nil.
func FirstOrPanic[T any](v T, err error) T { if err != nil { panic(err) } return v }
// NoneOrPanic panics if the provided error is non-nil.
func NoneOrPanic(err error) { if err != nil { panic(err) } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package misc import ( "errors" "net/http" "net/url" "strings" ) var ( ErrDupRefSpec = errors.New("duplicate ref spec") ErrNoRefSpec = errors.New("no ref spec") ) // getParamRefTypeName looks at the query parameters in an HTTP request and // returns its ref name and type, if any. func GetParamRefTypeName(request *http.Request) (retRefType, retRefName string, err error) { rawQuery := request.URL.RawQuery queryValues, err := url.ParseQuery(rawQuery) if err != nil { return } done := false for _, refType := range []string{"commit", "branch", "tag"} { refName, ok := queryValues[refType] if ok { if done { err = ErrDupRefSpec return } done = true if len(refName) != 1 { err = ErrDupRefSpec return } retRefName = refName[0] retRefType = refType } } if !done { err = ErrNoRefSpec } return } // ParseReqURI parses an HTTP request URL, and returns a slice of path segments // and the query parameters. It handles %2F correctly. func ParseReqURI(requestURI string) (segments []string, params url.Values, err error) { path, paramsStr, _ := strings.Cut(requestURI, "?") segments, err = PathToSegments(path) if err != nil { return } params, err = url.ParseQuery(paramsStr) return }
// PathToSegments splits a path into unescaped segments. It handles %2F correctly.
func PathToSegments(path string) (segments []string, err error) { segments = strings.Split(strings.TrimPrefix(path, "/"), "/") for i, segment := range segments { segments[i], err = url.PathUnescape(segment) if err != nil { return } } return } // RedirectDir returns true and redirects the user to a version of the URL with // a trailing slash, if and only if the request URL does not already have a // trailing slash. func RedirectDir(writer http.ResponseWriter, request *http.Request) bool { requestURI := request.RequestURI pathEnd := strings.IndexAny(requestURI, "?#") var path, rest string if pathEnd == -1 { path = requestURI } else { path = requestURI[:pathEnd] rest = requestURI[pathEnd:] } if !strings.HasSuffix(path, "/") { http.Redirect(writer, request, path+"/"+rest, http.StatusSeeOther) return true } return false } // RedirectNoDir returns true and redirects the user to a version of the URL // without a trailing slash, if and only if the request URL has a trailing // slash. func RedirectNoDir(writer http.ResponseWriter, request *http.Request) bool { requestURI := request.RequestURI pathEnd := strings.IndexAny(requestURI, "?#") var path, rest string if pathEnd == -1 { path = requestURI } else { path = requestURI[:pathEnd] rest = requestURI[pathEnd:] } if strings.HasSuffix(path, "/") { http.Redirect(writer, request, strings.TrimSuffix(path, "/")+rest, http.StatusSeeOther) return true } return false } // RedirectUnconditionally unconditionally redirects the user back to the // current page while preserving query parameters. func RedirectUnconditionally(writer http.ResponseWriter, request *http.Request) { requestURI := request.RequestURI pathEnd := strings.IndexAny(requestURI, "?#") var path, rest string if pathEnd == -1 { path = requestURI } else { path = requestURI[:pathEnd] rest = requestURI[pathEnd:] } http.Redirect(writer, request, path+rest, http.StatusSeeOther) } // SegmentsToURL joins URL segments to the path component of a URL. // Each segment is escaped properly first. func SegmentsToURL(segments []string) string { for i, segment := range segments { segments[i] = url.PathEscape(segment) } return strings.Join(segments, "/") } // AnyContain returns true if and only if ss contains a string that contains c. func AnyContain(ss []string, c string) bool { for _, s := range ss { if strings.Contains(s, c) { return true } } return false }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package oldgit import ( "errors" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) // CommitToPatch creates an [object.Patch] from the first parent of a given // [object.Commit]. // // TODO: This function should be deprecated as it only diffs with the first // parent and does not correctly handle merge commits. func CommitToPatch(commit *object.Commit) (parentCommitHash plumbing.Hash, patch *object.Patch, err error) { var parentCommit *object.Commit var commitTree *object.Tree parentCommit, err = commit.Parent(0) switch { case errors.Is(err, object.ErrParentNotFound): if commitTree, err = commit.Tree(); err != nil { return } if patch, err = NullTree.Patch(commitTree); err != nil { return } case err != nil: return default: parentCommitHash = parentCommit.Hash if patch, err = parentCommit.Patch(commit); err != nil { return } } return }
// NullTree is a tree object that is empty and has no hash.
var NullTree object.Tree //nolint:gochecknoglobals
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package render import ( "bytes" "html/template" chromaHTML "github.com/alecthomas/chroma/v2/formatters/html" chromaLexers "github.com/alecthomas/chroma/v2/lexers" chromaStyles "github.com/alecthomas/chroma/v2/styles" )
// Highlight returns HTML with syntax highlighting for the given file content, // using Chroma. The lexer is selected based on the filename. // If tokenization or formatting fails, a fallback <pre> block is returned with the error.
func Highlight(filename, content string) template.HTML { lexer := chromaLexers.Match(filename) if lexer == nil { lexer = chromaLexers.Fallback } iterator, err := lexer.Tokenise(nil, content) if err != nil { return template.HTML("<pre>Error tokenizing file: " + err.Error() + "</pre>") //#nosec G203` } var buf bytes.Buffer style := chromaStyles.Get("autumn") formatter := chromaHTML.New( chromaHTML.WithClasses(true), chromaHTML.TabWidth(8), ) if err := formatter.Format(&buf, style, iterator); err != nil { return template.HTML("<pre>Error formatting file: " + err.Error() + "</pre>") //#nosec G203 } return template.HTML(buf.Bytes()) //#nosec G203 }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package web import ( "html/template" "net/http" )
// ErrorPage404 renders a 404 Not Found error page using the "404" template.
func ErrorPage404(templates *template.Template, w http.ResponseWriter, params map[string]any) { w.WriteHeader(http.StatusNotFound) _ = templates.ExecuteTemplate(w, "404", params) }
// ErrorPage400 renders a 400 Bad Request error page using the "400" template. // The error message is passed via the "complete_error_msg" template param.
func ErrorPage400(templates *template.Template, w http.ResponseWriter, params map[string]any, msg string) { w.WriteHeader(http.StatusBadRequest) params["complete_error_msg"] = msg _ = templates.ExecuteTemplate(w, "400", params) }
// ErrorPage400Colon renders a 400 Bad Request error page telling the user // that we migrated from : to -.
func ErrorPage400Colon(templates *template.Template, w http.ResponseWriter, params map[string]any) { w.WriteHeader(http.StatusBadRequest) _ = templates.ExecuteTemplate(w, "400_colon", params) }
// ErrorPage403 renders a 403 Forbidden error page using the "403" template. // The error message is passed via the "complete_error_msg" template param.
func ErrorPage403(templates *template.Template, w http.ResponseWriter, params map[string]any, msg string) { w.WriteHeader(http.StatusForbidden) params["complete_error_msg"] = msg _ = templates.ExecuteTemplate(w, "403", params) }
// ErrorPage451 renders a 451 Unavailable For Legal Reasons error page using the "451" template. // The error message is passed via the "complete_error_msg" template param.
func ErrorPage451(templates *template.Template, w http.ResponseWriter, params map[string]any, msg string) { w.WriteHeader(http.StatusUnavailableForLegalReasons) params["complete_error_msg"] = msg _ = templates.ExecuteTemplate(w, "451", params) }
// ErrorPage500 renders a 500 Internal Server Error page using the "500" template. // The error message is passed via the "complete_error_msg" template param.
func ErrorPage500(templates *template.Template, w http.ResponseWriter, params map[string]any, msg string) { w.WriteHeader(http.StatusInternalServerError) params["complete_error_msg"] = msg _ = templates.ExecuteTemplate(w, "500", params) }
// ErrorPage501 renders a 501 Not Implemented error page using the "501" template.
func ErrorPage501(templates *template.Template, w http.ResponseWriter, params map[string]any) { w.WriteHeader(http.StatusNotImplemented) _ = templates.ExecuteTemplate(w, "501", params) }