aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-08-12 22:30:39 +0800
committerRunxi Yu <me@runxiyu.org>2025-08-12 22:30:39 +0800
commitbaaf3d6121a3a05509de704b339ad5a16efd9334 (patch)
tree9fa2bc1d96d6e985f6c1fbae2253756b9d44a636
parentFix selection box text color (diff)
downloadforge-test-0001.tar.gz
forge-test-0001.tar.zst
forge-test-0001.zip
-rw-r--r--Makefile2
-rw-r--r--forged/internal/config/config.go (renamed from forged/internal/unsorted/config.go)41
-rw-r--r--forged/internal/models/users.go (renamed from forged/internal/unsorted/users.go)9
-rw-r--r--forged/internal/repos/acl.go (renamed from forged/internal/unsorted/acl.go)9
-rw-r--r--forged/internal/repos/repos.go (renamed from forged/internal/unsorted/git_misc.go)11
-rw-r--r--forged/internal/server/fedauth.go (renamed from forged/internal/unsorted/fedauth.go)2
-rw-r--r--forged/internal/server/git_hooks_handle_linux.go (renamed from forged/internal/unsorted/git_hooks_handle_linux.go)55
-rw-r--r--forged/internal/server/git_hooks_handle_other.go (renamed from forged/internal/unsorted/git_hooks_handle_other.go)51
-rw-r--r--forged/internal/server/git_init.go (renamed from forged/internal/unsorted/git_init.go)2
-rw-r--r--forged/internal/server/git_plumbing.go (renamed from forged/internal/unsorted/git_plumbing.go)2
-rw-r--r--forged/internal/server/git_ref.go (renamed from forged/internal/unsorted/git_ref.go)2
-rw-r--r--forged/internal/server/lmtp_handle_patch.go (renamed from forged/internal/unsorted/lmtp_handle_patch.go)5
-rw-r--r--forged/internal/server/lmtp_server.go (renamed from forged/internal/unsorted/lmtp_server.go)2
-rw-r--r--forged/internal/server/server.go (renamed from forged/internal/unsorted/server.go)56
-rw-r--r--forged/internal/server/unsorted.go (renamed from forged/internal/unsorted/unsorted.go)4
-rw-r--r--forged/internal/server/version.go (renamed from forged/internal/unsorted/version.go)2
-rw-r--r--forged/internal/ssh/receive_pack.go (renamed from forged/internal/unsorted/ssh_handle_receive_pack.go)19
-rw-r--r--forged/internal/ssh/server.go (renamed from forged/internal/unsorted/ssh_server.go)49
-rw-r--r--forged/internal/ssh/upload_pack.go (renamed from forged/internal/unsorted/ssh_handle_upload_pack.go)2
-rw-r--r--forged/internal/ssh/utils.go (renamed from forged/internal/unsorted/ssh_utils.go)23
-rw-r--r--forged/internal/web/database.go (renamed from forged/internal/unsorted/database.go)2
-rw-r--r--forged/internal/web/http_auth.go (renamed from forged/internal/unsorted/http_auth.go)2
-rw-r--r--forged/internal/web/http_handle_branches.go (renamed from forged/internal/unsorted/http_handle_branches.go)2
-rw-r--r--forged/internal/web/http_handle_group_index.go (renamed from forged/internal/unsorted/http_handle_group_index.go)31
-rw-r--r--forged/internal/web/http_handle_index.go (renamed from forged/internal/unsorted/http_handle_index.go)6
-rw-r--r--forged/internal/web/http_handle_login.go (renamed from forged/internal/unsorted/http_handle_login.go)11
-rw-r--r--forged/internal/web/http_handle_repo_commit.go (renamed from forged/internal/unsorted/http_handle_repo_commit.go)9
-rw-r--r--forged/internal/web/http_handle_repo_contrib_index.go (renamed from forged/internal/unsorted/http_handle_repo_contrib_index.go)9
-rw-r--r--forged/internal/web/http_handle_repo_contrib_one.go (renamed from forged/internal/unsorted/http_handle_repo_contrib_one.go)21
-rw-r--r--forged/internal/web/http_handle_repo_index.go (renamed from forged/internal/unsorted/http_handle_repo_index.go)10
-rw-r--r--forged/internal/web/http_handle_repo_info.go (renamed from forged/internal/unsorted/http_handle_repo_info.go)2
-rw-r--r--forged/internal/web/http_handle_repo_log.go (renamed from forged/internal/unsorted/http_handle_repo_log.go)10
-rw-r--r--forged/internal/web/http_handle_repo_raw.go (renamed from forged/internal/unsorted/http_handle_repo_raw.go)12
-rw-r--r--forged/internal/web/http_handle_repo_tree.go (renamed from forged/internal/unsorted/http_handle_repo_tree.go)12
-rw-r--r--forged/internal/web/http_handle_repo_upload_pack.go (renamed from forged/internal/unsorted/http_handle_repo_upload_pack.go)10
-rw-r--r--forged/internal/web/http_handle_users.go (renamed from forged/internal/unsorted/http_handle_users.go)6
-rw-r--r--forged/internal/web/remote_url.go (renamed from forged/internal/unsorted/remote_url.go)2
-rw-r--r--forged/internal/web/resources.go (renamed from forged/internal/unsorted/resources.go)2
-rw-r--r--forged/internal/web/server.go (renamed from forged/internal/unsorted/http_server.go)42
-rw-r--r--forged/internal/web/template.go (renamed from forged/internal/unsorted/http_template.go)2
-rw-r--r--forged/internal/web/web.go48
-rw-r--r--forged/main.go4
42 files changed, 337 insertions, 266 deletions
diff --git a/Makefile b/Makefile
index b606a12..8c8c4da 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@ EMBED = git2d/git2d hookc/hookc $(wildcard LICENSE*) $(wildcard forged/static/*)
EMBED_ = $(EMBED:%=forged/internal/embed/%)
forge: $(EMBED_) $(SOURCE_FILES)
- CGO_ENABLED=0 go build -o forge -ldflags '-extldflags "-f no-PIC -static" -X "go.lindenii.runxiyu.org/forge/forged/internal/unsorted.version=$(VERSION)"' -tags 'osusergo netgo static_build' ./forged
+ CGO_ENABLED=0 go build -o forge -ldflags '-extldflags "-f no-PIC -static" -X "go.lindenii.runxiyu.org/forge/forged/internal/server.version=$(VERSION)"' -tags 'osusergo netgo static_build' ./forged
utils/colb:
diff --git a/forged/internal/unsorted/config.go b/forged/internal/config/config.go
index 9f07480..6eb36ab 100644
--- a/forged/internal/unsorted/config.go
+++ b/forged/internal/config/config.go
@@ -1,19 +1,18 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package config
import (
"bufio"
- "errors"
"log/slog"
"os"
- "go.lindenii.runxiyu.org/forge/forged/internal/database"
"go.lindenii.runxiyu.org/forge/forged/internal/irc"
"go.lindenii.runxiyu.org/forge/forged/internal/scfg"
)
+// Config holds runtime configuration for the Forge server.
type Config struct {
HTTP struct {
Net string `scfg:"net"`
@@ -61,34 +60,24 @@ type Config struct {
} `scfg:"pprof"`
}
-// LoadConfig loads a configuration file from the specified path and unmarshals
-// it to the global [config] struct. This may race with concurrent reads from
-// [config]; additional synchronization is necessary if the configuration is to
-// be made reloadable.
-func (s *Server) loadConfig(path string) (err error) {
- var configFile *os.File
- if configFile, err = os.Open(path); err != nil {
- return err
+// Load reads the configuration file from the given path and unmarshals it into
+// a Config value.
+func Load(path string) (Config, error) {
+ var cfg Config
+
+ f, err := os.Open(path)
+ if err != nil {
+ return cfg, err
}
- defer configFile.Close()
+ defer f.Close()
- decoder := scfg.NewDecoder(bufio.NewReader(configFile))
- if err = decoder.Decode(&s.config); err != nil {
- return err
+ decoder := scfg.NewDecoder(bufio.NewReader(f))
+ if err = decoder.Decode(&cfg); err != nil {
+ return cfg, err
}
for _, u := range decoder.UnknownDirectives() {
slog.Warn("unknown configuration directive", "directive", u)
}
- if s.config.DB.Type != "postgres" {
- return errors.New("unsupported database type")
- }
-
- if s.database, err = database.Open(s.config.DB.Conn); err != nil {
- return err
- }
-
- s.globalData["forge_title"] = s.config.General.Title
-
- return nil
+ return cfg, nil
}
diff --git a/forged/internal/unsorted/users.go b/forged/internal/models/users.go
index 0f72eed..61c77a3 100644
--- a/forged/internal/unsorted/users.go
+++ b/forged/internal/models/users.go
@@ -1,21 +1,22 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package models
import (
"context"
"github.com/jackc/pgx/v5"
+ "go.lindenii.runxiyu.org/forge/forged/internal/database"
)
-// addUserSSH adds a new user solely based on their SSH public key.
+// AddUserSSH adds a new user solely based on their SSH public key.
//
// TODO: Audit all users of this function.
-func (s *Server) addUserSSH(ctx context.Context, pubkey string) (userID int, err error) {
+func AddUserSSH(ctx context.Context, db database.Database, pubkey string) (userID int, err error) {
var txn pgx.Tx
- if txn, err = s.database.Begin(ctx); err != nil {
+ if txn, err = db.Begin(ctx); err != nil {
return
}
defer func() {
diff --git a/forged/internal/unsorted/acl.go b/forged/internal/repos/acl.go
index c2e887d..0727e91 100644
--- a/forged/internal/unsorted/acl.go
+++ b/forged/internal/repos/acl.go
@@ -1,20 +1,21 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package repos
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
+ "go.lindenii.runxiyu.org/forge/forged/internal/database"
)
-// getRepoInfo returns the filesystem path and direct access permission for a
+// GetInfo returns the filesystem path and direct access permission for a
// given repo and a provided ssh public key.
//
// TODO: Revamp.
-func (s *Server) getRepoInfo(ctx context.Context, groupPath []string, repoName, sshPubkey string) (repoID int, fsPath string, access bool, contribReq, userType string, userID int, err error) {
- err = s.database.QueryRow(ctx, `
+func GetInfo(ctx context.Context, db database.Database, groupPath []string, repoName, sshPubkey string) (repoID int, fsPath string, access bool, contribReq, userType string, userID int, err error) {
+ err = db.QueryRow(ctx, `
WITH RECURSIVE group_path_cte AS (
-- Start: match the first name in the path where parent_group IS NULL
SELECT
diff --git a/forged/internal/unsorted/git_misc.go b/forged/internal/repos/repos.go
index dd93726..e2a9467 100644
--- a/forged/internal/unsorted/git_misc.go
+++ b/forged/internal/repos/repos.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package repos
import (
"context"
@@ -12,15 +12,16 @@ import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/jackc/pgx/v5/pgtype"
+ "go.lindenii.runxiyu.org/forge/forged/internal/database"
)
-// openRepo opens a git repository by group and repo name.
+// Open opens a git repository by group and repo name.
//
// TODO: This should be deprecated in favor of doing it in the relevant
// request/router context in the future, as it cannot cover the nuance of
// fields needed.
-func (s *Server) openRepo(ctx context.Context, groupPath []string, repoName string) (repo *git.Repository, description string, repoID int, fsPath string, err error) {
- err = s.database.QueryRow(ctx, `
+func Open(ctx context.Context, db database.Database, groupPath []string, repoName string) (repo *git.Repository, description string, repoID int, fsPath string, err error) {
+ err = db.QueryRow(ctx, `
WITH RECURSIVE group_path_cte AS (
-- Start: match the first name in the path where parent_group IS NULL
SELECT
@@ -67,7 +68,7 @@ WHERE g.depth = cardinality($1::text[])
// The pointer to error is guaranteed to be populated with either nil or the
// error returned by the commit iterator after the returned iterator is
// finished.
-func commitIterSeqErr(ctx context.Context, commitIter object.CommitIter) (iter.Seq[*object.Commit], *error) {
+func CommitIterSeqErr(ctx context.Context, commitIter object.CommitIter) (iter.Seq[*object.Commit], *error) {
var err error
return func(yield func(*object.Commit) bool) {
for {
diff --git a/forged/internal/unsorted/fedauth.go b/forged/internal/server/fedauth.go
index f54649b..6948767 100644
--- a/forged/internal/unsorted/fedauth.go
+++ b/forged/internal/server/fedauth.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package server
import (
"bufio"
diff --git a/forged/internal/unsorted/git_hooks_handle_linux.go b/forged/internal/server/git_hooks_handle_linux.go
index f904550..e36e12e 100644
--- a/forged/internal/unsorted/git_hooks_handle_linux.go
+++ b/forged/internal/server/git_hooks_handle_linux.go
@@ -3,7 +3,7 @@
//
//go:build linux
-package unsorted
+package server
import (
"bytes"
@@ -24,6 +24,7 @@ import (
"github.com/jackc/pgx/v5"
"go.lindenii.runxiyu.org/forge/forged/internal/ansiec"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
+ "go.lindenii.runxiyu.org/forge/forged/internal/ssh"
)
var (
@@ -39,7 +40,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
var ucred *syscall.Ucred
var err error
var cookie []byte
- var packPass packPass
+ var packPass ssh.PackPass
var sshStderr io.Writer
var hookRet byte
@@ -53,7 +54,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
if _, err = conn.Write([]byte{1}); err != nil {
return
}
- writeRedError(conn, "\nUnable to get peer credentials: %v", err)
+ ssh.WriteRedError(conn, "\nUnable to get peer credentials: %v", err)
return
}
uint32uid := uint32(os.Getuid()) //#nosec G115
@@ -61,7 +62,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
if _, err = conn.Write([]byte{1}); err != nil {
return
}
- writeRedError(conn, "\nUID mismatch")
+ ssh.WriteRedError(conn, "\nUID mismatch")
return
}
@@ -70,7 +71,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
if _, err = conn.Write([]byte{1}); err != nil {
return
}
- writeRedError(conn, "\nFailed to read cookie: %v", err)
+ ssh.WriteRedError(conn, "\nFailed to read cookie: %v", err)
return
}
@@ -81,7 +82,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
if _, err = conn.Write([]byte{1}); err != nil {
return
}
- writeRedError(conn, "\nInvalid handler cookie")
+ ssh.WriteRedError(conn, "\nInvalid handler cookie")
return
}
}
@@ -93,7 +94,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
hookRet = func() byte {
var argc64 uint64
if err = binary.Read(conn, binary.NativeEndian, &argc64); err != nil {
- writeRedError(sshStderr, "Failed to read argc: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to read argc: %v", err)
return 1
}
var args []string
@@ -103,7 +104,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
nextByte := make([]byte, 1)
n, err := conn.Read(nextByte)
if err != nil || n != 1 {
- writeRedError(sshStderr, "Failed to read arg: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to read arg: %v", err)
return 1
}
if nextByte[0] == 0 {
@@ -121,7 +122,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
nextByte := make([]byte, 1)
n, err := conn.Read(nextByte)
if err != nil || n != 1 {
- writeRedError(sshStderr, "Failed to read environment variable: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to read environment variable: %v", err)
return 1
}
if nextByte[0] == 0 {
@@ -135,7 +136,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
kv := envLine.String()
parts := strings.SplitN(kv, "=", 2)
if len(parts) < 2 {
- writeRedError(sshStderr, "Invalid environment variable line: %v", kv)
+ ssh.WriteRedError(sshStderr, "Invalid environment variable line: %v", kv)
return 1
}
gitEnv[parts[0]] = parts[1]
@@ -143,7 +144,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
var stdin bytes.Buffer
if _, err = io.Copy(&stdin, conn); err != nil {
- writeRedError(conn, "Failed to read to the stdin buffer: %v", err)
+ ssh.WriteRedError(conn, "Failed to read to the stdin buffer: %v", err)
}
switch filepath.Base(args[0]) {
@@ -161,7 +162,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
pushOptCount, err = strconv.Atoi(gitEnv["GIT_PUSH_OPTION_COUNT"])
if err != nil {
- writeRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err)
return 1
}
@@ -169,37 +170,37 @@ func (s *Server) hooksHandler(conn net.Conn) {
// Also it'd be nice to be able to combine users or whatever
if packPass.contribReq == "federated" && packPass.userType != "federated" && packPass.userType != "registered" {
if pushOptCount == 0 {
- writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
+ ssh.WriteRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
return 1
}
for pushOptIndex := range pushOptCount {
pushOpt, ok := gitEnv[fmt.Sprintf("GIT_PUSH_OPTION_%d", pushOptIndex)]
if !ok {
- writeRedError(sshStderr, "Failed to get push option %d", pushOptIndex)
+ ssh.WriteRedError(sshStderr, "Failed to get push option %d", pushOptIndex)
return 1
}
if strings.HasPrefix(pushOpt, "fedid=") {
fedUserID := strings.TrimPrefix(pushOpt, "fedid=")
service, username, found := strings.Cut(fedUserID, ":")
if !found {
- writeRedError(sshStderr, "Invalid federated user identifier %#v does not contain a colon", fedUserID)
+ ssh.WriteRedError(sshStderr, "Invalid federated user identifier %#v does not contain a colon", fedUserID)
return 1
}
ok, err := s.fedauth(ctx, packPass.userID, service, username, packPass.pubkey)
if err != nil {
- writeRedError(sshStderr, "Failed to verify federated user identifier %#v: %v", fedUserID, err)
+ ssh.WriteRedError(sshStderr, "Failed to verify federated user identifier %#v: %v", fedUserID, err)
return 1
}
if !ok {
- writeRedError(sshStderr, "Failed to verify federated user identifier %#v: you don't seem to be on the list", fedUserID)
+ ssh.WriteRedError(sshStderr, "Failed to verify federated user identifier %#v: you don't seem to be on the list", fedUserID)
return 1
}
break
}
if pushOptIndex == pushOptCount-1 {
- writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
+ ssh.WriteRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
return 1
}
}
@@ -209,20 +210,20 @@ func (s *Server) hooksHandler(conn net.Conn) {
if errors.Is(err, io.EOF) {
break
} else if err != nil {
- writeRedError(sshStderr, "Failed to read pre-receive line: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to read pre-receive line: %v", err)
return 1
}
line = line[:len(line)-1]
oldOID, rest, found = strings.Cut(line, " ")
if !found {
- writeRedError(sshStderr, "Invalid pre-receive line: %v", line)
+ ssh.WriteRedError(sshStderr, "Invalid pre-receive line: %v", line)
return 1
}
newIOID, refName, found = strings.Cut(rest, " ")
if !found {
- writeRedError(sshStderr, "Invalid pre-receive line: %v", line)
+ ssh.WriteRedError(sshStderr, "Invalid pre-receive line: %v", line)
return 1
}
@@ -243,7 +244,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
).Scan(&newMRLocalID)
}
if err != nil {
- writeRedError(sshStderr, "Error creating merge request: %v", err)
+ ssh.WriteRedError(sshStderr, "Error creating merge request: %v", err)
return 1
}
mergeRequestWebURL := fmt.Sprintf("%s/contrib/%d/", s.genHTTPRemoteURL(packPass.groupPath, packPass.repoName), newMRLocalID)
@@ -260,9 +261,9 @@ func (s *Server) hooksHandler(conn net.Conn) {
).Scan(&existingMRUser)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
- writeRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err)
+ ssh.WriteRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err)
} else {
- writeRedError(sshStderr, "Error querying for existing merge request: %v", err)
+ ssh.WriteRedError(sshStderr, "Error querying for existing merge request: %v", err)
}
return 1
}
@@ -281,7 +282,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
oldHash = plumbing.NewHash(oldOID)
if oldCommit, err = packPass.repo.CommitObject(oldHash); err != nil {
- writeRedError(sshStderr, "Daemon failed to get old commit: %v", err)
+ ssh.WriteRedError(sshStderr, "Daemon failed to get old commit: %v", err)
return 1
}
@@ -291,12 +292,12 @@ func (s *Server) hooksHandler(conn net.Conn) {
// any reason for this to only work intermitently.
newHash = plumbing.NewHash(newIOID)
if newCommit, err = packPass.repo.CommitObject(newHash); err != nil {
- writeRedError(sshStderr, "Daemon failed to get new commit: %v", err)
+ ssh.WriteRedError(sshStderr, "Daemon failed to get new commit: %v", err)
return 1
}
if isAncestor, err = oldCommit.IsAncestor(newCommit); err != nil {
- writeRedError(sshStderr, "Daemon failed to check if old commit is ancestor: %v", err)
+ ssh.WriteRedError(sshStderr, "Daemon failed to check if old commit is ancestor: %v", err)
return 1
}
diff --git a/forged/internal/unsorted/git_hooks_handle_other.go b/forged/internal/server/git_hooks_handle_other.go
index 70b2072..5849ce6 100644
--- a/forged/internal/unsorted/git_hooks_handle_other.go
+++ b/forged/internal/server/git_hooks_handle_other.go
@@ -3,7 +3,7 @@
//
//go:build !linux
-package unsorted
+package server
import (
"bytes"
@@ -22,6 +22,7 @@ import (
"github.com/jackc/pgx/v5"
"go.lindenii.runxiyu.org/forge/forged/internal/ansiec"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
+ "go.lindenii.runxiyu.org/forge/forged/internal/ssh"
)
// hooksHandler handles a connection from hookc via the
@@ -31,7 +32,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
var cancel context.CancelFunc
var err error
var cookie []byte
- var packPass packPass
+ var packPass ssh.PackPass
var sshStderr io.Writer
var hookRet byte
@@ -46,7 +47,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
if _, err = conn.Write([]byte{1}); err != nil {
return
}
- writeRedError(conn, "\nFailed to read cookie: %v", err)
+ ssh.WriteRedError(conn, "\nFailed to read cookie: %v", err)
return
}
@@ -57,7 +58,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
if _, err = conn.Write([]byte{1}); err != nil {
return
}
- writeRedError(conn, "\nInvalid handler cookie")
+ ssh.WriteRedError(conn, "\nInvalid handler cookie")
return
}
}
@@ -69,7 +70,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
hookRet = func() byte {
var argc64 uint64
if err = binary.Read(conn, binary.NativeEndian, &argc64); err != nil {
- writeRedError(sshStderr, "Failed to read argc: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to read argc: %v", err)
return 1
}
var args []string
@@ -79,7 +80,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
nextByte := make([]byte, 1)
n, err := conn.Read(nextByte)
if err != nil || n != 1 {
- writeRedError(sshStderr, "Failed to read arg: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to read arg: %v", err)
return 1
}
if nextByte[0] == 0 {
@@ -97,7 +98,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
nextByte := make([]byte, 1)
n, err := conn.Read(nextByte)
if err != nil || n != 1 {
- writeRedError(sshStderr, "Failed to read environment variable: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to read environment variable: %v", err)
return 1
}
if nextByte[0] == 0 {
@@ -111,7 +112,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
kv := envLine.String()
parts := strings.SplitN(kv, "=", 2)
if len(parts) < 2 {
- writeRedError(sshStderr, "Invalid environment variable line: %v", kv)
+ ssh.WriteRedError(sshStderr, "Invalid environment variable line: %v", kv)
return 1
}
gitEnv[parts[0]] = parts[1]
@@ -119,7 +120,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
var stdin bytes.Buffer
if _, err = io.Copy(&stdin, conn); err != nil {
- writeRedError(conn, "Failed to read to the stdin buffer: %v", err)
+ ssh.WriteRedError(conn, "Failed to read to the stdin buffer: %v", err)
}
switch filepath.Base(args[0]) {
@@ -137,7 +138,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
pushOptCount, err = strconv.Atoi(gitEnv["GIT_PUSH_OPTION_COUNT"])
if err != nil {
- writeRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err)
return 1
}
@@ -145,37 +146,37 @@ func (s *Server) hooksHandler(conn net.Conn) {
// Also it'd be nice to be able to combine users or whatever
if packPass.contribReq == "federated" && packPass.userType != "federated" && packPass.userType != "registered" {
if pushOptCount == 0 {
- writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
+ ssh.WriteRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
return 1
}
for pushOptIndex := range pushOptCount {
pushOpt, ok := gitEnv[fmt.Sprintf("GIT_PUSH_OPTION_%d", pushOptIndex)]
if !ok {
- writeRedError(sshStderr, "Failed to get push option %d", pushOptIndex)
+ ssh.WriteRedError(sshStderr, "Failed to get push option %d", pushOptIndex)
return 1
}
if strings.HasPrefix(pushOpt, "fedid=") {
fedUserID := strings.TrimPrefix(pushOpt, "fedid=")
service, username, found := strings.Cut(fedUserID, ":")
if !found {
- writeRedError(sshStderr, "Invalid federated user identifier %#v does not contain a colon", fedUserID)
+ ssh.WriteRedError(sshStderr, "Invalid federated user identifier %#v does not contain a colon", fedUserID)
return 1
}
ok, err := s.fedauth(ctx, packPass.userID, service, username, packPass.pubkey)
if err != nil {
- writeRedError(sshStderr, "Failed to verify federated user identifier %#v: %v", fedUserID, err)
+ ssh.WriteRedError(sshStderr, "Failed to verify federated user identifier %#v: %v", fedUserID, err)
return 1
}
if !ok {
- writeRedError(sshStderr, "Failed to verify federated user identifier %#v: you don't seem to be on the list", fedUserID)
+ ssh.WriteRedError(sshStderr, "Failed to verify federated user identifier %#v: you don't seem to be on the list", fedUserID)
return 1
}
break
}
if pushOptIndex == pushOptCount-1 {
- writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
+ ssh.WriteRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
return 1
}
}
@@ -185,20 +186,20 @@ func (s *Server) hooksHandler(conn net.Conn) {
if errors.Is(err, io.EOF) {
break
} else if err != nil {
- writeRedError(sshStderr, "Failed to read pre-receive line: %v", err)
+ ssh.WriteRedError(sshStderr, "Failed to read pre-receive line: %v", err)
return 1
}
line = line[:len(line)-1]
oldOID, rest, found = strings.Cut(line, " ")
if !found {
- writeRedError(sshStderr, "Invalid pre-receive line: %v", line)
+ ssh.WriteRedError(sshStderr, "Invalid pre-receive line: %v", line)
return 1
}
newIOID, refName, found = strings.Cut(rest, " ")
if !found {
- writeRedError(sshStderr, "Invalid pre-receive line: %v", line)
+ ssh.WriteRedError(sshStderr, "Invalid pre-receive line: %v", line)
return 1
}
@@ -219,7 +220,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
).Scan(&newMRLocalID)
}
if err != nil {
- writeRedError(sshStderr, "Error creating merge request: %v", err)
+ ssh.WriteRedError(sshStderr, "Error creating merge request: %v", err)
return 1
}
mergeRequestWebURL := fmt.Sprintf("%s/contrib/%d/", s.genHTTPRemoteURL(packPass.groupPath, packPass.repoName), newMRLocalID)
@@ -236,9 +237,9 @@ func (s *Server) hooksHandler(conn net.Conn) {
).Scan(&existingMRUser)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
- writeRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err)
+ ssh.WriteRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err)
} else {
- writeRedError(sshStderr, "Error querying for existing merge request: %v", err)
+ ssh.WriteRedError(sshStderr, "Error querying for existing merge request: %v", err)
}
return 1
}
@@ -257,7 +258,7 @@ func (s *Server) hooksHandler(conn net.Conn) {
oldHash = plumbing.NewHash(oldOID)
if oldCommit, err = packPass.repo.CommitObject(oldHash); err != nil {
- writeRedError(sshStderr, "Daemon failed to get old commit: %v", err)
+ ssh.WriteRedError(sshStderr, "Daemon failed to get old commit: %v", err)
return 1
}
@@ -267,12 +268,12 @@ func (s *Server) hooksHandler(conn net.Conn) {
// any reason for this to only work intermitently.
newHash = plumbing.NewHash(newIOID)
if newCommit, err = packPass.repo.CommitObject(newHash); err != nil {
- writeRedError(sshStderr, "Daemon failed to get new commit: %v", err)
+ ssh.WriteRedError(sshStderr, "Daemon failed to get new commit: %v", err)
return 1
}
if isAncestor, err = oldCommit.IsAncestor(newCommit); err != nil {
- writeRedError(sshStderr, "Daemon failed to check if old commit is ancestor: %v", err)
+ ssh.WriteRedError(sshStderr, "Daemon failed to check if old commit is ancestor: %v", err)
return 1
}
diff --git a/forged/internal/unsorted/git_init.go b/forged/internal/server/git_init.go
index a9bba78..8ac036b 100644
--- a/forged/internal/unsorted/git_init.go
+++ b/forged/internal/server/git_init.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package server
import (
"github.com/go-git/go-git/v5"
diff --git a/forged/internal/unsorted/git_plumbing.go b/forged/internal/server/git_plumbing.go
index e7ebe8f..64569d8 100644
--- a/forged/internal/unsorted/git_plumbing.go
+++ b/forged/internal/server/git_plumbing.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package server
import (
"bytes"
diff --git a/forged/internal/unsorted/git_ref.go b/forged/internal/server/git_ref.go
index d9735ba..c459247 100644
--- a/forged/internal/unsorted/git_ref.go
+++ b/forged/internal/server/git_ref.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package server
import (
"github.com/go-git/go-git/v5"
diff --git a/forged/internal/unsorted/lmtp_handle_patch.go b/forged/internal/server/lmtp_handle_patch.go
index b258bfc..06096e8 100644
--- a/forged/internal/unsorted/lmtp_handle_patch.go
+++ b/forged/internal/server/lmtp_handle_patch.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package server
import (
"bytes"
@@ -17,6 +17,7 @@ import (
"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/go-git/go-git/v5"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
+ "go.lindenii.runxiyu.org/forge/forged/internal/repos"
)
func (s *Server) lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string, mbox io.Reader) (err error) {
@@ -33,7 +34,7 @@ func (s *Server) lmtpHandlePatch(session *lmtpSession, groupPath []string, repoN
var repo *git.Repository
var fsPath string
- repo, _, _, fsPath, err = s.openRepo(session.ctx, groupPath, repoName)
+ repo, _, _, fsPath, err = repos.Open(session.ctx, s.database, groupPath, repoName)
if err != nil {
return fmt.Errorf("failed to open repo: %w", err)
}
diff --git a/forged/internal/unsorted/lmtp_server.go b/forged/internal/server/lmtp_server.go
index 1e94894..83bad1e 100644
--- a/forged/internal/unsorted/lmtp_server.go
+++ b/forged/internal/server/lmtp_server.go
@@ -2,7 +2,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
// SPDX-FileCopyrightText: Copyright (c) 2024 Robin Jarry <robin@jarry.cc>
-package unsorted
+package server
import (
"bytes"
diff --git a/forged/internal/unsorted/server.go b/forged/internal/server/server.go
index 84379b0..4ec0b5c 100644
--- a/forged/internal/unsorted/server.go
+++ b/forged/internal/server/server.go
@@ -1,12 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package server
import (
"errors"
- "html/template"
- "io/fs"
"log"
"log/slog"
"net"
@@ -19,32 +17,30 @@ import (
"time"
"go.lindenii.runxiyu.org/forge/forged/internal/cmap"
+ "go.lindenii.runxiyu.org/forge/forged/internal/config"
"go.lindenii.runxiyu.org/forge/forged/internal/database"
"go.lindenii.runxiyu.org/forge/forged/internal/embed"
"go.lindenii.runxiyu.org/forge/forged/internal/irc"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
+ "go.lindenii.runxiyu.org/forge/forged/internal/ssh"
+ "go.lindenii.runxiyu.org/forge/forged/internal/web"
goSSH "golang.org/x/crypto/ssh"
)
type Server struct {
- config Config
+ config config.Config
database database.Database
- sourceHandler http.Handler
- staticHandler http.Handler
-
- // globalData is passed as "global" when rendering HTML templates.
- globalData map[string]any
-
serverPubkeyString string
serverPubkeyFP string
serverPubkey goSSH.PublicKey
// packPasses contains hook cookies mapped to their packPass.
- packPasses cmap.Map[string, packPass]
+ packPasses cmap.Map[string, ssh.PackPass]
- templates *template.Template
+ web *web.Server
+ ssh *ssh.Server
ircBot *irc.Bot
@@ -52,31 +48,25 @@ type Server struct {
}
func NewServer(configPath string) (*Server, error) {
- s := &Server{
- globalData: make(map[string]any),
- } //exhaustruct:ignore
-
- s.sourceHandler = http.StripPrefix(
- "/-/source/",
- http.FileServer(http.FS(embed.Source)),
- )
- staticFS, err := fs.Sub(embed.Resources, "forged/static")
+ s := &Server{} //exhaustruct:ignore
+ s.packPasses = cmap.Map[string, ssh.PackPass]{}
+
+ cfg, err := config.Load(configPath)
if err != nil {
return s, err
}
- s.staticHandler = http.StripPrefix("/-/static/", http.FileServer(http.FS(staticFS)))
- s.globalData = map[string]any{
- "server_public_key_string": &s.serverPubkeyString,
- "server_public_key_fingerprint": &s.serverPubkeyFP,
- "forge_version": version,
- // Some other ones are populated after config parsing
+ if cfg.DB.Type != "postgres" {
+ return s, errors.New("unsupported database type")
}
-
- if err := s.loadConfig(configPath); err != nil {
+ s.config = cfg
+ if s.database, err = database.Open(s.config.DB.Conn); err != nil {
return s, err
}
-
- misc.NoneOrPanic(s.loadTemplates())
+ s.web, err = web.New(s.config, s.database, &s.serverPubkeyString, &s.serverPubkeyFP, version)
+ if err != nil {
+ return s, err
+ }
+ s.ssh = ssh.New(s.config, s.database, &s.serverPubkeyString, &s.serverPubkeyFP, &s.packPasses)
misc.NoneOrPanic(misc.DeployBinary(misc.FirstOrPanic(embed.Resources.Open("git2d/git2d")), s.config.Git.DaemonPath))
misc.NoneOrPanic(misc.DeployBinary(misc.FirstOrPanic(embed.Resources.Open("hookc/hookc")), filepath.Join(s.config.Hooks.Execs, "pre-receive")))
misc.NoneOrPanic(os.Chmod(filepath.Join(s.config.Hooks.Execs, "pre-receive"), 0o755))
@@ -172,7 +162,7 @@ func (s *Server) Run() error {
}
slog.Info("listening SSH on", "net", s.config.SSH.Net, "addr", s.config.SSH.Addr)
go func() {
- if err = s.serveSSH(sshListener); err != nil {
+ if err = s.ssh.Serve(sshListener); err != nil {
slog.Error("serving SSH", "error", err)
os.Exit(1)
}
@@ -197,7 +187,7 @@ func (s *Server) Run() error {
os.Exit(1)
}
server := http.Server{
- Handler: s,
+ Handler: s.web,
ReadTimeout: time.Duration(s.config.HTTP.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(s.config.HTTP.ReadTimeout) * time.Second,
IdleTimeout: time.Duration(s.config.HTTP.ReadTimeout) * time.Second,
diff --git a/forged/internal/unsorted/unsorted.go b/forged/internal/server/unsorted.go
index f26b0e4..81ef88a 100644
--- a/forged/internal/unsorted/unsorted.go
+++ b/forged/internal/server/unsorted.go
@@ -1,5 +1,5 @@
// 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
+// Package server contains the core orchestration components of the forge.
+package server
diff --git a/forged/internal/unsorted/version.go b/forged/internal/server/version.go
index 52c0f32..da72389 100644
--- a/forged/internal/unsorted/version.go
+++ b/forged/internal/server/version.go
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package server
var version = "unknown"
diff --git a/forged/internal/unsorted/ssh_handle_receive_pack.go b/forged/internal/ssh/receive_pack.go
index a354273..57b46b0 100644
--- a/forged/internal/unsorted/ssh_handle_receive_pack.go
+++ b/forged/internal/ssh/receive_pack.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package ssh
import (
"errors"
@@ -11,23 +11,12 @@ import (
gliderSSH "github.com/gliderlabs/ssh"
"github.com/go-git/go-git/v5"
+ "go.lindenii.runxiyu.org/forge/forged/internal/models"
)
// packPass contains information known when handling incoming SSH connections
// that then needs to be used in hook socket connection handlers. See hookc(1).
-type packPass struct {
- session gliderSSH.Session
- repo *git.Repository
- pubkey string
- directAccess bool
- repoPath string
- userID int
- userType string
- repoID int
- groupPath []string
- repoName string
- contribReq string
-}
+type packPass = PackPass
// sshHandleRecvPack handles attempts to push to repos.
func (s *Server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdentifier string) (err error) {
@@ -72,7 +61,7 @@ func (s *Server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdenti
return errors.New("you need to have an SSH public key to push to this repo")
}
if userType == "" {
- userID, err = s.addUserSSH(session.Context(), pubkey)
+ userID, err = models.AddUserSSH(session.Context(), s.database, pubkey)
if err != nil {
return err
}
diff --git a/forged/internal/unsorted/ssh_server.go b/forged/internal/ssh/server.go
index 43cc0c4..b0b702d 100644
--- a/forged/internal/unsorted/ssh_server.go
+++ b/forged/internal/ssh/server.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package ssh
import (
"fmt"
@@ -11,15 +11,50 @@ import (
"strings"
gliderSSH "github.com/gliderlabs/ssh"
+ "github.com/go-git/go-git/v5"
"go.lindenii.runxiyu.org/forge/forged/internal/ansiec"
+ "go.lindenii.runxiyu.org/forge/forged/internal/cmap"
+ "go.lindenii.runxiyu.org/forge/forged/internal/config"
+ "go.lindenii.runxiyu.org/forge/forged/internal/database"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
goSSH "golang.org/x/crypto/ssh"
)
-// serveSSH serves SSH on a [net.Listener]. The listener should generally be a
+// PackPass contains information known when handling incoming SSH connections
+// that then needs to be used in hook socket connection handlers. See hookc(1).
+type PackPass struct {
+ session gliderSSH.Session
+ repo *git.Repository
+ pubkey string
+ directAccess bool
+ repoPath string
+ userID int
+ userType string
+ repoID int
+ groupPath []string
+ repoName string
+ contribReq string
+}
+
+// Server handles SSH connections.
+type Server struct {
+ config config.Config
+ database database.Database
+ serverPubkey goSSH.PublicKey
+ serverPubkeyString *string
+ serverPubkeyFP *string
+ packPasses *cmap.Map[string, PackPass]
+}
+
+// New creates a new SSH server.
+func New(cfg config.Config, db database.Database, pubkeyStr, pubkeyFP *string, passes *cmap.Map[string, PackPass]) *Server {
+ return &Server{config: cfg, database: db, serverPubkeyString: pubkeyStr, serverPubkeyFP: pubkeyFP, packPasses: passes}
+}
+
+// Serve serves SSH on a [net.Listener]. The listener should generally be a
// TCP listener, although AF_UNIX SOCK_STREAM listeners may be appropriate in
// rare cases.
-func (s *Server) serveSSH(listener net.Listener) error {
+func (s *Server) Serve(listener net.Listener) error {
var hostKeyBytes []byte
var hostKey goSSH.Signer
var err error
@@ -34,8 +69,12 @@ func (s *Server) serveSSH(listener net.Listener) error {
}
s.serverPubkey = hostKey.PublicKey()
- s.serverPubkeyString = misc.BytesToString(goSSH.MarshalAuthorizedKey(s.serverPubkey))
- s.serverPubkeyFP = goSSH.FingerprintSHA256(s.serverPubkey)
+ if s.serverPubkeyString != nil {
+ *s.serverPubkeyString = misc.BytesToString(goSSH.MarshalAuthorizedKey(s.serverPubkey))
+ }
+ if s.serverPubkeyFP != nil {
+ *s.serverPubkeyFP = goSSH.FingerprintSHA256(s.serverPubkey)
+ }
server = &gliderSSH.Server{
Handler: func(session gliderSSH.Session) {
diff --git a/forged/internal/unsorted/ssh_handle_upload_pack.go b/forged/internal/ssh/upload_pack.go
index 735a053..92589e0 100644
--- a/forged/internal/unsorted/ssh_handle_upload_pack.go
+++ b/forged/internal/ssh/upload_pack.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package ssh
import (
"fmt"
diff --git a/forged/internal/unsorted/ssh_utils.go b/forged/internal/ssh/utils.go
index 6f50a87..0153dd7 100644
--- a/forged/internal/unsorted/ssh_utils.go
+++ b/forged/internal/ssh/utils.go
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package ssh
import (
"context"
+ "crypto/rand"
+ "encoding/base64"
"errors"
"fmt"
"io"
@@ -12,6 +14,7 @@ import (
"go.lindenii.runxiyu.org/forge/forged/internal/ansiec"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
+ "go.lindenii.runxiyu.org/forge/forged/internal/repos"
)
var errIllegalSSHRepoPath = errors.New("illegal SSH repo path")
@@ -64,16 +67,28 @@ func (s *Server) getRepoInfo2(ctx context.Context, sshPath, sshPubkey string) (g
repoName = moduleName
switch moduleType {
case "repos":
- _1, _2, _3, _4, _5, _6, _7 := s.getRepoInfo(ctx, groupPath, moduleName, sshPubkey)
+ _1, _2, _3, _4, _5, _6, _7 := repos.GetInfo(ctx, s.database, groupPath, moduleName, sshPubkey)
return groupPath, repoName, _1, _2, _3, _4, _5, _6, _7
default:
return []string{}, "", 0, "", false, "", "", 0, errIllegalSSHRepoPath
}
}
-// writeRedError is a helper function that basically does a Fprintf but makes
+// WriteRedError is a helper function that basically does a Fprintf but makes
// the entire thing red, in terms of ANSI escape sequences. It's useful when
// producing error messages on SSH connections.
-func writeRedError(w io.Writer, format string, args ...any) {
+// WriteRedError writes a formatted error in red ANSI color.
+func WriteRedError(w io.Writer, format string, args ...any) {
fmt.Fprintln(w, ansiec.Red+fmt.Sprintf(format, args...)+ansiec.Reset)
}
+
+// randomUrlsafeStr generates a random string of the given entropic size
+// using the URL-safe base64 encoding. The actual size of the string returned
+// will be 4*sz.
+func randomUrlsafeStr(sz int) (string, error) {
+ r := make([]byte, 3*sz)
+ if _, err := rand.Read(r); err != nil {
+ return "", fmt.Errorf("error generating random string: %w", err)
+ }
+ return base64.RawURLEncoding.EncodeToString(r), nil
+}
diff --git a/forged/internal/unsorted/database.go b/forged/internal/web/database.go
index 222b0c4..aa3936d 100644
--- a/forged/internal/unsorted/database.go
+++ b/forged/internal/web/database.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"context"
diff --git a/forged/internal/unsorted/http_auth.go b/forged/internal/web/http_auth.go
index b0afa05..6946570 100644
--- a/forged/internal/unsorted/http_auth.go
+++ b/forged/internal/web/http_auth.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"net/http"
diff --git a/forged/internal/unsorted/http_handle_branches.go b/forged/internal/web/http_handle_branches.go
index 704e1d8..280daee 100644
--- a/forged/internal/unsorted/http_handle_branches.go
+++ b/forged/internal/web/http_handle_branches.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"net/http"
diff --git a/forged/internal/unsorted/http_handle_group_index.go b/forged/internal/web/http_handle_group_index.go
index ce28a1c..45f62bd 100644
--- a/forged/internal/unsorted/http_handle_group_index.go
+++ b/forged/internal/web/http_handle_group_index.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"errors"
@@ -12,7 +12,6 @@ import (
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
)
// httpHandleGroupIndex provides index pages for groups, which includes a list
@@ -61,10 +60,10 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.
).Scan(&groupID, &groupDesc)
if errors.Is(err, pgx.ErrNoRows) {
- web.ErrorPage404(s.templates, writer, params)
+ ErrorPage404(s.templates, writer, params)
return
} else if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting group: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting group: "+err.Error())
return
}
@@ -77,14 +76,14 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.
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())
+ 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")
+ ErrorPage403(s.templates, writer, params, "You do not have direct access to this group")
return
}
@@ -92,7 +91,7 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.
repoDesc := request.FormValue("repo_desc")
contribReq := request.FormValue("repo_contrib")
if repoName == "" {
- web.ErrorPage400(s.templates, writer, params, "Repo name is required")
+ ErrorPage400(s.templates, writer, params, "Repo name is required")
return
}
@@ -108,7 +107,7 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.
contribReq,
).Scan(&newRepoID)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error creating repo: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error creating repo: "+err.Error())
return
}
@@ -123,12 +122,12 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.
newRepoID,
)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error updating repo path: "+err.Error())
+ 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())
+ ErrorPage500(s.templates, writer, params, "Error initializing repo: "+err.Error())
return
}
@@ -144,7 +143,7 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.
WHERE group_id = $1
`, groupID)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error())
return
}
defer rows.Close()
@@ -152,13 +151,13 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.
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())
+ 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())
+ ErrorPage500(s.templates, writer, params, "Error getting repos: "+err.Error())
return
}
@@ -169,7 +168,7 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.
WHERE parent_group = $1
`, groupID)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error())
return
}
defer rows.Close()
@@ -177,13 +176,13 @@ func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.
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())
+ 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())
+ ErrorPage500(s.templates, writer, params, "Error getting subgroups: "+err.Error())
return
}
diff --git a/forged/internal/unsorted/http_handle_index.go b/forged/internal/web/http_handle_index.go
index a3141f4..e77155b 100644
--- a/forged/internal/unsorted/http_handle_index.go
+++ b/forged/internal/web/http_handle_index.go
@@ -1,12 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"net/http"
-
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
)
// httpHandleIndex provides the main index page which includes a list of groups
@@ -17,7 +15,7 @@ func (s *Server) httpHandleIndex(writer http.ResponseWriter, request *http.Reque
groups, err = s.queryNameDesc(request.Context(), "SELECT name, COALESCE(description, '') FROM groups WHERE parent_group IS NULL")
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error querying groups: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error querying groups: "+err.Error())
return
}
params["groups"] = groups
diff --git a/forged/internal/unsorted/http_handle_login.go b/forged/internal/web/http_handle_login.go
index 8adbe17..a18025e 100644
--- a/forged/internal/unsorted/http_handle_login.go
+++ b/forged/internal/web/http_handle_login.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"crypto/rand"
@@ -13,7 +13,6 @@ import (
"github.com/jackc/pgx/v5"
"go.lindenii.runxiyu.org/forge/forged/internal/argon2id"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
)
// httpHandleLogin provides the login page for local users.
@@ -46,7 +45,7 @@ func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque
s.renderTemplate(writer, "login", params)
return
}
- web.ErrorPage500(s.templates, writer, params, "Error querying user information: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error querying user information: "+err.Error())
return
}
if passwordHash == "" {
@@ -56,7 +55,7 @@ func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque
}
if passwordMatches, err = argon2id.ComparePasswordAndHash(password, passwordHash); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error comparing password and hash: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error comparing password and hash: "+err.Error())
return
}
@@ -67,7 +66,7 @@ func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque
}
if cookieValue, err = randomUrlsafeStr(16); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting random string: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting random string: "+err.Error())
return
}
@@ -88,7 +87,7 @@ func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Reque
_, err = s.database.Exec(request.Context(), "INSERT INTO sessions (user_id, session_id) VALUES ($1, $2)", userID, cookieValue)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error inserting session: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error inserting session: "+err.Error())
return
}
diff --git a/forged/internal/unsorted/http_handle_repo_commit.go b/forged/internal/web/http_handle_repo_commit.go
index 2afdf3a..ce3e799 100644
--- a/forged/internal/unsorted/http_handle_repo_commit.go
+++ b/forged/internal/web/http_handle_repo_commit.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"fmt"
@@ -15,7 +15,6 @@ import (
"github.com/go-git/go-git/v5/plumbing/object"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
"go.lindenii.runxiyu.org/forge/forged/internal/oldgit"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
)
// usableFilePatch is a [diff.FilePatch] that is structured in a way more
@@ -48,13 +47,13 @@ func (s *Server) httpHandleRepoCommit(writer http.ResponseWriter, request *http.
commitIDStrSpecNoSuffix = strings.TrimSuffix(commitIDStrSpec, ".patch")
commitID = plumbing.NewHash(commitIDStrSpecNoSuffix)
if commitObj, err = repo.CommitObject(commitID); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting commit object: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting commit object: "+err.Error())
return
}
if commitIDStrSpecNoSuffix != commitIDStrSpec {
var patchStr string
if patchStr, err = oldgit.FmtCommitPatch(commitObj); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error formatting patch: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error formatting patch: "+err.Error())
return
}
fmt.Fprintln(writer, patchStr)
@@ -72,7 +71,7 @@ func (s *Server) httpHandleRepoCommit(writer http.ResponseWriter, request *http.
parentCommitHash, patch, err = oldgit.CommitToPatch(commitObj)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting patch from commit: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting patch from commit: "+err.Error())
return
}
params["parent_commit_hash"] = parentCommitHash.String()
diff --git a/forged/internal/unsorted/http_handle_repo_contrib_index.go b/forged/internal/web/http_handle_repo_contrib_index.go
index 5c68c08..218e800 100644
--- a/forged/internal/unsorted/http_handle_repo_contrib_index.go
+++ b/forged/internal/web/http_handle_repo_contrib_index.go
@@ -1,13 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"net/http"
"github.com/jackc/pgx/v5"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
)
// idTitleStatus describes properties of a merge request that needs to be
@@ -28,7 +27,7 @@ func (s *Server) httpHandleRepoContribIndex(writer http.ResponseWriter, request
"SELECT repo_local_id, COALESCE(title, 'Untitled'), status FROM merge_requests WHERE repo_id = $1",
params["repo_id"],
); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error querying merge requests: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error querying merge requests: "+err.Error())
return
}
defer rows.Close()
@@ -37,13 +36,13 @@ func (s *Server) httpHandleRepoContribIndex(writer http.ResponseWriter, request
var mrID int
var mrTitle, mrStatus string
if err = rows.Scan(&mrID, &mrTitle, &mrStatus); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error scanning merge request: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error scanning merge request: "+err.Error())
return
}
result = append(result, idTitleStatus{mrID, mrTitle, mrStatus})
}
if err = rows.Err(); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error ranging over merge requests: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error ranging over merge requests: "+err.Error())
return
}
params["merge_requests"] = result
diff --git a/forged/internal/unsorted/http_handle_repo_contrib_one.go b/forged/internal/web/http_handle_repo_contrib_one.go
index 1d733b0..5178b29 100644
--- a/forged/internal/unsorted/http_handle_repo_contrib_one.go
+++ b/forged/internal/web/http_handle_repo_contrib_one.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"net/http"
@@ -10,7 +10,6 @@ import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
)
// httpHandleRepoContribOne provides an interface to each merge request of a
@@ -29,7 +28,7 @@ func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *h
mrIDStr = params["mr_id"].(string)
mrIDInt64, err := strconv.ParseInt(mrIDStr, 10, strconv.IntSize)
if err != nil {
- web.ErrorPage400(s.templates, writer, params, "Merge request ID not an integer")
+ ErrorPage400(s.templates, writer, params, "Merge request ID not an integer")
return
}
mrIDInt = int(mrIDInt64)
@@ -38,18 +37,18 @@ func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *h
"SELECT COALESCE(title, ''), status, source_ref, COALESCE(destination_branch, '') FROM merge_requests WHERE repo_id = $1 AND repo_local_id = $2",
params["repo_id"], mrIDInt,
).Scan(&title, &status, &srcRefStr, &dstBranchStr); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error querying merge request: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error querying merge request: "+err.Error())
return
}
repo = params["repo"].(*git.Repository)
if srcRefHash, err = getRefHash(repo, "branch", srcRefStr); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting source ref hash: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting source ref hash: "+err.Error())
return
}
if srcCommit, err = repo.CommitObject(srcRefHash); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting source commit: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting source commit: "+err.Error())
return
}
params["source_commit"] = srcCommit
@@ -61,23 +60,23 @@ func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *h
dstBranchHash, err = getRefHash(repo, "branch", dstBranchStr)
}
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting destination branch hash: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting destination branch hash: "+err.Error())
return
}
if dstCommit, err = repo.CommitObject(dstBranchHash); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting destination commit: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting destination commit: "+err.Error())
return
}
params["destination_commit"] = dstCommit
if mergeBases, err = srcCommit.MergeBase(dstCommit); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting merge base: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting merge base: "+err.Error())
return
}
if len(mergeBases) < 1 {
- web.ErrorPage500(s.templates, writer, params, "No merge base found for this merge request; these two branches do not share any common history")
+ ErrorPage500(s.templates, writer, params, "No merge base found for this merge request; these two branches do not share any common history")
// TODO
return
}
@@ -87,7 +86,7 @@ func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *h
patch, err := mergeBaseCommit.Patch(srcCommit)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting patch: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting patch: "+err.Error())
return
}
params["file_patches"] = makeUsableFilePatches(patch)
diff --git a/forged/internal/unsorted/http_handle_repo_index.go b/forged/internal/web/http_handle_repo_index.go
index dd46dfe..1c9a625 100644
--- a/forged/internal/unsorted/http_handle_repo_index.go
+++ b/forged/internal/web/http_handle_repo_index.go
@@ -1,14 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"net/http"
"go.lindenii.runxiyu.org/forge/forged/internal/git2c"
"go.lindenii.runxiyu.org/forge/forged/internal/render"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
+ "go.lindenii.runxiyu.org/forge/forged/internal/repos"
)
// httpHandleRepoIndex provides the front page of a repo using git2d.
@@ -16,18 +16,18 @@ func (s *Server) httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, p
repoName := params["repo_name"].(string)
groupPath := params["group_path"].([]string)
- _, repoPath, _, _, _, _, _ := s.getRepoInfo(req.Context(), groupPath, repoName, "") // TODO: Don't use getRepoInfo
+ _, repoPath, _, _, _, _, _, _ := repos.GetInfo(req.Context(), s.database, groupPath, repoName, "") // TODO: Don't use getRepoInfo
client, err := git2c.NewClient(s.config.Git.Socket)
if err != nil {
- web.ErrorPage500(s.templates, w, params, err.Error())
+ ErrorPage500(s.templates, w, params, err.Error())
return
}
defer client.Close()
commits, readme, err := client.CmdIndex(repoPath)
if err != nil {
- web.ErrorPage500(s.templates, w, params, err.Error())
+ ErrorPage500(s.templates, w, params, err.Error())
return
}
diff --git a/forged/internal/unsorted/http_handle_repo_info.go b/forged/internal/web/http_handle_repo_info.go
index e23b1d2..d608466 100644
--- a/forged/internal/unsorted/http_handle_repo_info.go
+++ b/forged/internal/web/http_handle_repo_info.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"fmt"
diff --git a/forged/internal/unsorted/http_handle_repo_log.go b/forged/internal/web/http_handle_repo_log.go
index 5d90871..f50d233 100644
--- a/forged/internal/unsorted/http_handle_repo_log.go
+++ b/forged/internal/web/http_handle_repo_log.go
@@ -1,14 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"net/http"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
+ "go.lindenii.runxiyu.org/forge/forged/internal/repos"
)
// httpHandleRepoLog provides a page with a complete Git log.
@@ -23,17 +23,17 @@ func (s *Server) httpHandleRepoLog(writer http.ResponseWriter, req *http.Request
repo = params["repo"].(*git.Repository)
if refHash, err = getRefHash(repo, params["ref_type"].(string), params["ref_name"].(string)); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting ref hash: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting ref hash: "+err.Error())
return
}
logOptions := git.LogOptions{From: refHash} //exhaustruct:ignore
commitIter, err := repo.Log(&logOptions)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error getting recent commits: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting recent commits: "+err.Error())
return
}
- params["commits"], params["commits_err"] = commitIterSeqErr(req.Context(), commitIter)
+ params["commits"], params["commits_err"] = repos.CommitIterSeqErr(req.Context(), commitIter)
s.renderTemplate(writer, "repo_log", params)
}
diff --git a/forged/internal/unsorted/http_handle_repo_raw.go b/forged/internal/web/http_handle_repo_raw.go
index 1127284..c496c99 100644
--- a/forged/internal/unsorted/http_handle_repo_raw.go
+++ b/forged/internal/web/http_handle_repo_raw.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"fmt"
@@ -11,7 +11,7 @@ import (
"go.lindenii.runxiyu.org/forge/forged/internal/git2c"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
+ "go.lindenii.runxiyu.org/forge/forged/internal/repos"
)
// httpHandleRepoRaw serves raw files, or directory listings that point to raw
@@ -23,18 +23,18 @@ func (s *Server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Req
pathSpec := strings.TrimSuffix(rawPathSpec, "/")
params["path_spec"] = pathSpec
- _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "")
+ _, repoPath, _, _, _, _, _, _ := repos.GetInfo(request.Context(), s.database, groupPath, repoName, "")
client, err := git2c.NewClient(s.config.Git.Socket)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, err.Error())
+ ErrorPage500(s.templates, writer, params, err.Error())
return
}
defer client.Close()
files, content, err := client.CmdTreeRaw(repoPath, pathSpec)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, err.Error())
+ ErrorPage500(s.templates, writer, params, err.Error())
return
}
@@ -51,6 +51,6 @@ func (s *Server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Req
writer.Header().Set("Content-Type", "application/octet-stream")
fmt.Fprint(writer, content)
default:
- web.ErrorPage500(s.templates, writer, params, "Unknown error fetching repo raw data")
+ ErrorPage500(s.templates, writer, params, "Unknown error fetching repo raw data")
}
}
diff --git a/forged/internal/unsorted/http_handle_repo_tree.go b/forged/internal/web/http_handle_repo_tree.go
index 4799ccb..73da431 100644
--- a/forged/internal/unsorted/http_handle_repo_tree.go
+++ b/forged/internal/web/http_handle_repo_tree.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"html/template"
@@ -10,7 +10,7 @@ import (
"go.lindenii.runxiyu.org/forge/forged/internal/git2c"
"go.lindenii.runxiyu.org/forge/forged/internal/render"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
+ "go.lindenii.runxiyu.org/forge/forged/internal/repos"
)
// httpHandleRepoTree provides a friendly, syntax-highlighted view of
@@ -24,18 +24,18 @@ func (s *Server) httpHandleRepoTree(writer http.ResponseWriter, request *http.Re
pathSpec := strings.TrimSuffix(rawPathSpec, "/")
params["path_spec"] = pathSpec
- _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "")
+ _, repoPath, _, _, _, _, _, _ := repos.GetInfo(request.Context(), s.database, groupPath, repoName, "")
client, err := git2c.NewClient(s.config.Git.Socket)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, err.Error())
+ ErrorPage500(s.templates, writer, params, err.Error())
return
}
defer client.Close()
files, content, err := client.CmdTreeRaw(repoPath, pathSpec)
if err != nil {
- web.ErrorPage500(s.templates, writer, params, err.Error())
+ ErrorPage500(s.templates, writer, params, err.Error())
return
}
@@ -50,6 +50,6 @@ func (s *Server) httpHandleRepoTree(writer http.ResponseWriter, request *http.Re
params["file_contents"] = rendered
s.renderTemplate(writer, "repo_tree_file", params)
default:
- web.ErrorPage500(s.templates, writer, params, "Unknown object type, something is seriously wrong")
+ ErrorPage500(s.templates, writer, params, "Unknown object type, something is seriously wrong")
}
}
diff --git a/forged/internal/unsorted/http_handle_repo_upload_pack.go b/forged/internal/web/http_handle_repo_upload_pack.go
index 914c9cc..cbb133b 100644
--- a/forged/internal/unsorted/http_handle_repo_upload_pack.go
+++ b/forged/internal/web/http_handle_repo_upload_pack.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"bytes"
@@ -108,11 +108,15 @@ func decodeBody(r *http.Request) (io.ReadCloser, error) {
return r.Body, nil
case "gzip":
zr, err := gzip.NewReader(r.Body)
- if err != nil { return nil, err }
+ if err != nil {
+ return nil, err
+ }
return zr, nil
case "deflate":
zr, err := zlib.NewReader(r.Body)
- if err != nil { return nil, err }
+ if err != nil {
+ return nil, err
+ }
return zr, nil
default:
return nil, fmt.Errorf("unsupported Content-Encoding: %q", ce)
diff --git a/forged/internal/unsorted/http_handle_users.go b/forged/internal/web/http_handle_users.go
index b41ee44..9d90553 100644
--- a/forged/internal/unsorted/http_handle_users.go
+++ b/forged/internal/web/http_handle_users.go
@@ -1,15 +1,13 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"net/http"
-
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
)
// httpHandleUsers is a useless stub.
func (s *Server) httpHandleUsers(writer http.ResponseWriter, _ *http.Request, params map[string]any) {
- web.ErrorPage501(s.templates, writer, params)
+ ErrorPage501(s.templates, writer, params)
}
diff --git a/forged/internal/unsorted/remote_url.go b/forged/internal/web/remote_url.go
index f4d4c58..4650695 100644
--- a/forged/internal/unsorted/remote_url.go
+++ b/forged/internal/web/remote_url.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"net/url"
diff --git a/forged/internal/unsorted/resources.go b/forged/internal/web/resources.go
index 692b454..a0dd3e3 100644
--- a/forged/internal/unsorted/resources.go
+++ b/forged/internal/web/resources.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"html/template"
diff --git a/forged/internal/unsorted/http_server.go b/forged/internal/web/server.go
index f6a1794..3e43cb0 100644
--- a/forged/internal/unsorted/http_server.go
+++ b/forged/internal/web/server.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"errors"
@@ -13,7 +13,7 @@ import (
"github.com/jackc/pgx/v5"
"go.lindenii.runxiyu.org/forge/forged/internal/misc"
- "go.lindenii.runxiyu.org/forge/forged/internal/web"
+ "go.lindenii.runxiyu.org/forge/forged/internal/repos"
)
// ServeHTTP handles all incoming HTTP requests and routes them to the correct
@@ -40,7 +40,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
params := make(map[string]any)
if segments, _, err = misc.ParseReqURI(request.RequestURI); err != nil {
- web.ErrorPage400(s.templates, writer, params, "Error parsing request URI: "+err.Error())
+ ErrorPage400(s.templates, writer, params, "Error parsing request URI: "+err.Error())
return
}
dirMode := false
@@ -56,7 +56,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
userID, params["username"], err = s.getUserFromRequest(request)
params["user_id"] = userID
if err != nil && !errors.Is(err, http.ErrNoCookie) && !errors.Is(err, pgx.ErrNoRows) {
- web.ErrorPage500(s.templates, writer, params, "Error getting user info from request: "+err.Error())
+ ErrorPage500(s.templates, writer, params, "Error getting user info from request: "+err.Error())
return
}
@@ -68,7 +68,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
for _, v := range segments {
if strings.Contains(v, ":") {
- web.ErrorPage400Colon(s.templates, writer, params)
+ ErrorPage400Colon(s.templates, writer, params)
return
}
}
@@ -80,7 +80,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if segments[0] == "-" {
if len(segments) < 2 {
- web.ErrorPage404(s.templates, writer, params)
+ ErrorPage404(s.templates, writer, params)
return
} else if len(segments) == 2 && misc.RedirectDir(writer, request) {
return
@@ -105,7 +105,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
s.httpHandleUsers(writer, request, params)
return
default:
- web.ErrorPage404(s.templates, writer, params)
+ ErrorPage404(s.templates, writer, params)
return
}
}
@@ -138,10 +138,10 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
}
s.httpHandleGroupIndex(writer, request, params)
case len(segments) == sepIndex+1:
- web.ErrorPage404(s.templates, writer, params)
+ ErrorPage404(s.templates, writer, params)
return
case len(segments) == sepIndex+2:
- web.ErrorPage404(s.templates, writer, params)
+ ErrorPage404(s.templates, writer, params)
return
default:
moduleType = segments[sepIndex+1]
@@ -154,12 +154,12 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
switch segments[sepIndex+3] {
case "info":
if err = s.httpHandleRepoInfo(writer, request, params); err != nil {
- web.ErrorPage500(s.templates, writer, params, err.Error())
+ ErrorPage500(s.templates, writer, params, err.Error())
}
return
case "git-upload-pack":
if err = s.httpHandleUploadPack(writer, request, params); err != nil {
- web.ErrorPage500(s.templates, writer, params, err.Error())
+ ErrorPage500(s.templates, writer, params, err.Error())
}
return
}
@@ -169,13 +169,13 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if errors.Is(err, misc.ErrNoRefSpec) {
params["ref_type"] = ""
} else {
- web.ErrorPage400(s.templates, writer, params, "Error querying ref type: "+err.Error())
+ ErrorPage400(s.templates, writer, params, "Error querying ref type: "+err.Error())
return
}
}
- if params["repo"], params["repo_description"], params["repo_id"], _, err = s.openRepo(request.Context(), groupPath, moduleName); err != nil {
- web.ErrorPage500(s.templates, writer, params, "Error opening repo: "+err.Error())
+ if params["repo"], params["repo_description"], params["repo_id"], _, err = repos.Open(request.Context(), s.database, groupPath, moduleName); err != nil {
+ ErrorPage500(s.templates, writer, params, "Error opening repo: "+err.Error())
return
}
@@ -200,7 +200,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
switch repoFeature {
case "tree":
if misc.AnyContain(segments[sepIndex+4:], "/") {
- web.ErrorPage400(s.templates, writer, params, "Repo tree paths may not contain slashes in any segments")
+ ErrorPage400(s.templates, writer, params, "Repo tree paths may not contain slashes in any segments")
return
}
if dirMode {
@@ -220,7 +220,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
return
case "raw":
if misc.AnyContain(segments[sepIndex+4:], "/") {
- web.ErrorPage400(s.templates, writer, params, "Repo tree paths may not contain slashes in any segments")
+ ErrorPage400(s.templates, writer, params, "Repo tree paths may not contain slashes in any segments")
return
}
if dirMode {
@@ -234,7 +234,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
s.httpHandleRepoRaw(writer, request, params)
case "log":
if len(segments) > sepIndex+4 {
- web.ErrorPage400(s.templates, writer, params, "Too many parameters")
+ ErrorPage400(s.templates, writer, params, "Too many parameters")
return
}
if misc.RedirectDir(writer, request) {
@@ -243,7 +243,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
s.httpHandleRepoLog(writer, request, params)
case "commit":
if len(segments) != sepIndex+5 {
- web.ErrorPage400(s.templates, writer, params, "Incorrect number of parameters")
+ ErrorPage400(s.templates, writer, params, "Incorrect number of parameters")
return
}
if misc.RedirectNoDir(writer, request) {
@@ -262,14 +262,14 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
params["mr_id"] = segments[sepIndex+4]
s.httpHandleRepoContribOne(writer, request, params)
default:
- web.ErrorPage400(s.templates, writer, params, "Too many parameters")
+ ErrorPage400(s.templates, writer, params, "Too many parameters")
}
default:
- web.ErrorPage404(s.templates, writer, params)
+ ErrorPage404(s.templates, writer, params)
return
}
default:
- web.ErrorPage404(s.templates, writer, params)
+ ErrorPage404(s.templates, writer, params)
return
}
}
diff --git a/forged/internal/unsorted/http_template.go b/forged/internal/web/template.go
index db44e4c..1f4372a 100644
--- a/forged/internal/unsorted/http_template.go
+++ b/forged/internal/web/template.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-package unsorted
+package web
import (
"log/slog"
diff --git a/forged/internal/web/web.go b/forged/internal/web/web.go
index f4d15f8..b0f02cc 100644
--- a/forged/internal/web/web.go
+++ b/forged/internal/web/web.go
@@ -3,3 +3,51 @@
// Package web provides web-facing components of the forge.
package web
+
+import (
+ "html/template"
+ "io/fs"
+ "net/http"
+
+ "go.lindenii.runxiyu.org/forge/forged/internal/config"
+ "go.lindenii.runxiyu.org/forge/forged/internal/database"
+ "go.lindenii.runxiyu.org/forge/forged/internal/embed"
+ "go.lindenii.runxiyu.org/forge/forged/internal/misc"
+)
+
+// Server handles HTTP requests for the forge.
+type Server struct {
+ config config.Config
+ database database.Database
+ sourceHandler http.Handler
+ staticHandler http.Handler
+ templates *template.Template
+ globalData map[string]any
+}
+
+// New creates a new web server.
+func New(cfg config.Config, db database.Database, pubkeyStr, pubkeyFP *string, version string) (*Server, error) {
+ s := &Server{config: cfg, database: db}
+ s.globalData = map[string]any{
+ "server_public_key_string": pubkeyStr,
+ "server_public_key_fingerprint": pubkeyFP,
+ "forge_version": version,
+ "forge_title": cfg.General.Title,
+ }
+
+ s.sourceHandler = http.StripPrefix(
+ "/-/source/",
+ http.FileServer(http.FS(embed.Source)),
+ )
+ staticFS, err := fs.Sub(embed.Resources, "forged/static")
+ if err != nil {
+ return s, err
+ }
+ s.staticHandler = http.StripPrefix("/-/static/", http.FileServer(http.FS(staticFS)))
+
+ if err = s.loadTemplates(); err != nil {
+ return s, err
+ }
+
+ return s, nil
+}
diff --git a/forged/main.go b/forged/main.go
index fde15d1..d729e63 100644
--- a/forged/main.go
+++ b/forged/main.go
@@ -7,7 +7,7 @@ package main
import (
"flag"
- "go.lindenii.runxiyu.org/forge/forged/internal/unsorted"
+ "go.lindenii.runxiyu.org/forge/forged/internal/server"
)
func main() {
@@ -18,7 +18,7 @@ func main() {
)
flag.Parse()
- s, err := unsorted.NewServer(*configPath)
+ s, err := server.NewServer(*configPath)
if err != nil {
panic(err)
}