Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
Add missing license headers
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
// Package ansiec provides definitions for ANSI escape sequences. package ansiec
// 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"
)
type Database struct {
*pgxpool.Pool
}
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" //go:embed LICENSE* source.tar.gz var Source embed.FS //go:embed templates/* 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 irc provides basic IRC bot functionality.
package irc
import (
"crypto/tls"
"log/slog"
"net"
"go.lindenii.runxiyu.org/forge/internal/misc"
irc "go.lindenii.runxiyu.org/lindenii-irc"
)
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"`
}
type Bot struct {
config *Config
ircSendBuffered chan string
ircSendDirectChan chan misc.ErrorBack[string]
}
func NewBot(c *Config) (b *Bot) {
b = &Bot{
config: c,
}
return
}
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
}
func (b *Bot) Send(line string) {
select {
case b.ircSendBuffered <- line:
default:
slog.Error("irc sendq full", "line", line)
}
}
// TODO: Delay and warnings?
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
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"
)
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
func FirstOrPanic[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
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 oldgit provides deprecated functions that depend on go-git. package oldgit
// 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
}
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 provides functions to render code and READMEs. package render
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package unsorted
import (
"errors"
"net/http"
"path/filepath"
"strconv"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"go.lindenii.runxiyu.org/forge/internal/misc"
"go.lindenii.runxiyu.org/forge/internal/web"
)
// httpHandleGroupIndex provides index pages for groups, which includes a list
// of its subgroups and repos, as well as a form for group maintainers to
// create repos.
func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) {
var groupPath []string
var repos []nameDesc
var subgroups []nameDesc
var err error
var groupID int
var groupDesc string
groupPath = params["group_path"].([]string)
// The group itself
err = s.database.QueryRow(request.Context(), `
WITH RECURSIVE group_path_cte AS (
SELECT
id,
parent_group,
name,
1 AS depth
FROM groups
WHERE name = ($1::text[])[1]
AND parent_group IS NULL
UNION ALL
SELECT
g.id,
g.parent_group,
g.name,
group_path_cte.depth + 1
FROM groups g
JOIN group_path_cte ON g.parent_group = group_path_cte.id
WHERE g.name = ($1::text[])[group_path_cte.depth + 1]
AND group_path_cte.depth + 1 <= cardinality($1::text[])
)
SELECT c.id, COALESCE(g.description, '')
FROM group_path_cte c
JOIN groups g ON g.id = c.id
WHERE c.depth = cardinality($1::text[])
`,
pgtype.FlatArray[string](groupPath),
).Scan(&groupID, &groupDesc)
if errors.Is(err, pgx.ErrNoRows) {
web.ErrorPage404(s.templates, writer, params)
return
} else if err != nil {
web.ErrorPage500(s.templates, writer, params, "Error getting group: "+err.Error())
return
}
// ACL
var count int
err = s.database.QueryRow(request.Context(), `
SELECT COUNT(*)
FROM user_group_roles
WHERE user_id = $1
AND group_id = $2
`, params["user_id"].(int), groupID).Scan(&count)
if err != nil {
web.ErrorPage500(s.templates, writer, params, "Error checking access: "+err.Error())
return
}
directAccess := (count > 0)
if request.Method == http.MethodPost {
if !directAccess {
web.ErrorPage403(s.templates, writer, params, "You do not have direct access to this group")
return
}
repoName := request.FormValue("repo_name")
repoDesc := request.FormValue("repo_desc")
contribReq := request.FormValue("repo_contrib")
if repoName == "" {
web.ErrorPage400(s.templates, writer, params, "Repo name is required")
return
}
var newRepoID int
err := s.database.QueryRow(
request.Context(),
`INSERT INTO repos (name, description, group_id, contrib_requirements)
VALUES ($1, $2, $3, $4)
RETURNING id`,
repoName,
repoDesc,
groupID,
contribReq,
).Scan(&newRepoID)
if err != nil {
web.ErrorPage500(s.templates, writer, params, "Error creating repo: "+err.Error())
return
}
filePath := filepath.Join(s.config.Git.RepoDir, strconv.Itoa(newRepoID)+".git")
_, err = s.database.Exec(
request.Context(),
`UPDATE repos
SET filesystem_path = $1
WHERE id = $2`,
filePath,
newRepoID,
)
if err != nil {
web.ErrorPage500(s.templates, writer, params, "Error updating repo path: "+err.Error())
return
}
if err = s.gitInit(filePath); err != nil {
web.ErrorPage500(s.templates, writer, params, "Error initializing repo: "+err.Error())
return
}
misc.RedirectUnconditionally(writer, request)
return
}
// Repos
var rows pgx.Rows
rows, err = s.database.Query(request.Context(), `
SELECT name, COALESCE(description, '')
FROM repos
WHERE group_id = $1
`, groupID)
if err != nil {
web.ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error())
return
}
defer rows.Close()
for rows.Next() {
var name, description string
if err = rows.Scan(&name, &description); err != nil {
web.ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error())
return
}
repos = append(repos, nameDesc{name, description})
}
if err = rows.Err(); err != nil {
web.ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error())
return
}
// Subgroups
rows, err = s.database.Query(request.Context(), `
SELECT name, COALESCE(description, '')
FROM groups
WHERE parent_group = $1
`, groupID)
if err != nil {
web.ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error())
return
}
defer rows.Close()
for rows.Next() {
var name, description string
if err = rows.Scan(&name, &description); err != nil {
web.ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error())
return
}
subgroups = append(subgroups, nameDesc{name, description})
}
if err = rows.Err(); err != nil {
web.ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error())
return
}
params["repos"] = repos
params["subgroups"] = subgroups
params["description"] = groupDesc
params["direct_access"] = directAccess
s.renderTemplate(writer, "group", params)
}
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
// Package unsorted is where unsorted Go files from the old structure are kept. package unsorted
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package unsorted var version = "unknown"
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
// Package web provides web-facing components of the forge. package web