From b4b0d966340ad9c892f8b8912eebc6118eed7482 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sat, 05 Apr 2025 20:52:04 +0800 Subject: [PATCH] Use cmd/forge for the entry point --- Makefile | 2 +- acl.go | 6 +++--- cmd/forge/main.go | 32 ++++++++++++++++++++++++++++++++ config.go | 14 +++++++------- database.go | 6 +++--- fedauth.go | 6 +++--- git2d_deploy.go | 6 +++--- git_format_patch.go | 2 +- git_hooks_deploy.go | 10 +++++----- git_hooks_handle_linux.go | 16 ++++++++-------- git_hooks_handle_other.go | 2 +- git_init.go | 6 +++--- git_misc.go | 6 +++--- git_plumbing.go | 2 +- git_ref.go | 2 +- http_auth.go | 6 +++--- http_error_page.go | 2 +- http_handle_branches.go | 4 ++-- http_handle_group_index.go | 18 +++++++++--------- http_handle_index.go | 4 ++-- http_handle_login.go | 10 +++++----- http_handle_repo_commit.go | 2 +- http_handle_repo_contrib_index.go | 6 +++--- http_handle_repo_contrib_one.go | 6 +++--- http_handle_repo_index.go | 6 +++--- http_handle_repo_info.go | 6 +++--- http_handle_repo_log.go | 2 +- http_handle_repo_raw.go | 6 +++--- http_handle_repo_tree.go | 6 +++--- http_handle_repo_upload_pack.go | 8 ++++---- http_handle_users.go | 2 +- http_server.go | 14 +++++++------- http_template.go | 2 +- http_template_funcs.go | 2 +- irc.go | 32 ++++++++++++++++---------------- iter.go | 2 +- lmtp_handle_patch.go | 4 ++-- lmtp_server.go | 22 +++++++++++----------- main.go | 187 ----------------------------------------------------- remote_url.go | 10 +++++----- resources.go | 2 +- server.go | 199 ++++++++++++++++++++++++++++++++++++++++++++++++----- ssh_handle_receive_pack.go | 12 ++++++------ ssh_handle_upload_pack.go | 6 +++--- ssh_server.go | 14 +++++++------- ssh_utils.go | 4 ++-- users.go | 6 +++--- version.go | 2 +- diff --git a/Makefile b/Makefile index 0d464a41f6de033f03c27695da8101d6c0e27711..eb2df23974fd01fa2529ca8b4a7f59b01e97afe9 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ VERSION = $(shell git describe --tags --always --dirty) SOURCE_FILES = $(shell git ls-files) forge: source.tar.gz hookc/hookc git2d/git2d $(SOURCE_FILES) - CGO_ENABLED=0 go build -o forge -ldflags '-extldflags "-f no-PIC -static" -X "main.VERSION=$(VERSION)"' -tags 'osusergo netgo static_build' + CGO_ENABLED=0 go build -o forge -ldflags '-extldflags "-f no-PIC -static" -X "main.VERSION=$(VERSION)"' -tags 'osusergo netgo static_build' ./cmd/forge utils/colb: diff --git a/acl.go b/acl.go index dfe128a3f7857e7885d4e12259aaee2907c87cb3..c4bf29fe4f992004c6b15669619f93eed883b410 100644 --- a/acl.go +++ b/acl.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "context" @@ -13,8 +13,8 @@ // getRepoInfo 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 (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, ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT diff --git a/cmd/forge/main.go b/cmd/forge/main.go new file mode 100644 index 0000000000000000000000000000000000000000..102f4daaac9a2b06f530a2911a060a7cf84c1ec1 --- /dev/null +++ b/cmd/forge/main.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu + +package main + +import ( + "flag" + "log/slog" + "os" + + "go.lindenii.runxiyu.org/forge" +) + +func main() { + configPath := flag.String( + "config", + "/etc/lindenii/forge.scfg", + "path to configuration file", + ) + flag.Parse() + + s := forge.Server{} + + s.Setup() + + if err := s.LoadConfig(*configPath); err != nil { + slog.Error("loading configuration", "error", err) + os.Exit(1) + } + + s.Run() +} diff --git a/config.go b/config.go index ec8daf8c4fdcea6eca760b8fc402d62ea87205d2..84633eafa22b66ad06bb46b6e02ae95c5949d4c3 100644 --- a/config.go +++ b/config.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "bufio" @@ -67,7 +67,7 @@ Conn string `scfg:"conn"` } `scfg:"db"` } -// loadConfig loads a configuration file from the specified path and unmarshals +// 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. @@ -76,7 +76,7 @@ // TODO: Currently, it returns an error when the user specifies any unknown // configuration patterns, but silently ignores fields in the [config] struct // that is not present in the user's configuration file. We would prefer the // exact opposite behavior. -func (s *server) loadConfig(path string) (err error) { +func (s *Server) LoadConfig(path string) (err error) { var configFile *os.File if configFile, err = os.Open(path); err != nil { return err @@ -84,19 +84,19 @@ } defer configFile.Close() decoder := scfg.NewDecoder(bufio.NewReader(configFile)) - if err = decoder.Decode(&s.config); err != nil { + if err = decoder.Decode(&s.Config); err != nil { return err } - if s.config.DB.Type != "postgres" { + if s.Config.DB.Type != "postgres" { return errors.New("unsupported database type") } - if s.database, err = pgxpool.New(context.Background(), s.config.DB.Conn); err != nil { + if s.Database, err = pgxpool.New(context.Background(), s.Config.DB.Conn); err != nil { return err } - s.globalData["forge_title"] = s.config.General.Title + s.GlobalData["forge_title"] = s.Config.General.Title return nil } diff --git a/database.go b/database.go index 1ea075301c2327063048b4dbb2a774798d88baa2..eafad3326dc15a9e3ab2fb5ae25f78611acf7ebb 100644 --- a/database.go +++ b/database.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "context" @@ -18,10 +18,10 @@ // queryNameDesc is a helper function that executes a query and returns a // list of nameDesc results. The query must return two string arguments, i.e. a // name and a description. -func (s *server) queryNameDesc(ctx context.Context, query string, args ...any) (result []nameDesc, err error) { +func (s *Server) queryNameDesc(ctx context.Context, query string, args ...any) (result []nameDesc, err error) { var rows pgx.Rows - if rows, err = s.database.Query(ctx, query, args...); err != nil { + if rows, err = s.Database.Query(ctx, query, args...); err != nil { return nil, err } defer rows.Close() diff --git a/fedauth.go b/fedauth.go index 43cb4e32cd9c02152d9a95c3fa8bda8777291589..fd84047004b755bf7ade0e78ccd152a13e340286 100644 --- a/fedauth.go +++ b/fedauth.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "bufio" @@ -17,7 +17,7 @@ ) // fedauth checks whether a user's SSH public key matches the remote username // they claim to have on the service. If so, the association is recorded. -func (s *server) fedauth(ctx context.Context, userID int, service, remoteUsername, pubkey string) (bool, error) { +func (s *Server) fedauth(ctx context.Context, userID int, service, remoteUsername, pubkey string) (bool, error) { var err error matched := false @@ -77,7 +77,7 @@ return false, nil } var txn pgx.Tx - if txn, err = s.database.Begin(ctx); err != nil { + if txn, err = s.Database.Begin(ctx); err != nil { return false, err } defer func() { diff --git a/git2d_deploy.go b/git2d_deploy.go index ba63a1b6d4c603ae6426135646037287a2f5d1db..5a5f336b44411ebd5e36443d2ddbd077d8588088 100644 --- a/git2d_deploy.go +++ b/git2d_deploy.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "io" @@ -9,7 +9,7 @@ "io/fs" "os" ) -func (s *server) deployGit2D() (err error) { +func (s *Server) deployGit2D() (err error) { var srcFD fs.File var dstFD *os.File @@ -18,7 +18,7 @@ return err } defer srcFD.Close() - if dstFD, err = os.OpenFile(s.config.Git.DaemonPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755); err != nil { + if dstFD, err = os.OpenFile(s.Config.Git.DaemonPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755); err != nil { return err } defer dstFD.Close() diff --git a/git_format_patch.go b/git_format_patch.go index 79a7474210b3468af37ffc8c1aa41e4a97eab74a..5628ce13c82a4fc3a186dee168483b51ffdcc63b 100644 --- a/git_format_patch.go +++ b/git_format_patch.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "bytes" diff --git a/git_hooks_deploy.go b/git_hooks_deploy.go index 0cfb4f9d4e97404c78ba6d26a4098302bc6e4197..eeda995637ae65b045a982c863379e45115df30d 100644 --- a/git_hooks_deploy.go +++ b/git_hooks_deploy.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "errors" @@ -14,7 +14,7 @@ // deployHooks deploys the git hooks client to the filesystem. The git hooks // client is expected to be embedded in resourcesFS and must be pre-compiled // during the build process; see the Makefile. -func (s *server) deployHooks() (err error) { +func (s *Server) deployHooks() (err error) { err = func() (err error) { var srcFD fs.File var dstFD *os.File @@ -24,7 +24,7 @@ return err } defer srcFD.Close() - if dstFD, err = os.OpenFile(filepath.Join(s.config.Hooks.Execs, "hookc"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755); err != nil { + if dstFD, err = os.OpenFile(filepath.Join(s.Config.Hooks.Execs, "hookc"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755); err != nil { return err } defer dstFD.Close() @@ -41,14 +41,14 @@ } // Go's embed filesystems do not store permissions; but in any case, // they would need to be 0o755: - if err = os.Chmod(filepath.Join(s.config.Hooks.Execs, "hookc"), 0o755); err != nil { + if err = os.Chmod(filepath.Join(s.Config.Hooks.Execs, "hookc"), 0o755); err != nil { return err } for _, hookName := range []string{ "pre-receive", } { - if err = os.Symlink(filepath.Join(s.config.Hooks.Execs, "hookc"), filepath.Join(s.config.Hooks.Execs, hookName)); err != nil { + if err = os.Symlink(filepath.Join(s.Config.Hooks.Execs, "hookc"), filepath.Join(s.Config.Hooks.Execs, hookName)); err != nil { if !errors.Is(err, fs.ErrExist) { return err } diff --git a/git_hooks_handle_linux.go b/git_hooks_handle_linux.go index 097f236ac2cc7b86b2752cfb9a6fea9ef33cd995..11013029df5ccefca02dfcdd83f86660689035b1 100644 --- a/git_hooks_handle_linux.go +++ b/git_hooks_handle_linux.go @@ -3,7 +3,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu // //go:build linux -package main +package forge import ( "bytes" @@ -34,7 +34,7 @@ ) // hooksHandler handles a connection from hookc via the // unix socket. -func (s *server) hooksHandler(conn net.Conn) { +func (s *Server) hooksHandler(conn net.Conn) { var ctx context.Context var cancel context.CancelFunc var ucred *syscall.Ucred @@ -77,7 +77,7 @@ } { var ok bool - packPass, ok = s.packPasses.Load(misc.BytesToString(cookie)) + packPass, ok = s.PackPasses.Load(misc.BytesToString(cookie)) if !ok { if _, err = conn.Write([]byte{1}); err != nil { return @@ -233,12 +233,12 @@ fmt.Fprintln(sshStderr, ansiec.Blue+"POK"+ansiec.Reset, refName) var newMRLocalID int if packPass.userID != 0 { - err = s.database.QueryRow(ctx, + err = s.Database.QueryRow(ctx, "INSERT INTO merge_requests (repo_id, creator, source_ref, status) VALUES ($1, $2, $3, 'open') RETURNING repo_local_id", packPass.repoID, packPass.userID, strings.TrimPrefix(refName, "refs/heads/"), ).Scan(&newMRLocalID) } else { - err = s.database.QueryRow(ctx, + err = s.Database.QueryRow(ctx, "INSERT INTO merge_requests (repo_id, source_ref, status) VALUES ($1, $2, 'open') RETURNING repo_local_id", packPass.repoID, strings.TrimPrefix(refName, "refs/heads/"), ).Scan(&newMRLocalID) @@ -251,7 +251,7 @@ mergeRequestWebURL := fmt.Sprintf("%s/contrib/%d/", s.genHTTPRemoteURL(packPass.groupPath, packPass.repoName), newMRLocalID) fmt.Fprintln(sshStderr, ansiec.Blue+"Created merge request at", mergeRequestWebURL+ansiec.Reset) select { - case s.ircSendBuffered <- "PRIVMSG #chat :New merge request at " + mergeRequestWebURL: + case s.IrcSendBuffered <- "PRIVMSG #chat :New merge request at " + mergeRequestWebURL: default: slog.Error("IRC SendQ exceeded") } @@ -259,7 +259,7 @@ } else { // Existing contrib branch var existingMRUser int var isAncestor bool - err = s.database.QueryRow(ctx, + err = s.Database.QueryRow(ctx, "SELECT COALESCE(creator, 0) FROM merge_requests WHERE source_ref = $1 AND repo_id = $2", strings.TrimPrefix(refName, "refs/heads/"), packPass.repoID, ).Scan(&existingMRUser) @@ -342,7 +342,7 @@ // serveGitHooks handles connections on the specified network listener and // treats incoming connections as those from git hook handlers by spawning // sessions. The listener must be a SOCK_STREAM UNIX domain socket. The // function itself blocks. -func (s *server) serveGitHooks(listener net.Listener) error { +func (s *Server) serveGitHooks(listener net.Listener) error { for { conn, err := listener.Accept() if err != nil { diff --git a/git_hooks_handle_other.go b/git_hooks_handle_other.go index 687bd8f1c115c921ef519c8a67f2ad643aefcdfb..4a4328f7b99f1ffcf6f6fc7f0a7c45c745abc0f2 100644 --- a/git_hooks_handle_other.go +++ b/git_hooks_handle_other.go @@ -3,7 +3,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu // //go:build !linux -package main +package forge import ( "bytes" diff --git a/git_init.go b/git_init.go index 1800c5acf2ec026d306b60f357abb730cfe95f5e..b448451ad5051513d7e6daf92a9949baa8e61c6b 100644 --- a/git_init.go +++ b/git_init.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "github.com/go-git/go-git/v5" @@ -11,7 +11,7 @@ ) // gitInit initializes a bare git repository with the forge-deployed hooks // directory as the hooksPath. -func (s *server) gitInit(repoPath string) (err error) { +func (s *Server) gitInit(repoPath string) (err error) { var repo *git.Repository var gitConf *gitConfig.Config @@ -23,7 +23,7 @@ if gitConf, err = repo.Config(); err != nil { return err } - gitConf.Raw.SetOption("core", gitFmtConfig.NoSubsection, "hooksPath", s.config.Hooks.Execs) + gitConf.Raw.SetOption("core", gitFmtConfig.NoSubsection, "hooksPath", s.Config.Hooks.Execs) gitConf.Raw.SetOption("receive", gitFmtConfig.NoSubsection, "advertisePushOptions", "true") if err = repo.SetConfig(gitConf); err != nil { diff --git a/git_misc.go b/git_misc.go index 17f834c6cce7e4665bb7cb30cb3e8cdd37dc84f3..8ba10e1343841a9f17acc51a6c73dc4ca93ba77b 100644 --- a/git_misc.go +++ b/git_misc.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "context" @@ -22,8 +22,8 @@ // // 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 (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, ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT diff --git a/git_plumbing.go b/git_plumbing.go index 74c80ac982a140d5e8793406f6726a5b692a67d5..440de7c64b2a1668c5ea72631ce77b4996b793de 100644 --- a/git_plumbing.go +++ b/git_plumbing.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "bytes" diff --git a/git_ref.go b/git_ref.go index 921359ea1d4ffff6aab968b7c073f751891fc22b..476dde04bdc42da371e5789ba16b6b5affe66c18 100644 --- a/git_ref.go +++ b/git_ref.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "github.com/go-git/go-git/v5" diff --git a/http_auth.go b/http_auth.go index 5f0dc66cb2d8a55bf78aa6e079dd016fdf78609a..5ba278bb69141993ead568d6d23bf9362439912d 100644 --- a/http_auth.go +++ b/http_auth.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/http" @@ -9,14 +9,14 @@ ) // getUserFromRequest returns the user ID and username associated with the // session cookie in a given [http.Request]. -func (s *server) getUserFromRequest(request *http.Request) (id int, username string, err error) { +func (s *Server) getUserFromRequest(request *http.Request) (id int, username string, err error) { var sessionCookie *http.Cookie if sessionCookie, err = request.Cookie("session"); err != nil { return } - err = s.database.QueryRow( + err = s.Database.QueryRow( request.Context(), "SELECT user_id, COALESCE(username, '') FROM users u JOIN sessions s ON u.id = s.user_id WHERE s.session_id = $1;", sessionCookie.Value, diff --git a/http_error_page.go b/http_error_page.go index 00ef04b5d5cc378b83cc73eb36c167642174d461..0cce72e82577782a70f7c173cdf98a255755daa1 100644 --- a/http_error_page.go +++ b/http_error_page.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/http" diff --git a/http_handle_branches.go b/http_handle_branches.go index 01a162a3a6322172129d669d1261e772bde0960a..96c4ac7f4edf3c0f225de95eedc0b2684d0b2527 100644 --- a/http_handle_branches.go +++ b/http_handle_branches.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/http" @@ -14,7 +14,7 @@ "go.lindenii.runxiyu.org/forge/misc" ) // httpHandleRepoBranches provides the branches page in repos. -func (s *server) httpHandleRepoBranches(writer http.ResponseWriter, _ *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoBranches(writer http.ResponseWriter, _ *http.Request, params map[string]any) { var repo *git.Repository var repoName string var groupPath []string diff --git a/http_handle_group_index.go b/http_handle_group_index.go index 46f1f6a0f68937da5fc8e541aac721aa4897dc79..cc3386064b89c60f7dd22f40eb963fb943d2df68 100644 --- a/http_handle_group_index.go +++ b/http_handle_group_index.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "errors" @@ -17,7 +17,7 @@ // 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) { +func (s *Server) httpHandleGroupIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { var groupPath []string var repos []nameDesc var subgroups []nameDesc @@ -28,7 +28,7 @@ groupPath = params["group_path"].([]string) // The group itself - err = s.database.QueryRow(request.Context(), ` + err = s.Database.QueryRow(request.Context(), ` WITH RECURSIVE group_path_cte AS ( SELECT id, @@ -69,7 +69,7 @@ } // ACL var count int - err = s.database.QueryRow(request.Context(), ` + err = s.Database.QueryRow(request.Context(), ` SELECT COUNT(*) FROM user_group_roles WHERE user_id = $1 @@ -96,7 +96,7 @@ return } var newRepoID int - err := s.database.QueryRow( + err := s.Database.QueryRow( request.Context(), `INSERT INTO repos (name, description, group_id, contrib_requirements) VALUES ($1, $2, $3, $4) @@ -111,9 +111,9 @@ errorPage500(writer, params, "Error creating repo: "+err.Error()) return } - filePath := filepath.Join(s.config.Git.RepoDir, strconv.Itoa(newRepoID)+".git") + filePath := filepath.Join(s.Config.Git.RepoDir, strconv.Itoa(newRepoID)+".git") - _, err = s.database.Exec( + _, err = s.Database.Exec( request.Context(), `UPDATE repos SET filesystem_path = $1 @@ -137,7 +137,7 @@ } // Repos var rows pgx.Rows - rows, err = s.database.Query(request.Context(), ` + rows, err = s.Database.Query(request.Context(), ` SELECT name, COALESCE(description, '') FROM repos WHERE group_id = $1 @@ -162,7 +162,7 @@ return } // Subgroups - rows, err = s.database.Query(request.Context(), ` + rows, err = s.Database.Query(request.Context(), ` SELECT name, COALESCE(description, '') FROM groups WHERE parent_group = $1 diff --git a/http_handle_index.go b/http_handle_index.go index 755e7c4bb53f65d730b1fe51cecfca90fb28df91..a519a5a7099e1de29de74181e508261e498b8497 100644 --- a/http_handle_index.go +++ b/http_handle_index.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/http" @@ -12,7 +12,7 @@ ) // httpHandleIndex provides the main index page which includes a list of groups // and some global information such as SSH keys. -func (s *server) httpHandleIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { var err error var groups []nameDesc diff --git a/http_handle_login.go b/http_handle_login.go index 10bfdcd46a4a5f4325ae8fda55452ad2a551d211..e02ba10861658678c82e9b8d04122974d5b989c3 100644 --- a/http_handle_login.go +++ b/http_handle_login.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "crypto/rand" @@ -16,7 +16,7 @@ "github.com/jackc/pgx/v5" ) // httpHandleLogin provides the login page for local users. -func (s *server) httpHandleLogin(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleLogin(writer http.ResponseWriter, request *http.Request, params map[string]any) { var username, password string var userID int var passwordHash string @@ -35,7 +35,7 @@ username = request.PostFormValue("username") password = request.PostFormValue("password") - err = s.database.QueryRow(request.Context(), + err = s.Database.QueryRow(request.Context(), "SELECT id, COALESCE(password, '') FROM users WHERE username = $1", username, ).Scan(&userID, &passwordHash) @@ -71,7 +71,7 @@ return } now = time.Now() - expiry = now.Add(time.Duration(s.config.HTTP.CookieExpiry) * time.Second) + expiry = now.Add(time.Duration(s.Config.HTTP.CookieExpiry) * time.Second) cookie = http.Cookie{ Name: "session", @@ -85,7 +85,7 @@ } //exhaustruct:ignore http.SetCookie(writer, &cookie) - _, err = s.database.Exec(request.Context(), "INSERT INTO sessions (user_id, session_id) VALUES ($1, $2)", userID, cookieValue) + _, err = s.Database.Exec(request.Context(), "INSERT INTO sessions (user_id, session_id) VALUES ($1, $2)", userID, cookieValue) if err != nil { errorPage500(writer, params, "Error inserting session: "+err.Error()) return diff --git a/http_handle_repo_commit.go b/http_handle_repo_commit.go index a398dc2aa27e3163ac1b88627ac0173461e0d0ec..88ade4b47c1bb51eba14a0e3df03e38e714eb201 100644 --- a/http_handle_repo_commit.go +++ b/http_handle_repo_commit.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "fmt" diff --git a/http_handle_repo_contrib_index.go b/http_handle_repo_contrib_index.go index e0c8478962c42772b0d36f3bdd7432add2fa203e..f729cbe82ba7478c180a20f4274e6f6228a21f3e 100644 --- a/http_handle_repo_contrib_index.go +++ b/http_handle_repo_contrib_index.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/http" @@ -18,12 +18,12 @@ Status string } // httpHandleRepoContribIndex provides an index to merge requests of a repo. -func (s *server) httpHandleRepoContribIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoContribIndex(writer http.ResponseWriter, request *http.Request, params map[string]any) { var rows pgx.Rows var result []idTitleStatus var err error - if rows, err = s.database.Query(request.Context(), + if rows, err = s.Database.Query(request.Context(), "SELECT repo_local_id, COALESCE(title, 'Untitled'), status FROM merge_requests WHERE repo_id = $1", params["repo_id"], ); err != nil { diff --git a/http_handle_repo_contrib_one.go b/http_handle_repo_contrib_one.go index 0df749148527c1bb02893c3b8acc7629349ed35f..9a261a45ea57c1d22c9f21a41d49aee17a860736 100644 --- a/http_handle_repo_contrib_one.go +++ b/http_handle_repo_contrib_one.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/http" @@ -14,7 +14,7 @@ ) // httpHandleRepoContribOne provides an interface to each merge request of a // repo. -func (s *server) httpHandleRepoContribOne(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoContribOne(writer http.ResponseWriter, request *http.Request, params map[string]any) { var mrIDStr string var mrIDInt int var err error @@ -33,7 +33,7 @@ return } mrIDInt = int(mrIDInt64) - if err = s.database.QueryRow(request.Context(), + if err = s.Database.QueryRow(request.Context(), "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 { diff --git a/http_handle_repo_index.go b/http_handle_repo_index.go index c6338cfccd3fb1b6cb62b64018a4cfc3d4148e82..edba57bb5930bcfbd900f7df520c73f6ecb451e0 100644 --- a/http_handle_repo_index.go +++ b/http_handle_repo_index.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/http" @@ -19,13 +19,13 @@ Message string } // httpHandleRepoIndex provides the front page of a repo using git2d. -func (s *server) httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) _, repoPath, _, _, _, _, _ := s.getRepoInfo(req.Context(), groupPath, repoName, "") // TODO: Don't use getRepoInfo - client, err := git2c.NewClient(s.config.Git.Socket) + client, err := git2c.NewClient(s.Config.Git.Socket) if err != nil { errorPage500(w, params, err.Error()) return diff --git a/http_handle_repo_info.go b/http_handle_repo_info.go index b7b743800dc6a83c38ac0406008feaaee86f782b..e2080acf4216cd6d41740ae8cf24b11c16fe281e 100644 --- a/http_handle_repo_info.go +++ b/http_handle_repo_info.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "fmt" @@ -16,12 +16,12 @@ // httpHandleRepoInfo provides advertised refs of a repo for use in Git's Smart // HTTP protocol. // // TODO: Reject access from web browsers. -func (s *server) httpHandleRepoInfo(writer http.ResponseWriter, request *http.Request, params map[string]any) (err error) { +func (s *Server) httpHandleRepoInfo(writer http.ResponseWriter, request *http.Request, params map[string]any) (err error) { groupPath := params["group_path"].([]string) repoName := params["repo_name"].(string) var repoPath string - if err := s.database.QueryRow(request.Context(), ` + if err := s.Database.QueryRow(request.Context(), ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT diff --git a/http_handle_repo_log.go b/http_handle_repo_log.go index 5c69836d3d6f9407d169c5e61dd24bf0597382b6..b104491e3273589a4a1817a09162d6c76482dd8b 100644 --- a/http_handle_repo_log.go +++ b/http_handle_repo_log.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/http" diff --git a/http_handle_repo_raw.go b/http_handle_repo_raw.go index 570030fb33f035f6a5adb917e05ff2c1be226ccd..7e19e02d4baa96f0ad8ed16d77af31954c9ddd5b 100644 --- a/http_handle_repo_raw.go +++ b/http_handle_repo_raw.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "fmt" @@ -15,7 +15,7 @@ ) // httpHandleRepoRaw serves raw files, or directory listings that point to raw // files. -func (s *server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) rawPathSpec := params["rest"].(string) @@ -24,7 +24,7 @@ params["path_spec"] = pathSpec _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "") - client, err := git2c.NewClient(s.config.Git.Socket) + client, err := git2c.NewClient(s.Config.Git.Socket) if err != nil { errorPage500(writer, params, err.Error()) return diff --git a/http_handle_repo_tree.go b/http_handle_repo_tree.go index 7af6e3e75b0eb1844a6f860eaeb89ea91f3f89cb..e8e5ff805452cecf77e935282a9492bbe77a939b 100644 --- a/http_handle_repo_tree.go +++ b/http_handle_repo_tree.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "html/template" @@ -16,7 +16,7 @@ // httpHandleRepoTree provides a friendly, syntax-highlighted view of // individual files, and provides directory views that link to these files. // // TODO: Do not highlight files that are too large. -func (s *server) httpHandleRepoTree(writer http.ResponseWriter, request *http.Request, params map[string]any) { +func (s *Server) httpHandleRepoTree(writer http.ResponseWriter, request *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) rawPathSpec := params["rest"].(string) @@ -25,7 +25,7 @@ params["path_spec"] = pathSpec _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "") - client, err := git2c.NewClient(s.config.Git.Socket) + client, err := git2c.NewClient(s.Config.Git.Socket) if err != nil { errorPage500(writer, params, err.Error()) return diff --git a/http_handle_repo_upload_pack.go b/http_handle_repo_upload_pack.go index a6580a7720a2969944774d9ccdb3a06fc7e68262..4c7291b28cd23868696aa54f72134b4639adc988 100644 --- a/http_handle_repo_upload_pack.go +++ b/http_handle_repo_upload_pack.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "io" @@ -14,7 +14,7 @@ ) // httpHandleUploadPack handles incoming Git fetch/pull/clone's over the Smart // HTTP protocol. -func (s *server) httpHandleUploadPack(writer http.ResponseWriter, request *http.Request, params map[string]any) (err error) { +func (s *Server) httpHandleUploadPack(writer http.ResponseWriter, request *http.Request, params map[string]any) (err error) { var groupPath []string var repoName string var repoPath string @@ -24,7 +24,7 @@ var cmd *exec.Cmd groupPath, repoName = params["group_path"].([]string), params["repo_name"].(string) - if err := s.database.QueryRow(request.Context(), ` + if err := s.Database.QueryRow(request.Context(), ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT @@ -67,7 +67,7 @@ writer.Header().Set("Transfer-Encoding", "chunked") writer.WriteHeader(http.StatusOK) cmd = exec.Command("git", "upload-pack", "--stateless-rpc", repoPath) - cmd.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.config.Hooks.Socket) + cmd.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.Config.Hooks.Socket) if stdout, err = cmd.StdoutPipe(); err != nil { return err } diff --git a/http_handle_users.go b/http_handle_users.go index e02d4b280775ac3da7563e78b15b8b66bd411149..d3796247f444eca05433ec9e25993bf94337f8ad 100644 --- a/http_handle_users.go +++ b/http_handle_users.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/http" diff --git a/http_server.go b/http_server.go index 13ee4929b2c9fa782cd19c64faabd14bab29db63..fdb55b474c67fc9ead28fbdd3d8f26de3bc75b2b 100644 --- a/http_server.go +++ b/http_server.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "errors" @@ -19,9 +19,9 @@ // ServeHTTP handles all incoming HTTP requests and routes them to the correct // location. // // TODO: This function is way too large. -func (s *server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { +func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var remoteAddr string - if s.config.HTTP.ReverseProxy { + if s.Config.HTTP.ReverseProxy { remoteAddrs, ok := request.Header["X-Forwarded-For"] if ok && len(remoteAddrs) == 1 { remoteAddr = remoteAddrs[0] @@ -50,7 +50,7 @@ } params["url_segments"] = segments params["dir_mode"] = dirMode - params["global"] = s.globalData + params["global"] = s.GlobalData var userID int // 0 for none userID, params["username"], err = s.getUserFromRequest(request) params["user_id"] = userID @@ -87,10 +87,10 @@ } switch segments[1] { case "static": - s.staticHandler.ServeHTTP(writer, request) + s.StaticHandler.ServeHTTP(writer, request) return case "source": - s.sourceHandler.ServeHTTP(writer, request) + s.SourceHandler.ServeHTTP(writer, request) return } } @@ -183,7 +183,7 @@ for _, part := range segments[:sepIndex+3] { repoURLRoot = repoURLRoot + url.PathEscape(part) + "/" } params["repo_url_root"] = repoURLRoot - params["repo_patch_mailing_list"] = repoURLRoot[1:len(repoURLRoot)-1] + "@" + s.config.LMTP.Domain + params["repo_patch_mailing_list"] = repoURLRoot[1:len(repoURLRoot)-1] + "@" + s.Config.LMTP.Domain params["http_clone_url"] = s.genHTTPRemoteURL(groupPath, moduleName) params["ssh_clone_url"] = s.genSSHRemoteURL(groupPath, moduleName) diff --git a/http_template.go b/http_template.go index 9aa15cb92375caa0a42b01452972c24c7a4f38d5..f60f0261bf13e198d8f13e87ad270d1b6d3f08ef 100644 --- a/http_template.go +++ b/http_template.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "log/slog" diff --git a/http_template_funcs.go b/http_template_funcs.go index 526841f1de10f3c33efd01187a2a79fcf7e101ff..616afe24654e1d8b28333c7e485a5ee71e52c45d 100644 --- a/http_template_funcs.go +++ b/http_template_funcs.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/url" diff --git a/irc.go b/irc.go index 49fa28fccdaac74c7a89f445cbaabb1c5b71551b..298930f19cc6a22129fde1a06c722e9d8696cddb 100644 --- a/irc.go +++ b/irc.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "crypto/tls" @@ -16,13 +16,13 @@ content T errorBack chan error } -func (s *server) ircBotSession() error { +func (s *Server) ircBotSession() error { var err error var underlyingConn net.Conn - if s.config.IRC.TLS { - underlyingConn, err = tls.Dial(s.config.IRC.Net, s.config.IRC.Addr, nil) + if s.Config.IRC.TLS { + underlyingConn, err = tls.Dial(s.Config.IRC.Net, s.Config.IRC.Addr, nil) } else { - underlyingConn, err = net.Dial(s.config.IRC.Net, s.config.IRC.Addr) + underlyingConn, err = net.Dial(s.Config.IRC.Net, s.Config.IRC.Addr) } if err != nil { return err @@ -36,11 +36,11 @@ slog.Debug("irc tx", "line", s) return conn.WriteString(s + "\r\n") } - _, err = logAndWriteLn("NICK " + s.config.IRC.Nick) + _, err = logAndWriteLn("NICK " + s.Config.IRC.Nick) if err != nil { return err } - _, err = logAndWriteLn("USER " + s.config.IRC.User + " 0 * :" + s.config.IRC.Gecos) + _, err = logAndWriteLn("USER " + s.Config.IRC.User + " 0 * :" + s.Config.IRC.Gecos) if err != nil { return err } @@ -81,7 +81,7 @@ c, ok := msg.Source.(irc.Client) if !ok { slog.Error("unable to convert source of JOIN to client") } - if c.Nick != s.config.IRC.Nick { + if c.Nick != s.Config.IRC.Nick { continue } default: @@ -93,18 +93,18 @@ for { select { case err = <-readLoopError: return err - case line := <-s.ircSendBuffered: + case line := <-s.IrcSendBuffered: _, err = logAndWriteLn(line) if err != nil { select { - case s.ircSendBuffered <- line: + case s.IrcSendBuffered <- line: default: slog.Error("unable to requeue message", "line", line) } writeLoopAbort <- struct{}{} return err } - case lineErrorBack := <-s.ircSendDirectChan: + case lineErrorBack := <-s.IrcSendDirectChan: _, err = logAndWriteLn(lineErrorBack.content) lineErrorBack.errorBack <- err if err != nil { @@ -117,10 +117,10 @@ } // ircSendDirect sends an IRC message directly to the connection and bypasses // the buffering system. -func (s *server) ircSendDirect(line string) error { +func (s *Server) ircSendDirect(line string) error { ech := make(chan error, 1) - s.ircSendDirectChan <- errorBack[string]{ + s.IrcSendDirectChan <- errorBack[string]{ content: line, errorBack: ech, } @@ -129,9 +129,9 @@ return <-ech } // TODO: Delay and warnings? -func (s *server) ircBotLoop() { - s.ircSendBuffered = make(chan string, s.config.IRC.SendQ) - s.ircSendDirectChan = make(chan errorBack[string]) +func (s *Server) ircBotLoop() { + s.IrcSendBuffered = make(chan string, s.Config.IRC.SendQ) + s.IrcSendDirectChan = make(chan errorBack[string]) for { err := s.ircBotSession() diff --git a/iter.go b/iter.go index d4c7175ec6a1b6b41443da9461a4f292a20ad5bc..e237118cc7fda2884322c89b6a0e02fcadcf888b 100644 --- a/iter.go +++ b/iter.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import "iter" diff --git a/lmtp_handle_patch.go b/lmtp_handle_patch.go index ab846aa88bbf1240d21b57f59c85a728902f999e..bf1b94c7bb12dc5e8ede3b8f94e861faca3bb26f 100644 --- a/lmtp_handle_patch.go +++ b/lmtp_handle_patch.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "bytes" @@ -19,7 +19,7 @@ "github.com/go-git/go-git/v5" "go.lindenii.runxiyu.org/forge/misc" ) -func (s *server) lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string, mbox io.Reader) (err error) { +func (s *Server) lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string, mbox io.Reader) (err error) { var diffFiles []*gitdiff.File var preamble string if diffFiles, preamble, err = gitdiff.Parse(mbox); err != nil { diff --git a/lmtp_server.go b/lmtp_server.go index 8191766f8f9d02038c4ffe9c393ca07ef903632d..863a5c002e6ca189db97a67ee368c10f449933bc 100644 --- a/lmtp_server.go +++ b/lmtp_server.go @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu // SPDX-FileCopyrightText: Copyright (c) 2024 Robin Jarry -package main +package forge import ( "bytes" @@ -27,7 +27,7 @@ from string to []string ctx context.Context cancel context.CancelFunc - s server + s Server } func (session *lmtpSession) Reset() { @@ -63,13 +63,13 @@ } return session, nil } -func (s *server) serveLMTP(listener net.Listener) error { +func (s *Server) serveLMTP(listener net.Listener) error { smtpServer := smtp.NewServer(&lmtpHandler{}) smtpServer.LMTP = true - smtpServer.Domain = s.config.LMTP.Domain - smtpServer.Addr = s.config.LMTP.Socket - smtpServer.WriteTimeout = time.Duration(s.config.LMTP.WriteTimeout) * time.Second - smtpServer.ReadTimeout = time.Duration(s.config.LMTP.ReadTimeout) * time.Second + smtpServer.Domain = s.Config.LMTP.Domain + smtpServer.Addr = s.Config.LMTP.Socket + smtpServer.WriteTimeout = time.Duration(s.Config.LMTP.WriteTimeout) * time.Second + smtpServer.ReadTimeout = time.Duration(s.Config.LMTP.ReadTimeout) * time.Second smtpServer.EnableSMTPUTF8 = true return smtpServer.Serve(listener) } @@ -85,9 +85,9 @@ data []byte n int64 ) - n, err = io.CopyN(&buf, r, session.s.config.LMTP.MaxSize) + n, err = io.CopyN(&buf, r, session.s.Config.LMTP.MaxSize) switch { - case n == session.s.config.LMTP.MaxSize: + case n == session.s.Config.LMTP.MaxSize: err = errors.New("Message too big.") // drain whatever is left in the pipe _, _ = io.Copy(io.Discard, r) @@ -133,10 +133,10 @@ _ = from for _, to := range to { - if !strings.HasSuffix(to, "@"+session.s.config.LMTP.Domain) { + if !strings.HasSuffix(to, "@"+session.s.Config.LMTP.Domain) { continue } - localPart := to[:len(to)-len("@"+session.s.config.LMTP.Domain)] + localPart := to[:len(to)-len("@"+session.s.Config.LMTP.Domain)] var segments []string segments, err = misc.PathToSegments(localPart) if err != nil { diff --git a/main.go b/main.go deleted file mode 100644 index 2e1e094ddc0781116ed01f61c1fc3fa21cecdb44..0000000000000000000000000000000000000000 --- a/main.go +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu - -package main - -import ( - "errors" - "flag" - "io/fs" - "log" - "log/slog" - "net" - "net/http" - "os" - "os/exec" - "syscall" - "time" -) - -func main() { - configPath := flag.String( - "config", - "/etc/lindenii/forge.scfg", - "path to configuration file", - ) - flag.Parse() - - s := server{} - - s.sourceHandler = http.StripPrefix( - "/-/source/", - http.FileServer(http.FS(embeddedSourceFS)), - ) - staticFS, err := fs.Sub(embeddedResourcesFS, "static") - if err != nil { - panic(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 err := s.loadConfig(*configPath); err != nil { - slog.Error("loading configuration", "error", err) - os.Exit(1) - } - if err := s.deployHooks(); err != nil { - slog.Error("deploying hooks", "error", err) - os.Exit(1) - } - if err := loadTemplates(); err != nil { - slog.Error("loading templates", "error", err) - os.Exit(1) - } - if err := s.deployGit2D(); err != nil { - slog.Error("deploying git2d", "error", err) - os.Exit(1) - } - - // Launch Git2D - go func() { - cmd := exec.Command(s.config.Git.DaemonPath, s.config.Git.Socket) //#nosec G204 - cmd.Stderr = log.Writer() - cmd.Stdout = log.Writer() - if err := cmd.Run(); err != nil { - panic(err) - } - }() - - // UNIX socket listener for hooks - { - hooksListener, err := net.Listen("unix", s.config.Hooks.Socket) - if errors.Is(err, syscall.EADDRINUSE) { - slog.Warn("removing existing socket", "path", s.config.Hooks.Socket) - if err = syscall.Unlink(s.config.Hooks.Socket); err != nil { - slog.Error("removing existing socket", "path", s.config.Hooks.Socket, "error", err) - os.Exit(1) - } - if hooksListener, err = net.Listen("unix", s.config.Hooks.Socket); err != nil { - slog.Error("listening hooks", "error", err) - os.Exit(1) - } - } else if err != nil { - slog.Error("listening hooks", "error", err) - os.Exit(1) - } - slog.Info("listening hooks on unix", "path", s.config.Hooks.Socket) - go func() { - if err = s.serveGitHooks(hooksListener); err != nil { - slog.Error("serving hooks", "error", err) - os.Exit(1) - } - }() - } - - // UNIX socket listener for LMTP - { - lmtpListener, err := net.Listen("unix", s.config.LMTP.Socket) - if errors.Is(err, syscall.EADDRINUSE) { - slog.Warn("removing existing socket", "path", s.config.LMTP.Socket) - if err = syscall.Unlink(s.config.LMTP.Socket); err != nil { - slog.Error("removing existing socket", "path", s.config.LMTP.Socket, "error", err) - os.Exit(1) - } - if lmtpListener, err = net.Listen("unix", s.config.LMTP.Socket); err != nil { - slog.Error("listening LMTP", "error", err) - os.Exit(1) - } - } else if err != nil { - slog.Error("listening LMTP", "error", err) - os.Exit(1) - } - slog.Info("listening LMTP on unix", "path", s.config.LMTP.Socket) - go func() { - if err = s.serveLMTP(lmtpListener); err != nil { - slog.Error("serving LMTP", "error", err) - os.Exit(1) - } - }() - } - - // SSH listener - { - sshListener, err := net.Listen(s.config.SSH.Net, s.config.SSH.Addr) - if errors.Is(err, syscall.EADDRINUSE) && s.config.SSH.Net == "unix" { - slog.Warn("removing existing socket", "path", s.config.SSH.Addr) - if err = syscall.Unlink(s.config.SSH.Addr); err != nil { - slog.Error("removing existing socket", "path", s.config.SSH.Addr, "error", err) - os.Exit(1) - } - if sshListener, err = net.Listen(s.config.SSH.Net, s.config.SSH.Addr); err != nil { - slog.Error("listening SSH", "error", err) - os.Exit(1) - } - } else if err != nil { - slog.Error("listening SSH", "error", err) - os.Exit(1) - } - slog.Info("listening SSH on", "net", s.config.SSH.Net, "addr", s.config.SSH.Addr) - go func() { - if err = s.serveSSH(sshListener); err != nil { - slog.Error("serving SSH", "error", err) - os.Exit(1) - } - }() - } - - // HTTP listener - { - httpListener, err := net.Listen(s.config.HTTP.Net, s.config.HTTP.Addr) - if errors.Is(err, syscall.EADDRINUSE) && s.config.HTTP.Net == "unix" { - slog.Warn("removing existing socket", "path", s.config.HTTP.Addr) - if err = syscall.Unlink(s.config.HTTP.Addr); err != nil { - slog.Error("removing existing socket", "path", s.config.HTTP.Addr, "error", err) - os.Exit(1) - } - if httpListener, err = net.Listen(s.config.HTTP.Net, s.config.HTTP.Addr); err != nil { - slog.Error("listening HTTP", "error", err) - os.Exit(1) - } - } else if err != nil { - slog.Error("listening HTTP", "error", err) - os.Exit(1) - } - server := http.Server{ - Handler: &s, - 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, - } //exhaustruct:ignore - slog.Info("listening HTTP on", "net", s.config.HTTP.Net, "addr", s.config.HTTP.Addr) - go func() { - if err = server.Serve(httpListener); err != nil && !errors.Is(err, http.ErrServerClosed) { - slog.Error("serving HTTP", "error", err) - os.Exit(1) - } - }() - } - - // IRC bot - go s.ircBotLoop() - - select {} -} diff --git a/remote_url.go b/remote_url.go index 9f30993a271085907e18a62a03a531ad9f6fe2bd..453ddeb8f00e172eaf2c59cd2981bbe011b43407 100644 --- a/remote_url.go +++ b/remote_url.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "net/url" @@ -14,12 +14,12 @@ // We don't use path.Join because it collapses multiple slashes into one. // genSSHRemoteURL generates SSH remote URLs from a given group path and repo // name. -func (s *server) genSSHRemoteURL(groupPath []string, repoName string) string { - return strings.TrimSuffix(s.config.SSH.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) +func (s *Server) genSSHRemoteURL(groupPath []string, repoName string) string { + return strings.TrimSuffix(s.Config.SSH.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) } // genHTTPRemoteURL generates HTTP remote URLs from a given group path and repo // name. -func (s *server) genHTTPRemoteURL(groupPath []string, repoName string) string { - return strings.TrimSuffix(s.config.HTTP.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) +func (s *Server) genHTTPRemoteURL(groupPath []string, repoName string) string { + return strings.TrimSuffix(s.Config.HTTP.Root, "/") + "/" + misc.SegmentsToURL(groupPath) + "/-/repos/" + url.PathEscape(repoName) } diff --git a/resources.go b/resources.go index 5ecb218e9b34f48bdb0c385312bb8c35f46b867b..ffe1008ca1fa11615a54446e611894b16b1bd114 100644 --- a/resources.go +++ b/resources.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "embed" diff --git a/server.go b/server.go index c99596886f89609b44e908e284fc24691dccd9a1..ce69380a1ac3f85ab96cd03cb4d39be3425b6f34 100644 --- a/server.go +++ b/server.go @@ -1,34 +1,201 @@ -package main +package forge import ( + "errors" + "io/fs" + "log" + "log/slog" + "net" "net/http" + "os" + "os/exec" + "syscall" + "time" "github.com/jackc/pgx/v5/pgxpool" "go.lindenii.runxiyu.org/lindenii-common/cmap" goSSH "golang.org/x/crypto/ssh" ) -type server struct { - config Config +type Server struct { + Config Config - // database serves as the primary database handle for this entire application. + // Database serves as the primary Database handle for this entire application. // Transactions or single reads may be used from it. A [pgxpool.Pool] is // necessary to safely use pgx concurrently; pgx.Conn, etc. are insufficient. - database *pgxpool.Pool + Database *pgxpool.Pool + + SourceHandler http.Handler + StaticHandler http.Handler + + IrcSendBuffered chan string + IrcSendDirectChan chan errorBack[string] + + // 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] +} + +func (s *Server) Setup() { + s.SourceHandler = http.StripPrefix( + "/-/source/", + http.FileServer(http.FS(embeddedSourceFS)), + ) + staticFS, err := fs.Sub(embeddedResourcesFS, "static") + if err != nil { + panic(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 + } +} + +func (s *Server) Run() { + if err := s.deployHooks(); err != nil { + slog.Error("deploying hooks", "error", err) + os.Exit(1) + } + if err := loadTemplates(); err != nil { + slog.Error("loading templates", "error", err) + os.Exit(1) + } + if err := s.deployGit2D(); err != nil { + slog.Error("deploying git2d", "error", err) + os.Exit(1) + } + + // Launch Git2D + go func() { + cmd := exec.Command(s.Config.Git.DaemonPath, s.Config.Git.Socket) //#nosec G204 + cmd.Stderr = log.Writer() + cmd.Stdout = log.Writer() + if err := cmd.Run(); err != nil { + panic(err) + } + }() + + // UNIX socket listener for hooks + { + hooksListener, err := net.Listen("unix", s.Config.Hooks.Socket) + if errors.Is(err, syscall.EADDRINUSE) { + slog.Warn("removing existing socket", "path", s.Config.Hooks.Socket) + if err = syscall.Unlink(s.Config.Hooks.Socket); err != nil { + slog.Error("removing existing socket", "path", s.Config.Hooks.Socket, "error", err) + os.Exit(1) + } + if hooksListener, err = net.Listen("unix", s.Config.Hooks.Socket); err != nil { + slog.Error("listening hooks", "error", err) + os.Exit(1) + } + } else if err != nil { + slog.Error("listening hooks", "error", err) + os.Exit(1) + } + slog.Info("listening hooks on unix", "path", s.Config.Hooks.Socket) + go func() { + if err = s.serveGitHooks(hooksListener); err != nil { + slog.Error("serving hooks", "error", err) + os.Exit(1) + } + }() + } - sourceHandler http.Handler - staticHandler http.Handler + // UNIX socket listener for LMTP + { + lmtpListener, err := net.Listen("unix", s.Config.LMTP.Socket) + if errors.Is(err, syscall.EADDRINUSE) { + slog.Warn("removing existing socket", "path", s.Config.LMTP.Socket) + if err = syscall.Unlink(s.Config.LMTP.Socket); err != nil { + slog.Error("removing existing socket", "path", s.Config.LMTP.Socket, "error", err) + os.Exit(1) + } + if lmtpListener, err = net.Listen("unix", s.Config.LMTP.Socket); err != nil { + slog.Error("listening LMTP", "error", err) + os.Exit(1) + } + } else if err != nil { + slog.Error("listening LMTP", "error", err) + os.Exit(1) + } + slog.Info("listening LMTP on unix", "path", s.Config.LMTP.Socket) + go func() { + if err = s.serveLMTP(lmtpListener); err != nil { + slog.Error("serving LMTP", "error", err) + os.Exit(1) + } + }() + } - ircSendBuffered chan string - ircSendDirectChan chan errorBack[string] + // SSH listener + { + sshListener, err := net.Listen(s.Config.SSH.Net, s.Config.SSH.Addr) + if errors.Is(err, syscall.EADDRINUSE) && s.Config.SSH.Net == "unix" { + slog.Warn("removing existing socket", "path", s.Config.SSH.Addr) + if err = syscall.Unlink(s.Config.SSH.Addr); err != nil { + slog.Error("removing existing socket", "path", s.Config.SSH.Addr, "error", err) + os.Exit(1) + } + if sshListener, err = net.Listen(s.Config.SSH.Net, s.Config.SSH.Addr); err != nil { + slog.Error("listening SSH", "error", err) + os.Exit(1) + } + } else if err != nil { + slog.Error("listening SSH", "error", err) + os.Exit(1) + } + slog.Info("listening SSH on", "net", s.Config.SSH.Net, "addr", s.Config.SSH.Addr) + go func() { + if err = s.serveSSH(sshListener); err != nil { + slog.Error("serving SSH", "error", err) + os.Exit(1) + } + }() + } - // globalData is passed as "global" when rendering HTML templates. - globalData map[string]any + // HTTP listener + { + httpListener, err := net.Listen(s.Config.HTTP.Net, s.Config.HTTP.Addr) + if errors.Is(err, syscall.EADDRINUSE) && s.Config.HTTP.Net == "unix" { + slog.Warn("removing existing socket", "path", s.Config.HTTP.Addr) + if err = syscall.Unlink(s.Config.HTTP.Addr); err != nil { + slog.Error("removing existing socket", "path", s.Config.HTTP.Addr, "error", err) + os.Exit(1) + } + if httpListener, err = net.Listen(s.Config.HTTP.Net, s.Config.HTTP.Addr); err != nil { + slog.Error("listening HTTP", "error", err) + os.Exit(1) + } + } else if err != nil { + slog.Error("listening HTTP", "error", err) + os.Exit(1) + } + server := http.Server{ + Handler: s, + 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, + } //exhaustruct:ignore + slog.Info("listening HTTP on", "net", s.Config.HTTP.Net, "addr", s.Config.HTTP.Addr) + go func() { + if err = server.Serve(httpListener); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Error("serving HTTP", "error", err) + os.Exit(1) + } + }() + } - serverPubkeyString string - serverPubkeyFP string - serverPubkey goSSH.PublicKey + // IRC bot + go s.ircBotLoop() - // packPasses contains hook cookies mapped to their packPass. - packPasses cmap.Map[string, packPass] + select {} } diff --git a/ssh_handle_receive_pack.go b/ssh_handle_receive_pack.go index 33262e4a3bd04998d00bed8bfbc010e87be1e57f..724c3fdf8399e5e142414f5264809ace586b0369 100644 --- a/ssh_handle_receive_pack.go +++ b/ssh_handle_receive_pack.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "errors" @@ -30,7 +30,7 @@ contribReq string } // sshHandleRecvPack handles attempts to push to repos. -func (s *server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdentifier string) (err error) { +func (s *Server) sshHandleRecvPack(session gliderSSH.Session, pubkey, repoIdentifier string) (err error) { groupPath, repoName, repoID, repoPath, directAccess, contribReq, userType, userID, err := s.getRepoInfo2(session.Context(), repoIdentifier, pubkey) if err != nil { return err @@ -51,7 +51,7 @@ return errors.New("repository has no core section in config") } hooksPath := repoConfCore.OptionAll("hooksPath") - if len(hooksPath) != 1 || hooksPath[0] != s.config.Hooks.Execs { + if len(hooksPath) != 1 || hooksPath[0] != s.Config.Hooks.Execs { return errors.New("repository has hooksPath set to an unexpected value") } @@ -91,7 +91,7 @@ if err != nil { fmt.Fprintln(session.Stderr(), "Error while generating cookie:", err) } - s.packPasses.Store(cookie, packPass{ + s.PackPasses.Store(cookie, packPass{ session: session, pubkey: pubkey, directAccess: directAccess, @@ -104,13 +104,13 @@ repo: repo, contribReq: contribReq, userType: userType, }) - defer s.packPasses.Delete(cookie) + defer s.PackPasses.Delete(cookie) // The Delete won't execute until proc.Wait returns unless something // horribly wrong such as a panic occurs. proc := exec.CommandContext(session.Context(), "git-receive-pack", repoPath) proc.Env = append(os.Environ(), - "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.config.Hooks.Socket, + "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.Config.Hooks.Socket, "LINDENII_FORGE_HOOKS_COOKIE="+cookie, ) proc.Stdin = session diff --git a/ssh_handle_upload_pack.go b/ssh_handle_upload_pack.go index 7f2a52c50699bf4189ed0ae9bc134c6e0221d52c..45ecfd84b7c9c03998fb0cc3c12540e4c94cba50 100644 --- a/ssh_handle_upload_pack.go +++ b/ssh_handle_upload_pack.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "fmt" @@ -13,14 +13,14 @@ ) // sshHandleUploadPack handles clones/fetches. It just uses git-upload-pack // and has no ACL checks. -func (s *server) sshHandleUploadPack(session glider_ssh.Session, pubkey, repoIdentifier string) (err error) { +func (s *Server) sshHandleUploadPack(session glider_ssh.Session, pubkey, repoIdentifier string) (err error) { var repoPath string if _, _, _, repoPath, _, _, _, _, err = s.getRepoInfo2(session.Context(), repoIdentifier, pubkey); err != nil { return err } proc := exec.CommandContext(session.Context(), "git-upload-pack", repoPath) - proc.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.config.Hooks.Socket) + proc.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+s.Config.Hooks.Socket) proc.Stdin = session proc.Stdout = session proc.Stderr = session.Stderr() diff --git a/ssh_server.go b/ssh_server.go index afb0d9501446c8f0f8f6827723d11392f19ff79c..ed303b957330b0d63cf07c70340c0212fa417f36 100644 --- a/ssh_server.go +++ b/ssh_server.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "fmt" @@ -19,13 +19,13 @@ // serveSSH 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) serveSSH(listener net.Listener) error { var hostKeyBytes []byte var hostKey goSSH.Signer var err error var server *gliderSSH.Server - if hostKeyBytes, err = os.ReadFile(s.config.SSH.Key); err != nil { + if hostKeyBytes, err = os.ReadFile(s.Config.SSH.Key); err != nil { return err } @@ -33,9 +33,9 @@ if hostKey, err = goSSH.ParsePrivateKey(hostKeyBytes); err != nil { return err } - s.serverPubkey = hostKey.PublicKey() - s.serverPubkeyString = misc.BytesToString(goSSH.MarshalAuthorizedKey(s.serverPubkey)) - s.serverPubkeyFP = goSSH.FingerprintSHA256(s.serverPubkey) + s.ServerPubkey = hostKey.PublicKey() + s.ServerPubkeyString = misc.BytesToString(goSSH.MarshalAuthorizedKey(s.ServerPubkey)) + s.ServerPubkeyFP = goSSH.FingerprintSHA256(s.ServerPubkey) server = &gliderSSH.Server{ Handler: func(session gliderSSH.Session) { @@ -46,7 +46,7 @@ clientPubkeyStr = strings.TrimSuffix(misc.BytesToString(goSSH.MarshalAuthorizedKey(clientPubkey)), "\n") } slog.Info("incoming ssh", "addr", session.RemoteAddr().String(), "key", clientPubkeyStr, "command", session.RawCommand()) - fmt.Fprintln(session.Stderr(), ansiec.Blue+"Lindenii Forge "+VERSION+", source at "+strings.TrimSuffix(s.config.HTTP.Root, "/")+"/-/source/"+ansiec.Reset+"\r") + fmt.Fprintln(session.Stderr(), ansiec.Blue+"Lindenii Forge "+VERSION+", source at "+strings.TrimSuffix(s.Config.HTTP.Root, "/")+"/-/source/"+ansiec.Reset+"\r") cmd := session.Command() diff --git a/ssh_utils.go b/ssh_utils.go index 02069dd9d524fcb1864a172925b00fdd0d7e2576..8f04209455f6baec180577eac6359c32851b3ae4 100644 --- a/ssh_utils.go +++ b/ssh_utils.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "context" @@ -18,7 +18,7 @@ var errIllegalSSHRepoPath = errors.New("illegal SSH repo path") // getRepoInfo2 also fetches repo information... it should be deprecated and // implemented in individual handlers. -func (s *server) getRepoInfo2(ctx context.Context, sshPath, sshPubkey string) (groupPath []string, repoName string, repoID int, repoPath string, directAccess bool, contribReq, userType string, userID int, err error) { +func (s *Server) getRepoInfo2(ctx context.Context, sshPath, sshPubkey string) (groupPath []string, repoName string, repoID int, repoPath string, directAccess bool, contribReq, userType string, userID int, err error) { var segments []string var sepIndex int var moduleType, moduleName string diff --git a/users.go b/users.go index 1b31f3a1f9b66d61a9d9f76dd6d47ee323277168..3f57a35e321fc0bd42cb4570b2da5093b8fe5e3d 100644 --- a/users.go +++ b/users.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu -package main +package forge import ( "context" @@ -12,10 +12,10 @@ // 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 (s *Server) addUserSSH(ctx context.Context, pubkey string) (userID int, err error) { var txn pgx.Tx - if txn, err = s.database.Begin(ctx); err != nil { + if txn, err = s.Database.Begin(ctx); err != nil { return } defer func() { diff --git a/version.go b/version.go index c5e5aab92ffcd4881ab171bb314758feaeff8903..92fc82bd5b71dc14f4b2cff8ac024c92056a75d9 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ -package main +package forge var VERSION = "unknown" //nolint:gochecknoglobals -- 2.48.1