diff options
Diffstat (limited to '')
45 files changed, 976 insertions, 623 deletions
diff --git a/forged/internal/common/bare/errors.go b/forged/internal/common/bare/errors.go index 39c951a..4634f0c 100644 --- a/forged/internal/common/bare/errors.go +++ b/forged/internal/common/bare/errors.go @@ -9,12 +9,12 @@ import ( "reflect" ) -var ErrInvalidStr = errors.New("String contains invalid UTF-8 sequences") +var ErrInvalidStr = errors.New("string contains invalid UTF-8 sequences") type UnsupportedTypeError struct { Type reflect.Type } func (e *UnsupportedTypeError) Error() string { - return fmt.Sprintf("Unsupported type for marshaling: %s\n", e.Type.String()) + return fmt.Sprintf("unsupported type for marshaling: %s\n", e.Type.String()) } diff --git a/forged/internal/common/bare/limit.go b/forged/internal/common/bare/limit.go index 212bc05..7eece8c 100644 --- a/forged/internal/common/bare/limit.go +++ b/forged/internal/common/bare/limit.go @@ -32,7 +32,7 @@ func MaxMapSize(size uint64) { // Use MaxUnmarshalBytes to prevent this error from occuring on messages which // are large by design. -var ErrLimitExceeded = errors.New("Maximum message size exceeded") +var ErrLimitExceeded = errors.New("maximum message size exceeded") // Identical to io.LimitedReader, except it returns our custom error instead of // EOF if the limit is reached. diff --git a/forged/internal/common/bare/marshal.go b/forged/internal/common/bare/marshal.go index 1ce942d..d4c338e 100644 --- a/forged/internal/common/bare/marshal.go +++ b/forged/internal/common/bare/marshal.go @@ -54,7 +54,7 @@ func MarshalWriter(w *Writer, val interface{}) error { t := reflect.TypeOf(val) v := reflect.ValueOf(val) if t.Kind() != reflect.Ptr { - return errors.New("Expected val to be pointer type") + return errors.New("expected val to be pointer type") } return getEncoder(t.Elem())(w, v.Elem()) diff --git a/forged/internal/common/bare/reader.go b/forged/internal/common/bare/reader.go index 028a7aa..7e872f4 100644 --- a/forged/internal/common/bare/reader.go +++ b/forged/internal/common/bare/reader.go @@ -157,7 +157,7 @@ func (r *Reader) ReadString() (string, error) { // Reads a fixed amount of arbitrary data, defined by the length of the slice. func (r *Reader) ReadDataFixed(dest []byte) error { - var amt int = 0 + var amt int for amt < len(dest) { n, err := r.base.Read(dest[amt:]) if err != nil { diff --git a/forged/internal/common/bare/writer.go b/forged/internal/common/bare/writer.go index 80cd7e2..1b23c9f 100644 --- a/forged/internal/common/bare/writer.go +++ b/forged/internal/common/bare/writer.go @@ -92,7 +92,7 @@ func (w *Writer) WriteString(str string) error { // Writes a fixed amount of arbitrary data, defined by the length of the slice. func (w *Writer) WriteDataFixed(data []byte) error { - var amt int = 0 + var amt int for amt < len(data) { n, err := w.base.Write(data[amt:]) if err != nil { @@ -109,7 +109,7 @@ func (w *Writer) WriteData(data []byte) error { if err != nil { return err } - var amt int = 0 + var amt int for amt < len(data) { n, err := w.base.Write(data[amt:]) if err != nil { diff --git a/forged/internal/common/scfg/reader.go b/forged/internal/common/scfg/reader.go index 6a2bedc..b0e2cc0 100644 --- a/forged/internal/common/scfg/reader.go +++ b/forged/internal/common/scfg/reader.go @@ -15,12 +15,16 @@ import ( const maxNestingDepth = 1000 // Load loads a configuration file. -func Load(path string) (Block, error) { +func Load(path string) (block Block, err error) { f, err := os.Open(path) if err != nil { return nil, err } - defer f.Close() + defer func() { + if cerr := f.Close(); err == nil && cerr != nil { + err = cerr + } + }() return Read(f) } diff --git a/forged/internal/config/config.go b/forged/internal/config/config.go index da28e05..1825882 100644 --- a/forged/internal/config/config.go +++ b/forged/internal/config/config.go @@ -7,32 +7,82 @@ import ( "os" "go.lindenii.runxiyu.org/forge/forged/internal/common/scfg" - "go.lindenii.runxiyu.org/forge/forged/internal/database" - "go.lindenii.runxiyu.org/forge/forged/internal/incoming/hooks" - "go.lindenii.runxiyu.org/forge/forged/internal/incoming/lmtp" - "go.lindenii.runxiyu.org/forge/forged/internal/incoming/ssh" - "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web" - "go.lindenii.runxiyu.org/forge/forged/internal/ipc/irc" ) type Config struct { - DB database.Config `scfg:"db"` - Web web.Config `scfg:"web"` - Hooks hooks.Config `scfg:"hooks"` - LMTP lmtp.Config `scfg:"lmtp"` - SSH ssh.Config `scfg:"ssh"` - IRC irc.Config `scfg:"irc"` - Git struct { - RepoDir string `scfg:"repo_dir"` - Socket string `scfg:"socket"` - } `scfg:"git"` - General struct { - Title string `scfg:"title"` - } `scfg:"general"` - Pprof struct { - Net string `scfg:"net"` - Addr string `scfg:"addr"` - } `scfg:"pprof"` + DB DB `scfg:"db"` + Web Web `scfg:"web"` + Hooks Hooks `scfg:"hooks"` + LMTP LMTP `scfg:"lmtp"` + SSH SSH `scfg:"ssh"` + IRC IRC `scfg:"irc"` + Git Git `scfg:"git"` + General General `scfg:"general"` + Pprof Pprof `scfg:"pprof"` +} + +type DB struct { + Conn string `scfg:"conn"` +} + +type Web struct { + Net string `scfg:"net"` + Addr string `scfg:"addr"` + Root string `scfg:"root"` + CookieExpiry int `scfg:"cookie_expiry"` + ReadTimeout uint32 `scfg:"read_timeout"` + WriteTimeout uint32 `scfg:"write_timeout"` + IdleTimeout uint32 `scfg:"idle_timeout"` + MaxHeaderBytes int `scfg:"max_header_bytes"` + ReverseProxy bool `scfg:"reverse_proxy"` + ShutdownTimeout uint32 `scfg:"shutdown_timeout"` + TemplatesPath string `scfg:"templates_path"` + StaticPath string `scfg:"static_path"` +} + +type Hooks struct { + Socket string `scfg:"socket"` + Execs string `scfg:"execs"` +} + +type LMTP struct { + Socket string `scfg:"socket"` + Domain string `scfg:"domain"` + MaxSize int64 `scfg:"max_size"` + WriteTimeout uint32 `scfg:"write_timeout"` + ReadTimeout uint32 `scfg:"read_timeout"` +} + +type SSH struct { + Net string `scfg:"net"` + Addr string `scfg:"addr"` + Key string `scfg:"key"` + Root string `scfg:"root"` + ShutdownTimeout uint32 `scfg:"shutdown_timeout"` +} + +type IRC struct { + Net string `scfg:"net"` + Addr string `scfg:"addr"` + TLS bool `scfg:"tls"` + SendQ uint `scfg:"sendq"` + Nick string `scfg:"nick"` + User string `scfg:"user"` + Gecos string `scfg:"gecos"` +} + +type Git struct { + RepoDir string `scfg:"repo_dir"` + Socket string `scfg:"socket"` +} + +type General struct { + Title string `scfg:"title"` +} + +type Pprof struct { + Net string `scfg:"net"` + Addr string `scfg:"addr"` } func Open(path string) (config Config, err error) { diff --git a/forged/internal/database/config.go b/forged/internal/database/config.go deleted file mode 100644 index 3697693..0000000 --- a/forged/internal/database/config.go +++ /dev/null @@ -1,5 +0,0 @@ -package database - -type Config struct { - Conn string `scfg:"conn"` -} diff --git a/forged/internal/database/database.go b/forged/internal/database/database.go index 093ed8f..d96af6b 100644 --- a/forged/internal/database/database.go +++ b/forged/internal/database/database.go @@ -11,17 +11,12 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -// Database is a wrapper around pgxpool.Pool to provide a common interface for -// other packages in the forge. type Database struct { *pgxpool.Pool } -// Open opens a new database connection pool using the provided connection -// string. It returns a Database instance and an error if any occurs. -// It is run indefinitely in the background. -func Open(ctx context.Context, config Config) (Database, error) { - db, err := pgxpool.New(ctx, config.Conn) +func Open(ctx context.Context, conn string) (Database, error) { + db, err := pgxpool.New(ctx, conn) if err != nil { err = fmt.Errorf("create pgxpool: %w", err) } diff --git a/forged/internal/global/global.go b/forged/internal/global/global.go index 2aa8049..99f85e7 100644 --- a/forged/internal/global/global.go +++ b/forged/internal/global/global.go @@ -1,8 +1,18 @@ package global +import ( + "go.lindenii.runxiyu.org/forge/forged/internal/config" + "go.lindenii.runxiyu.org/forge/forged/internal/database" + "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" +) + type Global struct { - ForgeTitle string + ForgeTitle string // should be removed since it's in Config ForgeVersion string SSHPubkey string SSHFingerprint string + + Config *config.Config + Queries *queries.Queries + DB *database.Database } diff --git a/forged/internal/incoming/hooks/config.go b/forged/internal/incoming/hooks/config.go deleted file mode 100644 index 0d23dc0..0000000 --- a/forged/internal/incoming/hooks/config.go +++ /dev/null @@ -1,6 +0,0 @@ -package hooks - -type Config struct { - Socket string `scfg:"socket"` - Execs string `scfg:"execs"` -} diff --git a/forged/internal/incoming/hooks/hooks.go b/forged/internal/incoming/hooks/hooks.go index dfdf172..effd104 100644 --- a/forged/internal/incoming/hooks/hooks.go +++ b/forged/internal/incoming/hooks/hooks.go @@ -32,10 +32,11 @@ type hookInfo struct { contribReq string } -func New(config Config, global *global.Global) (server *Server) { +func New(global *global.Global) (server *Server) { + cfg := global.Config.Hooks return &Server{ - socketPath: config.Socket, - executablesPath: config.Execs, + socketPath: cfg.Socket, + executablesPath: cfg.Execs, hookMap: cmap.Map[string, hookInfo]{}, global: global, } diff --git a/forged/internal/incoming/lmtp/config.go b/forged/internal/incoming/lmtp/config.go deleted file mode 100644 index 6241608..0000000 --- a/forged/internal/incoming/lmtp/config.go +++ /dev/null @@ -1,9 +0,0 @@ -package lmtp - -type Config struct { - Socket string `scfg:"socket"` - Domain string `scfg:"domain"` - MaxSize int64 `scfg:"max_size"` - WriteTimeout uint32 `scfg:"write_timeout"` - ReadTimeout uint32 `scfg:"read_timeout"` -} diff --git a/forged/internal/incoming/lmtp/lmtp.go b/forged/internal/incoming/lmtp/lmtp.go index a7782a2..c8918f8 100644 --- a/forged/internal/incoming/lmtp/lmtp.go +++ b/forged/internal/incoming/lmtp/lmtp.go @@ -20,13 +20,14 @@ type Server struct { global *global.Global } -func New(config Config, global *global.Global) (server *Server) { +func New(global *global.Global) (server *Server) { + cfg := global.Config.LMTP return &Server{ - socket: config.Socket, - domain: config.Domain, - maxSize: config.MaxSize, - writeTimeout: config.WriteTimeout, - readTimeout: config.ReadTimeout, + socket: cfg.Socket, + domain: cfg.Domain, + maxSize: cfg.MaxSize, + writeTimeout: cfg.WriteTimeout, + readTimeout: cfg.ReadTimeout, global: global, } } diff --git a/forged/internal/incoming/ssh/config.go b/forged/internal/incoming/ssh/config.go deleted file mode 100644 index 7d22cc1..0000000 --- a/forged/internal/incoming/ssh/config.go +++ /dev/null @@ -1,9 +0,0 @@ -package ssh - -type Config struct { - Net string `scfg:"net"` - Addr string `scfg:"addr"` - Key string `scfg:"key"` - Root string `scfg:"root"` - ShutdownTimeout uint32 `scfg:"shutdown_timeout"` -} diff --git a/forged/internal/incoming/ssh/ssh.go b/forged/internal/incoming/ssh/ssh.go index 527cd28..1f27be2 100644 --- a/forged/internal/incoming/ssh/ssh.go +++ b/forged/internal/incoming/ssh/ssh.go @@ -23,18 +23,19 @@ type Server struct { global *global.Global } -func New(config Config, global *global.Global) (server *Server, err error) { +func New(global *global.Global) (server *Server, err error) { + cfg := global.Config.SSH server = &Server{ - net: config.Net, - addr: config.Addr, - root: config.Root, - shutdownTimeout: config.ShutdownTimeout, + net: cfg.Net, + addr: cfg.Addr, + root: cfg.Root, + shutdownTimeout: cfg.ShutdownTimeout, global: global, } //exhaustruct:ignore var privkeyBytes []byte - privkeyBytes, err = os.ReadFile(config.Key) + privkeyBytes, err = os.ReadFile(cfg.Key) if err != nil { return server, fmt.Errorf("read SSH private key: %w", err) } diff --git a/forged/internal/incoming/web/authn.go b/forged/internal/incoming/web/authn.go index 46263ee..9754eb1 100644 --- a/forged/internal/incoming/web/authn.go +++ b/forged/internal/incoming/web/authn.go @@ -21,7 +21,7 @@ func userResolver(r *http.Request) (string, string, error) { tokenHash := sha256.Sum256([]byte(cookie.Value)) - session, err := types.Base(r).Queries.GetUserFromSession(r.Context(), tokenHash[:]) + session, err := types.Base(r).Global.Queries.GetUserFromSession(r.Context(), tokenHash[:]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return "", "", nil diff --git a/forged/internal/incoming/web/config.go b/forged/internal/incoming/web/config.go deleted file mode 100644 index 8d32b34..0000000 --- a/forged/internal/incoming/web/config.go +++ /dev/null @@ -1,16 +0,0 @@ -package web - -type Config struct { - Net string `scfg:"net"` - Addr string `scfg:"addr"` - Root string `scfg:"root"` - CookieExpiry int `scfg:"cookie_expiry"` - ReadTimeout uint32 `scfg:"read_timeout"` - WriteTimeout uint32 `scfg:"write_timeout"` - IdleTimeout uint32 `scfg:"idle_timeout"` - MaxHeaderBytes int `scfg:"max_header_bytes"` - ReverseProxy bool `scfg:"reverse_proxy"` - ShutdownTimeout uint32 `scfg:"shutdown_timeout"` - TemplatesPath string `scfg:"templates_path"` - StaticPath string `scfg:"static_path"` -} diff --git a/forged/internal/incoming/web/handler.go b/forged/internal/incoming/web/handler.go index 20f7e79..e0e6ced 100644 --- a/forged/internal/incoming/web/handler.go +++ b/forged/internal/incoming/web/handler.go @@ -5,7 +5,6 @@ import ( "net/http" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" - "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" "go.lindenii.runxiyu.org/forge/forged/internal/global" handlers "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/handlers" repoHandlers "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/handlers/repo" @@ -17,8 +16,9 @@ type handler struct { r *Router } -func NewHandler(cfg Config, global *global.Global, queries *queries.Queries) *handler { - h := &handler{r: NewRouter().ReverseProxy(cfg.ReverseProxy).Global(global).Queries(queries).UserResolver(userResolver)} +func NewHandler(global *global.Global) *handler { + cfg := global.Config.Web + h := &handler{r: NewRouter().ReverseProxy(cfg.ReverseProxy).Global(global).UserResolver(userResolver)} staticFS := http.FileServer(http.Dir(cfg.StaticPath)) h.r.ANYHTTP("-/static/*rest", @@ -42,25 +42,17 @@ func NewHandler(cfg Config, global *global.Global, queries *queries.Queries) *ha repoHTTP := repoHandlers.NewHTTP(renderer) notImpl := handlers.NewNotImplementedHTTP(renderer) - // Index h.r.GET("/", indexHTTP.Index) - // Top-level utilities h.r.ANY("-/login", loginHTTP.Login) h.r.ANY("-/users", notImpl.Handle) - // Group index h.r.GET("@group/", groupHTTP.Index) h.r.POST("@group/", groupHTTP.Post) - // Repo index h.r.GET("@group/-/repos/:repo/", repoHTTP.Index) - - // Repo (not implemented yet) h.r.ANY("@group/-/repos/:repo/info", notImpl.Handle) h.r.ANY("@group/-/repos/:repo/git-upload-pack", notImpl.Handle) - - // Repo features h.r.GET("@group/-/repos/:repo/branches/", notImpl.Handle) h.r.GET("@group/-/repos/:repo/log/", notImpl.Handle) h.r.GET("@group/-/repos/:repo/commit/:commit", notImpl.Handle) diff --git a/forged/internal/incoming/web/handlers/group.go b/forged/internal/incoming/web/handlers/group.go index 3201491..4823cb7 100644 --- a/forged/internal/incoming/web/handlers/group.go +++ b/forged/internal/incoming/web/handlers/group.go @@ -1,13 +1,17 @@ package handlers import ( + "fmt" "log/slog" "net/http" + "path/filepath" "strconv" + "github.com/jackc/pgx/v5" "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/templates" wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" + "go.lindenii.runxiyu.org/forge/forged/internal/ipc/git2c" ) type GroupHTTP struct { @@ -31,19 +35,19 @@ func (h *GroupHTTP) Index(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) Column1: base.URLSegments, UserID: userID, } - p, err := base.Queries.GetGroupByPath(r.Context(), queryParams) + p, err := base.Global.Queries.GetGroupByPath(r.Context(), queryParams) if err != nil { slog.Error("failed to get group ID by path", "error", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - subgroups, err := base.Queries.GetSubgroups(r.Context(), &p.ID) + subgroups, err := base.Global.Queries.GetSubgroups(r.Context(), &p.ID) if err != nil { slog.Error("failed to get subgroups", "error", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) // TODO: gracefully fail this part of the page } - repos, err := base.Queries.GetReposInGroup(r.Context(), p.ID) + repos, err := base.Global.Queries.GetReposInGroup(r.Context(), p.ID) if err != nil { slog.Error("failed to get repos in group", "error", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) @@ -78,7 +82,7 @@ func (h *GroupHTTP) Post(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) Column1: base.URLSegments, UserID: userID, } - p, err := base.Queries.GetGroupByPath(r.Context(), queryParams) + p, err := base.Global.Queries.GetGroupByPath(r.Context(), queryParams) if err != nil { slog.Error("failed to get group ID by path", "error", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) @@ -89,4 +93,64 @@ func (h *GroupHTTP) Post(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) http.Error(w, "You do not have the necessary permissions to create repositories in this group.", http.StatusForbidden) return } + + name := r.PostFormValue("repo_name") + desc := r.PostFormValue("repo_desc") + contrib := r.PostFormValue("repo_contrib") + if name == "" { + http.Error(w, "Repo name is required", http.StatusBadRequest) + return + } + + if contrib == "" || contrib == "public" { + contrib = "open" + } + + tx, err := base.Global.DB.BeginTx(r.Context(), pgx.TxOptions{}) + if err != nil { + slog.Error("begin tx failed", "error", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + defer func() { _ = tx.Rollback(r.Context()) }() + + txq := base.Global.Queries.WithTx(tx) + var descPtr *string + if desc != "" { + descPtr = &desc + } + repoID, err := txq.InsertRepo(r.Context(), queries.InsertRepoParams{ + GroupID: p.ID, + Name: name, + Description: descPtr, + ContribRequirements: contrib, + }) + if err != nil { + slog.Error("insert repo failed", "error", err) + http.Error(w, "Failed to create repository", http.StatusInternalServerError) + return + } + + repoPath := filepath.Join(base.Global.Config.Git.RepoDir, fmt.Sprintf("%d.git", repoID)) + + gitc, err := git2c.NewClient(r.Context(), base.Global.Config.Git.Socket) + if err != nil { + slog.Error("git2d connect failed", "error", err) + http.Error(w, "Failed to initialize repository (backend)", http.StatusInternalServerError) + return + } + defer func() { _ = gitc.Close() }() + if err = gitc.InitRepo(repoPath, base.Global.Config.Hooks.Execs); err != nil { + slog.Error("git2d init failed", "error", err) + http.Error(w, "Failed to initialize repository", http.StatusInternalServerError) + return + } + + if err = tx.Commit(r.Context()); err != nil { + slog.Error("commit tx failed", "error", err) + http.Error(w, "Failed to finalize repository creation", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) } diff --git a/forged/internal/incoming/web/handlers/index.go b/forged/internal/incoming/web/handlers/index.go index 22e6201..a758b07 100644 --- a/forged/internal/incoming/web/handlers/index.go +++ b/forged/internal/incoming/web/handlers/index.go @@ -6,7 +6,6 @@ import ( "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/templates" - "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" ) @@ -21,17 +20,17 @@ func NewIndexHTTP(r templates.Renderer) *IndexHTTP { } func (h *IndexHTTP) Index(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) { - groups, err := types.Base(r).Queries.GetRootGroups(r.Context()) + groups, err := wtypes.Base(r).Global.Queries.GetRootGroups(r.Context()) if err != nil { http.Error(w, "failed to get root groups", http.StatusInternalServerError) log.Println("failed to get root groups", "error", err) return } err = h.r.Render(w, "index", struct { - BaseData *types.BaseData + BaseData *wtypes.BaseData Groups []queries.GetRootGroupsRow }{ - BaseData: types.Base(r), + BaseData: wtypes.Base(r), Groups: groups, }) if err != nil { diff --git a/forged/internal/incoming/web/handlers/repo/index.go b/forged/internal/incoming/web/handlers/repo/index.go index 1a804b2..c2cb24a 100644 --- a/forged/internal/incoming/web/handlers/repo/index.go +++ b/forged/internal/incoming/web/handlers/repo/index.go @@ -1,20 +1,132 @@ package repo import ( + "bytes" + "fmt" + "html/template" + "log/slog" "net/http" + "net/url" + "path/filepath" "strings" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" + "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" + "go.lindenii.runxiyu.org/forge/forged/internal/ipc/git2c" ) func (h *HTTP) Index(w http.ResponseWriter, r *http.Request, v wtypes.Vars) { base := wtypes.Base(r) - repo := v["repo"] - _ = h.r.Render(w, "repo/index.html", struct { - Group string - Repo string - }{ - Group: "/" + strings.Join(base.GroupPath, "/") + "/", - Repo: repo, + repoName := v["repo"] + slog.Info("repo index", "group_path", base.GroupPath, "repo", repoName) + + var userID int64 + if base.UserID != "" { + _, _ = fmt.Sscan(base.UserID, &userID) + } + grp, err := base.Global.Queries.GetGroupByPath(r.Context(), queries.GetGroupByPathParams{ + Column1: base.GroupPath, + UserID: userID, + }) + if err != nil { + slog.Error("get group by path", "error", err) + http.Error(w, "Group not found", http.StatusNotFound) + return + } + + repoRow, err := base.Global.Queries.GetRepoByGroupAndName(r.Context(), queries.GetRepoByGroupAndNameParams{ + GroupID: grp.ID, + Name: repoName, }) + if err != nil { + slog.Error("get repo by name", "error", err) + http.Error(w, "Repository not found", http.StatusNotFound) + return + } + + repoPath := filepath.Join(base.Global.Config.Git.RepoDir, fmt.Sprintf("%d.git", repoRow.ID)) + + var commits []git2c.Commit + var readme template.HTML + var commitsErr error + var readmeFile *git2c.FilenameContents + var cerr error + client, err := git2c.NewClient(r.Context(), base.Global.Config.Git.Socket) + if err == nil { + defer func() { _ = client.Close() }() + commits, readmeFile, cerr = client.CmdIndex(repoPath) + if cerr != nil { + commitsErr = cerr + slog.Error("git2d CmdIndex failed", "error", cerr, "path", repoPath) + } else if readmeFile != nil { + nameLower := strings.ToLower(readmeFile.Filename) + if strings.HasSuffix(nameLower, ".md") || strings.HasSuffix(nameLower, ".markdown") || nameLower == "readme" { + md := goldmark.New( + goldmark.WithExtensions(extension.GFM), + ) + var buf bytes.Buffer + if err := md.Convert(readmeFile.Content, &buf); err == nil { + readme = template.HTML(buf.String()) + } else { + readme = template.HTML(template.HTMLEscapeString(string(readmeFile.Content))) + } + } else { + readme = template.HTML(template.HTMLEscapeString(string(readmeFile.Content))) + } + } + } else { + commitsErr = err + slog.Error("git2d connect failed", "error", err) + } + + sshRoot := strings.TrimSuffix(base.Global.Config.SSH.Root, "/") + httpRoot := strings.TrimSuffix(base.Global.Config.Web.Root, "/") + pathPart := misc.SegmentsToURL(base.GroupPath) + "/-/repos/" + url.PathEscape(repoRow.Name) + sshURL := "" + httpURL := "" + if sshRoot != "" { + sshURL = sshRoot + "/" + pathPart + } + if httpRoot != "" { + httpURL = httpRoot + "/" + pathPart + } + + var notes []string + if len(commits) == 0 && commitsErr == nil { + notes = append(notes, "This repository has no commits yet.") + } + if readme == template.HTML("") { + notes = append(notes, "No README found in the default branch.") + } + if sshURL == "" && httpURL == "" { + notes = append(notes, "Clone URLs not configured (missing SSH root and HTTP root).") + } + + cloneURL := sshURL + if cloneURL == "" { + cloneURL = httpURL + } + + data := map[string]any{ + "BaseData": base, + "group_path": base.GroupPath, + "repo_name": repoRow.Name, + "repo_description": repoRow.Description, + "ssh_clone_url": cloneURL, + "ref_name": base.RefName, + "commits": commits, + "commits_err": &commitsErr, + "readme": readme, + "notes": notes, + "global": map[string]any{ + "forge_title": base.Global.ForgeTitle, + }, + } + if err := h.r.Render(w, "repo_index", data); err != nil { + slog.Error("render repo index", "error", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } } diff --git a/forged/internal/incoming/web/handlers/repo/raw.go b/forged/internal/incoming/web/handlers/repo/raw.go index e421f45..8bdfae3 100644 --- a/forged/internal/incoming/web/handlers/repo/raw.go +++ b/forged/internal/incoming/web/handlers/repo/raw.go @@ -15,5 +15,5 @@ func (h *HTTP) Raw(w http.ResponseWriter, r *http.Request, v wtypes.Vars) { if base.DirMode && rest != "" && !strings.HasSuffix(rest, "/") { rest += "/" } - _, _ = w.Write([]byte(fmt.Sprintf("raw: repo=%q path=%q", repo, rest))) + _, _ = fmt.Fprintf(w, "raw: repo=%q path=%q", repo, rest) } diff --git a/forged/internal/incoming/web/handlers/repo/tree.go b/forged/internal/incoming/web/handlers/repo/tree.go index 3432244..236dd48 100644 --- a/forged/internal/incoming/web/handlers/repo/tree.go +++ b/forged/internal/incoming/web/handlers/repo/tree.go @@ -15,5 +15,5 @@ func (h *HTTP) Tree(w http.ResponseWriter, r *http.Request, v wtypes.Vars) { if base.DirMode && rest != "" && !strings.HasSuffix(rest, "/") { rest += "/" } - _, _ = w.Write([]byte(fmt.Sprintf("tree: repo=%q path=%q", repo, rest))) + _, _ = fmt.Fprintf(w, "tree: repo=%q path=%q", repo, rest) } diff --git a/forged/internal/incoming/web/handlers/special/login.go b/forged/internal/incoming/web/handlers/special/login.go index 0287c47..5672f1f 100644 --- a/forged/internal/incoming/web/handlers/special/login.go +++ b/forged/internal/incoming/web/handlers/special/login.go @@ -14,7 +14,6 @@ import ( "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/templates" - "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" ) @@ -33,10 +32,10 @@ func NewLoginHTTP(r templates.Renderer, cookieExpiry int) *LoginHTTP { func (h *LoginHTTP) Login(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) { renderLoginPage := func(loginError string) bool { err := h.r.Render(w, "login", struct { - BaseData *types.BaseData + BaseData *wtypes.BaseData LoginError string }{ - BaseData: types.Base(r), + BaseData: wtypes.Base(r), LoginError: loginError, }) if err != nil { @@ -55,7 +54,7 @@ func (h *LoginHTTP) Login(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) username := r.PostFormValue("username") password := r.PostFormValue("password") - userCreds, err := types.Base(r).Queries.GetUserCreds(r.Context(), &username) + userCreds, err := wtypes.Base(r).Global.Queries.GetUserCreds(r.Context(), &username) if err != nil { if errors.Is(err, pgx.ErrNoRows) { renderLoginPage("User not found") @@ -102,7 +101,7 @@ func (h *LoginHTTP) Login(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) tokenHash := sha256.Sum256(misc.StringToBytes(cookieValue)) - err = types.Base(r).Queries.InsertSession(r.Context(), queries.InsertSessionParams{ + err = wtypes.Base(r).Global.Queries.InsertSession(r.Context(), queries.InsertSessionParams{ UserID: userCreds.ID, TokenHash: tokenHash[:], ExpiresAt: pgtype.Timestamptz{ @@ -110,6 +109,11 @@ func (h *LoginHTTP) Login(w http.ResponseWriter, r *http.Request, _ wtypes.Vars) Valid: true, }, }) + if err != nil { + log.Println("failed to insert session", "error", err) + http.Error(w, "Failed to create session", http.StatusInternalServerError) + return + } http.Redirect(w, r, "/", http.StatusSeeOther) } diff --git a/forged/internal/incoming/web/router.go b/forged/internal/incoming/web/router.go index 8356191..3809afb 100644 --- a/forged/internal/incoming/web/router.go +++ b/forged/internal/incoming/web/router.go @@ -7,7 +7,6 @@ import ( "sort" "strings" - "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" "go.lindenii.runxiyu.org/forge/forged/internal/global" wtypes "go.lindenii.runxiyu.org/forge/forged/internal/incoming/web/types" ) @@ -62,7 +61,6 @@ type Router struct { user UserResolver global *global.Global reverseProxy bool - queries *queries.Queries } func NewRouter() *Router { return &Router{} } @@ -71,10 +69,6 @@ func (r *Router) Global(g *global.Global) *Router { r.global = g return r } -func (r *Router) Queries(q *queries.Queries) *Router { - r.queries = q - return r -} func (r *Router) ReverseProxy(enabled bool) *Router { r.reverseProxy = enabled; return r } func (r *Router) Errors(e ErrorRenderers) *Router { r.errors = e; return r } func (r *Router) UserResolver(u UserResolver) *Router { r.user = u; return r } @@ -148,7 +142,6 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { Global: r.global, URLSegments: segments, DirMode: dirMode, - Queries: r.queries, } req = req.WithContext(wtypes.WithBaseData(req.Context(), bd)) @@ -202,7 +195,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { bd.GroupPath = strings.Split(g, "/") } - if rt.method != "" && !(rt.method == method || (method == http.MethodHead && rt.method == http.MethodGet)) { + if rt.method != "" && rt.method != method && (method != http.MethodHead || rt.method != http.MethodGet) { continue } @@ -423,6 +416,4 @@ func GetParamRefTypeName(request *http.Request) (retRefType, retRefName string, return } -var ( - errDupRefSpec = fmt.Errorf("duplicate ref specifications") -) +var errDupRefSpec = fmt.Errorf("duplicate ref specifications") diff --git a/forged/internal/incoming/web/server.go b/forged/internal/incoming/web/server.go index 465657c..ab70aec 100644 --- a/forged/internal/incoming/web/server.go +++ b/forged/internal/incoming/web/server.go @@ -9,7 +9,6 @@ import ( "time" "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" - "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" "go.lindenii.runxiyu.org/forge/forged/internal/global" ) @@ -22,19 +21,20 @@ type Server struct { global *global.Global } -func New(config Config, global *global.Global, queries *queries.Queries) *Server { +func New(global *global.Global) *Server { + cfg := global.Config.Web httpServer := &http.Server{ - Handler: NewHandler(config, global, queries), - ReadTimeout: time.Duration(config.ReadTimeout) * time.Second, - WriteTimeout: time.Duration(config.WriteTimeout) * time.Second, - IdleTimeout: time.Duration(config.IdleTimeout) * time.Second, - MaxHeaderBytes: config.MaxHeaderBytes, + Handler: NewHandler(global), + ReadTimeout: time.Duration(cfg.ReadTimeout) * time.Second, + WriteTimeout: time.Duration(cfg.WriteTimeout) * time.Second, + IdleTimeout: time.Duration(cfg.IdleTimeout) * time.Second, + MaxHeaderBytes: cfg.MaxHeaderBytes, } //exhaustruct:ignore return &Server{ - net: config.Net, - addr: config.Addr, - root: config.Root, - shutdownTimeout: config.ShutdownTimeout, + net: cfg.Net, + addr: cfg.Addr, + root: cfg.Root, + shutdownTimeout: cfg.ShutdownTimeout, httpServer: httpServer, global: global, } diff --git a/forged/internal/incoming/web/templates/renderer.go b/forged/internal/incoming/web/templates/renderer.go index 1e2f325..350e9ec 100644 --- a/forged/internal/incoming/web/templates/renderer.go +++ b/forged/internal/incoming/web/templates/renderer.go @@ -1,7 +1,9 @@ package templates import ( + "bytes" "html/template" + "log/slog" "net/http" ) @@ -18,6 +20,16 @@ func New(t *template.Template) Renderer { } func (r *tmplRenderer) Render(w http.ResponseWriter, name string, data any) error { + var buf bytes.Buffer + if err := r.t.ExecuteTemplate(&buf, name, data); err != nil { + slog.Error("template render failed", "name", name, "error", err) + return err + } w.Header().Set("Content-Type", "text/html; charset=utf-8") - return r.t.ExecuteTemplate(w, name, data) + n, err := w.Write(buf.Bytes()) + if err != nil { + return err + } + slog.Info("template rendered", "name", name, "bytes", n) + return nil } diff --git a/forged/internal/incoming/web/types/types.go b/forged/internal/incoming/web/types/types.go index bacce24..4b9a65a 100644 --- a/forged/internal/incoming/web/types/types.go +++ b/forged/internal/incoming/web/types/types.go @@ -4,12 +4,9 @@ import ( "context" "net/http" - "go.lindenii.runxiyu.org/forge/forged/internal/database/queries" "go.lindenii.runxiyu.org/forge/forged/internal/global" ) -// BaseData is per-request context computed by the router and read by handlers. -// Keep it small and stable; page-specific data should live in view models. type BaseData struct { UserID string Username string @@ -20,17 +17,14 @@ type BaseData struct { RefType string RefName string Global *global.Global - Queries *queries.Queries } type ctxKey struct{} -// WithBaseData attaches BaseData to a context. func WithBaseData(ctx context.Context, b *BaseData) context.Context { return context.WithValue(ctx, ctxKey{}, b) } -// Base retrieves BaseData from the request (never nil). func Base(r *http.Request) *BaseData { if v, ok := r.Context().Value(ctxKey{}).(*BaseData); ok && v != nil { return v @@ -38,8 +32,6 @@ func Base(r *http.Request) *BaseData { return &BaseData{} } -// Vars are route variables captured by the router (e.g., :repo, *rest). type Vars map[string]string -// HandlerFunc is the router↔handler function contract. type HandlerFunc func(http.ResponseWriter, *http.Request, Vars) diff --git a/forged/internal/ipc/git2c/build.go b/forged/internal/ipc/git2c/build.go new file mode 100644 index 0000000..3d1b7a0 --- /dev/null +++ b/forged/internal/ipc/git2c/build.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> + +package git2c + +import ( + "encoding/hex" + "fmt" + "path" + "sort" + "strings" +) + +func (c *Client) BuildTreeRecursive(repoPath, baseTreeHex string, updates map[string]string) (string, error) { + treeCache := make(map[string][]TreeEntryRaw) + var walk func(prefix, hexid string) error + walk = func(prefix, hexid string) error { + ents, err := c.TreeListByOID(repoPath, hexid) + if err != nil { + return err + } + treeCache[prefix] = ents + for _, e := range ents { + if e.Mode == 40000 { + sub := path.Join(prefix, e.Name) + if err := walk(sub, e.OID); err != nil { + return err + } + } + } + return nil + } + if err := walk("", baseTreeHex); err != nil { + return "", err + } + + for p, blob := range updates { + parts := strings.Split(p, "/") + dir := strings.Join(parts[:len(parts)-1], "/") + name := parts[len(parts)-1] + entries := treeCache[dir] + found := false + for i := range entries { + if entries[i].Name == name { + if blob == "" { + entries = append(entries[:i], entries[i+1:]...) + } else { + entries[i].Mode = 0o100644 + entries[i].OID = blob + } + found = true + break + } + } + if !found && blob != "" { + entries = append(entries, TreeEntryRaw{Mode: 0o100644, Name: name, OID: blob}) + } + treeCache[dir] = entries + } + + built := make(map[string]string) + var build func(prefix string) (string, error) + build = func(prefix string) (string, error) { + entries := treeCache[prefix] + for i := range entries { + if entries[i].Mode == 0o40000 || entries[i].Mode == 40000 { + sub := path.Join(prefix, entries[i].Name) + var ok bool + var oid string + if oid, ok = built[sub]; !ok { + var err error + oid, err = build(sub) + if err != nil { + return "", err + } + } + entries[i].Mode = 0o40000 + entries[i].OID = oid + } + } + sort.Slice(entries, func(i, j int) bool { + ni, nj := entries[i].Name, entries[j].Name + if ni == nj { + return entries[i].Mode != 0o40000 && entries[j].Mode == 0o40000 + } + if strings.HasPrefix(nj, ni) && len(ni) < len(nj) { + return entries[i].Mode != 0o40000 + } + if strings.HasPrefix(ni, nj) && len(nj) < len(ni) { + return entries[j].Mode == 0o40000 + } + return ni < nj + }) + wr := make([]TreeEntryRaw, 0, len(entries)) + for _, e := range entries { + if e.OID == "" { + continue + } + if e.Mode == 40000 { + e.Mode = 0o40000 + } + if _, err := hex.DecodeString(e.OID); err != nil { + return "", fmt.Errorf("invalid OID hex for %s/%s: %w", prefix, e.Name, err) + } + wr = append(wr, TreeEntryRaw{Mode: e.Mode, Name: e.Name, OID: e.OID}) + } + id, err := c.WriteTree(repoPath, wr) + if err != nil { + return "", err + } + built[prefix] = id + return id, nil + } + root, err := build("") + if err != nil { + return "", err + } + return root, nil +} diff --git a/forged/internal/ipc/git2c/cmd_init_repo.go b/forged/internal/ipc/git2c/cmd_init_repo.go new file mode 100644 index 0000000..ae1e92a --- /dev/null +++ b/forged/internal/ipc/git2c/cmd_init_repo.go @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> + +package git2c + +import "fmt" + +func (c *Client) InitRepo(repoPath, hooksPath string) error { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(15); err != nil { + return fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(hooksPath)); err != nil { + return fmt.Errorf("sending hooks path failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return Perror(status) + } + return nil +} diff --git a/forged/internal/ipc/git2c/extra.go b/forged/internal/ipc/git2c/extra.go new file mode 100644 index 0000000..4d3a07e --- /dev/null +++ b/forged/internal/ipc/git2c/extra.go @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> + +package git2c + +import ( + "encoding/hex" + "fmt" + "time" +) + +func (c *Client) ResolveRef(repoPath, refType, refName string) (string, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return "", fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(3); err != nil { + return "", fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(refType)); err != nil { + return "", fmt.Errorf("sending ref type failed: %w", err) + } + if err := c.writer.WriteData([]byte(refName)); err != nil { + return "", fmt.Errorf("sending ref name failed: %w", err) + } + + status, err := c.reader.ReadUint() + if err != nil { + return "", fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return "", Perror(status) + } + id, err := c.reader.ReadData() + if err != nil { + return "", fmt.Errorf("reading oid failed: %w", err) + } + return hex.EncodeToString(id), nil +} + +func (c *Client) ListBranches(repoPath string) ([]string, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return nil, fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(4); err != nil { + return nil, fmt.Errorf("sending command failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return nil, fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return nil, Perror(status) + } + count, err := c.reader.ReadUint() + if err != nil { + return nil, fmt.Errorf("reading count failed: %w", err) + } + branches := make([]string, 0, count) + for range count { + name, err := c.reader.ReadData() + if err != nil { + return nil, fmt.Errorf("reading branch name failed: %w", err) + } + branches = append(branches, string(name)) + } + return branches, nil +} + +func (c *Client) FormatPatch(repoPath, commitHex string) (string, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return "", fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(5); err != nil { + return "", fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(commitHex)); err != nil { + return "", fmt.Errorf("sending commit failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return "", fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return "", Perror(status) + } + buf, err := c.reader.ReadData() + if err != nil { + return "", fmt.Errorf("reading patch failed: %w", err) + } + return string(buf), nil +} + +func (c *Client) CommitPatch(repoPath, commitHex string) (parentHex string, stats string, patch string, err error) { + if err = c.writer.WriteData([]byte(repoPath)); err != nil { + return "", "", "", fmt.Errorf("sending repo path failed: %w", err) + } + if err = c.writer.WriteUint(6); err != nil { + return "", "", "", fmt.Errorf("sending command failed: %w", err) + } + if err = c.writer.WriteData([]byte(commitHex)); err != nil { + return "", "", "", fmt.Errorf("sending commit failed: %w", err) + } + status, err2 := c.reader.ReadUint() + if err2 != nil { + return "", "", "", fmt.Errorf("reading status failed: %w", err2) + } + if status != 0 { + return "", "", "", Perror(status) + } + id, err2 := c.reader.ReadData() + if err2 != nil { + return "", "", "", fmt.Errorf("reading parent oid failed: %w", err2) + } + statsBytes, err2 := c.reader.ReadData() + if err2 != nil { + return "", "", "", fmt.Errorf("reading stats failed: %w", err2) + } + patchBytes, err2 := c.reader.ReadData() + if err2 != nil { + return "", "", "", fmt.Errorf("reading patch failed: %w", err2) + } + return hex.EncodeToString(id), string(statsBytes), string(patchBytes), nil +} + +func (c *Client) MergeBase(repoPath, hexA, hexB string) (string, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return "", fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(7); err != nil { + return "", fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(hexA)); err != nil { + return "", fmt.Errorf("sending oid A failed: %w", err) + } + if err := c.writer.WriteData([]byte(hexB)); err != nil { + return "", fmt.Errorf("sending oid B failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return "", fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return "", Perror(status) + } + base, err := c.reader.ReadData() + if err != nil { + return "", fmt.Errorf("reading base oid failed: %w", err) + } + return hex.EncodeToString(base), nil +} + +func (c *Client) Log(repoPath, refSpec string, n uint) ([]Commit, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return nil, fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(8); err != nil { + return nil, fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(refSpec)); err != nil { + return nil, fmt.Errorf("sending refspec failed: %w", err) + } + if err := c.writer.WriteUint(uint64(n)); err != nil { + return nil, fmt.Errorf("sending limit failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return nil, fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return nil, Perror(status) + } + var out []Commit + for { + id, err := c.reader.ReadData() + if err != nil { + break + } + title, _ := c.reader.ReadData() + authorName, _ := c.reader.ReadData() + authorEmail, _ := c.reader.ReadData() + date, _ := c.reader.ReadData() + out = append(out, Commit{ + Hash: hex.EncodeToString(id), + Author: string(authorName), + Email: string(authorEmail), + Date: string(date), + Message: string(title), + }) + } + return out, nil +} + +func (c *Client) CommitTreeOID(repoPath, commitHex string) (string, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return "", fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(12); err != nil { + return "", fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(commitHex)); err != nil { + return "", fmt.Errorf("sending oid failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return "", fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return "", Perror(status) + } + id, err := c.reader.ReadData() + if err != nil { + return "", fmt.Errorf("reading tree oid failed: %w", err) + } + return hex.EncodeToString(id), nil +} + +func (c *Client) CommitCreate(repoPath, treeHex string, parents []string, authorName, authorEmail string, when time.Time, message string) (string, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return "", fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(13); err != nil { + return "", fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(treeHex)); err != nil { + return "", fmt.Errorf("sending tree oid failed: %w", err) + } + if err := c.writer.WriteUint(uint64(len(parents))); err != nil { + return "", fmt.Errorf("sending parents count failed: %w", err) + } + for _, p := range parents { + if err := c.writer.WriteData([]byte(p)); err != nil { + return "", fmt.Errorf("sending parent oid failed: %w", err) + } + } + if err := c.writer.WriteData([]byte(authorName)); err != nil { + return "", fmt.Errorf("sending author name failed: %w", err) + } + if err := c.writer.WriteData([]byte(authorEmail)); err != nil { + return "", fmt.Errorf("sending author email failed: %w", err) + } + if err := c.writer.WriteInt(when.Unix()); err != nil { + return "", fmt.Errorf("sending when failed: %w", err) + } + _, offset := when.Zone() + if err := c.writer.WriteInt(int64(offset / 60)); err != nil { + return "", fmt.Errorf("sending tz offset failed: %w", err) + } + if err := c.writer.WriteData([]byte(message)); err != nil { + return "", fmt.Errorf("sending message failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return "", fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return "", Perror(status) + } + id, err := c.reader.ReadData() + if err != nil { + return "", fmt.Errorf("reading commit oid failed: %w", err) + } + return hex.EncodeToString(id), nil +} + +func (c *Client) UpdateRef(repoPath, refName, commitHex string) error { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(14); err != nil { + return fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(refName)); err != nil { + return fmt.Errorf("sending ref name failed: %w", err) + } + if err := c.writer.WriteData([]byte(commitHex)); err != nil { + return fmt.Errorf("sending commit oid failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return Perror(status) + } + return nil +} diff --git a/forged/internal/ipc/git2c/perror.go b/forged/internal/ipc/git2c/perror.go index 6bc7595..4be2a07 100644 --- a/forged/internal/ipc/git2c/perror.go +++ b/forged/internal/ipc/git2c/perror.go @@ -8,19 +8,33 @@ package git2c import "errors" var ( - ErrUnknown = errors.New("git2c: unknown error") - ErrPath = errors.New("git2c: get tree entry by path failed") - ErrRevparse = errors.New("git2c: revparse failed") - ErrReadme = errors.New("git2c: no readme") - ErrBlobExpected = errors.New("git2c: blob expected") - ErrEntryToObject = errors.New("git2c: tree entry to object conversion failed") - ErrBlobRawContent = errors.New("git2c: get blob raw content failed") - ErrRevwalk = errors.New("git2c: revwalk failed") - ErrRevwalkPushHead = errors.New("git2c: revwalk push head failed") - ErrBareProto = errors.New("git2c: bare protocol error") + ErrUnknown = errors.New("git2c: unknown error") + ErrPath = errors.New("git2c: get tree entry by path failed") + ErrRevparse = errors.New("git2c: revparse failed") + ErrReadme = errors.New("git2c: no readme") + ErrBlobExpected = errors.New("git2c: blob expected") + ErrEntryToObject = errors.New("git2c: tree entry to object conversion failed") + ErrBlobRawContent = errors.New("git2c: get blob raw content failed") + ErrRevwalk = errors.New("git2c: revwalk failed") + ErrRevwalkPushHead = errors.New("git2c: revwalk push head failed") + ErrBareProto = errors.New("git2c: bare protocol error") + ErrRefResolve = errors.New("git2c: ref resolve failed") + ErrBranches = errors.New("git2c: list branches failed") + ErrCommitLookup = errors.New("git2c: commit lookup failed") + ErrDiff = errors.New("git2c: diff failed") + ErrMergeBaseNone = errors.New("git2c: no merge base found") + ErrMergeBase = errors.New("git2c: merge base failed") + ErrCommitCreate = errors.New("git2c: commit create failed") + ErrUpdateRef = errors.New("git2c: update ref failed") + ErrCommitTree = errors.New("git2c: commit tree lookup failed") + ErrInitRepoCreate = errors.New("git2c: init repo: create failed") + ErrInitRepoConfig = errors.New("git2c: init repo: open config failed") + ErrInitRepoSetHooksPath = errors.New("git2c: init repo: set core.hooksPath failed") + ErrInitRepoSetAdvertisePushOptions = errors.New("git2c: init repo: set receive.advertisePushOptions failed") + ErrInitRepoMkdir = errors.New("git2c: init repo: create directory failed") ) -func Perror(errno uint) error { +func Perror(errno uint64) error { switch errno { case 0: return nil @@ -42,6 +56,32 @@ func Perror(errno uint) error { return ErrRevwalkPushHead case 11: return ErrBareProto + case 12: + return ErrRefResolve + case 13: + return ErrBranches + case 14: + return ErrCommitLookup + case 15: + return ErrDiff + case 16: + return ErrMergeBaseNone + case 17: + return ErrMergeBase + case 18: + return ErrUpdateRef + case 19: + return ErrCommitCreate + case 20: + return ErrInitRepoCreate + case 21: + return ErrInitRepoConfig + case 22: + return ErrInitRepoSetHooksPath + case 23: + return ErrInitRepoSetAdvertisePushOptions + case 24: + return ErrInitRepoMkdir } return ErrUnknown } diff --git a/forged/internal/ipc/git2c/tree.go b/forged/internal/ipc/git2c/tree.go new file mode 100644 index 0000000..f598e14 --- /dev/null +++ b/forged/internal/ipc/git2c/tree.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> + +package git2c + +import ( + "encoding/hex" + "fmt" +) + +type TreeEntryRaw struct { + Mode uint64 + Name string + OID string // hex +} + +func (c *Client) TreeListByOID(repoPath, treeHex string) ([]TreeEntryRaw, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return nil, fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(9); err != nil { + return nil, fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData([]byte(treeHex)); err != nil { + return nil, fmt.Errorf("sending tree oid failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return nil, fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return nil, Perror(status) + } + count, err := c.reader.ReadUint() + if err != nil { + return nil, fmt.Errorf("reading count failed: %w", err) + } + entries := make([]TreeEntryRaw, 0, count) + for range count { + mode, err := c.reader.ReadUint() + if err != nil { + return nil, fmt.Errorf("reading mode failed: %w", err) + } + name, err := c.reader.ReadData() + if err != nil { + return nil, fmt.Errorf("reading name failed: %w", err) + } + id, err := c.reader.ReadData() + if err != nil { + return nil, fmt.Errorf("reading oid failed: %w", err) + } + entries = append(entries, TreeEntryRaw{Mode: mode, Name: string(name), OID: hex.EncodeToString(id)}) + } + return entries, nil +} + +func (c *Client) WriteTree(repoPath string, entries []TreeEntryRaw) (string, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return "", fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(10); err != nil { + return "", fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteUint(uint64(len(entries))); err != nil { + return "", fmt.Errorf("sending count failed: %w", err) + } + for _, e := range entries { + if err := c.writer.WriteUint(e.Mode); err != nil { + return "", fmt.Errorf("sending mode failed: %w", err) + } + if err := c.writer.WriteData([]byte(e.Name)); err != nil { + return "", fmt.Errorf("sending name failed: %w", err) + } + raw, err := hex.DecodeString(e.OID) + if err != nil { + return "", fmt.Errorf("decode oid hex: %w", err) + } + if err := c.writer.WriteDataFixed(raw); err != nil { + return "", fmt.Errorf("sending oid failed: %w", err) + } + } + status, err := c.reader.ReadUint() + if err != nil { + return "", fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return "", Perror(status) + } + id, err := c.reader.ReadData() + if err != nil { + return "", fmt.Errorf("reading oid failed: %w", err) + } + return hex.EncodeToString(id), nil +} + +func (c *Client) WriteBlob(repoPath string, content []byte) (string, error) { + if err := c.writer.WriteData([]byte(repoPath)); err != nil { + return "", fmt.Errorf("sending repo path failed: %w", err) + } + if err := c.writer.WriteUint(11); err != nil { + return "", fmt.Errorf("sending command failed: %w", err) + } + if err := c.writer.WriteData(content); err != nil { + return "", fmt.Errorf("sending blob content failed: %w", err) + } + status, err := c.reader.ReadUint() + if err != nil { + return "", fmt.Errorf("reading status failed: %w", err) + } + if status != 0 { + return "", Perror(status) + } + id, err := c.reader.ReadData() + if err != nil { + return "", fmt.Errorf("reading oid failed: %w", err) + } + return hex.EncodeToString(id), nil +} diff --git a/forged/internal/ipc/irc/bot.go b/forged/internal/ipc/irc/bot.go deleted file mode 100644 index 07008ae..0000000 --- a/forged/internal/ipc/irc/bot.go +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> - -package irc - -import ( - "context" - "crypto/tls" - "fmt" - "log/slog" - "net" - - "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" -) - -// Bot represents an IRC bot client that handles events and allows for sending messages. -type Bot struct { - // TODO: Use each config field instead of embedding Config here. - config *Config - ircSendBuffered chan string - ircSendDirectChan chan misc.ErrorBack[string] -} - -// NewBot creates a new Bot instance using the provided configuration. -func NewBot(c *Config) (b *Bot) { - b = &Bot{ - config: c, - } //exhaustruct:ignore - return -} - -// Connect establishes a new IRC session and starts handling incoming and outgoing messages. -// This method blocks until an error occurs or the connection is closed. -func (b *Bot) Connect(ctx context.Context) error { - var err error - var underlyingConn net.Conn - if b.config.TLS { - dialer := tls.Dialer{} //exhaustruct:ignore - underlyingConn, err = dialer.DialContext(ctx, b.config.Net, b.config.Addr) - } else { - dialer := net.Dialer{} //exhaustruct:ignore - underlyingConn, err = dialer.DialContext(ctx, b.config.Net, b.config.Addr) - } - if err != nil { - return fmt.Errorf("dialing irc: %w", err) - } - defer func() { - _ = underlyingConn.Close() - }() - - conn := NewConn(underlyingConn) - - logAndWriteLn := func(s string) (n int, err error) { - slog.Debug("irc tx", "line", s) - return conn.WriteString(s + "\r\n") - } - - _, err = logAndWriteLn("NICK " + b.config.Nick) - if err != nil { - return err - } - _, err = logAndWriteLn("USER " + b.config.User + " 0 * :" + b.config.Gecos) - if err != nil { - return err - } - - readLoopError := make(chan error) - writeLoopAbort := make(chan struct{}) - go func() { - for { - select { - case <-writeLoopAbort: - return - default: - } - - msg, line, err := conn.ReadMessage() - if err != nil { - readLoopError <- err - return - } - - slog.Debug("irc rx", "line", line) - - switch msg.Command { - case "001": - _, err = logAndWriteLn("JOIN #chat") - if err != nil { - readLoopError <- err - return - } - case "PING": - _, err = logAndWriteLn("PONG :" + msg.Args[0]) - if err != nil { - readLoopError <- err - return - } - case "JOIN": - c, ok := msg.Source.(Client) - if !ok { - slog.Error("unable to convert source of JOIN to client") - } - if c.Nick != b.config.Nick { - continue - } - default: - } - } - }() - - for { - select { - case err = <-readLoopError: - return err - case line := <-b.ircSendBuffered: - _, err = logAndWriteLn(line) - if err != nil { - select { - case b.ircSendBuffered <- line: - default: - slog.Error("unable to requeue message", "line", line) - } - writeLoopAbort <- struct{}{} - return err - } - case lineErrorBack := <-b.ircSendDirectChan: - _, err = logAndWriteLn(lineErrorBack.Content) - lineErrorBack.ErrorChan <- err - if err != nil { - writeLoopAbort <- struct{}{} - return err - } - } - } -} - -// SendDirect sends an IRC message directly to the connection and bypasses -// the buffering system. -func (b *Bot) SendDirect(line string) error { - ech := make(chan error, 1) - - b.ircSendDirectChan <- misc.ErrorBack[string]{ - Content: line, - ErrorChan: ech, - } - - return <-ech -} - -// Send queues a message to be sent asynchronously via the buffered send queue. -// If the queue is full, the message is dropped and an error is logged. -func (b *Bot) Send(line string) { - select { - case b.ircSendBuffered <- line: - default: - slog.Error("irc sendq full", "line", line) - } -} - -// ConnectLoop continuously attempts to maintain an IRC session. -// If the connection drops, it automatically retries with no delay. -func (b *Bot) ConnectLoop(ctx context.Context) { - b.ircSendBuffered = make(chan string, b.config.SendQ) - b.ircSendDirectChan = make(chan misc.ErrorBack[string]) - - for { - err := b.Connect(ctx) - slog.Error("irc session error", "error", err) - } -} diff --git a/forged/internal/ipc/irc/config.go b/forged/internal/ipc/irc/config.go deleted file mode 100644 index b1b5703..0000000 --- a/forged/internal/ipc/irc/config.go +++ /dev/null @@ -1,13 +0,0 @@ -package irc - -// Config contains IRC connection and identity settings for the bot. -// This should usually be a part of the primary config struct. -type Config struct { - Net string `scfg:"net"` - Addr string `scfg:"addr"` - TLS bool `scfg:"tls"` - SendQ uint `scfg:"sendq"` - Nick string `scfg:"nick"` - User string `scfg:"user"` - Gecos string `scfg:"gecos"` -} diff --git a/forged/internal/ipc/irc/conn.go b/forged/internal/ipc/irc/conn.go deleted file mode 100644 index b9b208c..0000000 --- a/forged/internal/ipc/irc/conn.go +++ /dev/null @@ -1,58 +0,0 @@ -package irc - -import ( - "bufio" - "fmt" - "net" - "slices" - - "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" -) - -type Conn struct { - netConn net.Conn - bufReader *bufio.Reader -} - -func NewConn(netConn net.Conn) Conn { - return Conn{ - netConn: netConn, - bufReader: bufio.NewReader(netConn), - } -} - -func (c *Conn) ReadMessage() (msg Message, line string, err error) { - raw, err := c.bufReader.ReadSlice('\n') - if err != nil { - return - } - - if raw[len(raw)-1] == '\n' { - raw = raw[:len(raw)-1] - } - if raw[len(raw)-1] == '\r' { - raw = raw[:len(raw)-1] - } - - lineBytes := slices.Clone(raw) - line = misc.BytesToString(lineBytes) - msg, err = Parse(lineBytes) - - return -} - -func (c *Conn) Write(p []byte) (n int, err error) { - n, err = c.netConn.Write(p) - if err != nil { - err = fmt.Errorf("write to connection: %w", err) - } - return n, err -} - -func (c *Conn) WriteString(s string) (n int, err error) { - n, err = c.netConn.Write(misc.StringToBytes(s)) - if err != nil { - err = fmt.Errorf("write to connection: %w", err) - } - return n, err -} diff --git a/forged/internal/ipc/irc/doc.go b/forged/internal/ipc/irc/doc.go deleted file mode 100644 index dcfca82..0000000 --- a/forged/internal/ipc/irc/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package irc provides basic IRC bot functionality. -package irc diff --git a/forged/internal/ipc/irc/errors.go b/forged/internal/ipc/irc/errors.go deleted file mode 100644 index 3506c70..0000000 --- a/forged/internal/ipc/irc/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package irc - -import "errors" - -var ( - ErrInvalidIRCv3Tag = errors.New("invalid ircv3 tag") - ErrMalformedMsg = errors.New("malformed irc message") -) diff --git a/forged/internal/ipc/irc/message.go b/forged/internal/ipc/irc/message.go deleted file mode 100644 index 3387bec..0000000 --- a/forged/internal/ipc/irc/message.go +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: Copyright (c) 2018-2024 luk3yx <https://luk3yx.github.io> -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> - -package irc - -import ( - "bytes" - - "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" -) - -type Message struct { - Command string - Source Source - Tags map[string]string - Args []string -} - -// All strings returned are borrowed from the input byte slice. -func Parse(raw []byte) (msg Message, err error) { - sp := bytes.Split(raw, []byte{' '}) // TODO: Use bytes.Cut instead here - - if bytes.HasPrefix(sp[0], []byte{'@'}) { // TODO: Check size manually - if len(sp[0]) < 2 { - err = ErrMalformedMsg - return msg, err - } - sp[0] = sp[0][1:] - - msg.Tags, err = tagsToMap(sp[0]) - if err != nil { - return msg, err - } - - if len(sp) < 2 { - err = ErrMalformedMsg - return msg, err - } - sp = sp[1:] - } else { - msg.Tags = nil // TODO: Is a nil map the correct thing to use here? - } - - if bytes.HasPrefix(sp[0], []byte{':'}) { // TODO: Check size manually - if len(sp[0]) < 2 { - err = ErrMalformedMsg - return msg, err - } - sp[0] = sp[0][1:] - - msg.Source = parseSource(sp[0]) - - if len(sp) < 2 { - err = ErrMalformedMsg - return msg, err - } - sp = sp[1:] - } - - msg.Command = misc.BytesToString(sp[0]) - if len(sp) < 2 { - return msg, err - } - sp = sp[1:] - - for i := 0; i < len(sp); i++ { - if len(sp[i]) == 0 { - continue - } - if sp[i][0] == ':' { - if len(sp[i]) < 2 { - sp[i] = []byte{} - } else { - sp[i] = sp[i][1:] - } - msg.Args = append(msg.Args, misc.BytesToString(bytes.Join(sp[i:], []byte{' '}))) - // TODO: Avoid Join by not using sp in the first place - break - } - msg.Args = append(msg.Args, misc.BytesToString(sp[i])) - } - - return msg, err -} - -var ircv3TagEscapes = map[byte]byte{ //nolint:gochecknoglobals - ':': ';', - 's': ' ', - 'r': '\r', - 'n': '\n', -} - -func tagsToMap(raw []byte) (tags map[string]string, err error) { - tags = make(map[string]string) - for rawTag := range bytes.SplitSeq(raw, []byte{';'}) { - key, value, found := bytes.Cut(rawTag, []byte{'='}) - if !found { - err = ErrInvalidIRCv3Tag - return tags, err - } - if len(value) == 0 { - tags[misc.BytesToString(key)] = "" - } else { - if !bytes.Contains(value, []byte{'\\'}) { - tags[misc.BytesToString(key)] = misc.BytesToString(value) - } else { - valueUnescaped := bytes.NewBuffer(make([]byte, 0, len(value))) - for i := 0; i < len(value); i++ { - if value[i] == '\\' { - i++ - byteUnescaped, ok := ircv3TagEscapes[value[i]] - if !ok { - byteUnescaped = value[i] - } - valueUnescaped.WriteByte(byteUnescaped) - } else { - valueUnescaped.WriteByte(value[i]) - } - } - tags[misc.BytesToString(key)] = misc.BytesToString(valueUnescaped.Bytes()) - } - } - } - return tags, err -} diff --git a/forged/internal/ipc/irc/source.go b/forged/internal/ipc/irc/source.go deleted file mode 100644 index 938751f..0000000 --- a/forged/internal/ipc/irc/source.go +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> - -package irc - -import ( - "bytes" - - "go.lindenii.runxiyu.org/forge/forged/internal/common/misc" -) - -type Source interface { - AsSourceString() string -} - -//nolint:ireturn -func parseSource(s []byte) Source { - nick, userhost, found := bytes.Cut(s, []byte{'!'}) - if !found { - return Server{name: misc.BytesToString(s)} - } - - user, host, found := bytes.Cut(userhost, []byte{'@'}) - if !found { - return Server{name: misc.BytesToString(s)} - } - - return Client{ - Nick: misc.BytesToString(nick), - User: misc.BytesToString(user), - Host: misc.BytesToString(host), - } -} - -type Server struct { - name string -} - -func (s Server) AsSourceString() string { - return s.name -} - -type Client struct { - Nick string - User string - Host string -} - -func (c Client) AsSourceString() string { - return c.Nick + "!" + c.User + "@" + c.Host -} diff --git a/forged/internal/server/server.go b/forged/internal/server/server.go index 62a9442..39a6823 100644 --- a/forged/internal/server/server.go +++ b/forged/internal/server/server.go @@ -39,11 +39,13 @@ func New(configPath string) (server *Server, err error) { server.global.ForgeVersion = "unknown" // TODO server.global.ForgeTitle = server.config.General.Title + server.global.Config = &server.config + server.global.Queries = queries - server.hookServer = hooks.New(server.config.Hooks, &server.global) - server.lmtpServer = lmtp.New(server.config.LMTP, &server.global) - server.webServer = web.New(server.config.Web, &server.global, queries) - server.sshServer, err = ssh.New(server.config.SSH, &server.global) + server.hookServer = hooks.New(&server.global) + server.lmtpServer = lmtp.New(&server.global) + server.webServer = web.New(&server.global) + server.sshServer, err = ssh.New(&server.global) if err != nil { return server, fmt.Errorf("create SSH server: %w", err) } @@ -57,12 +59,15 @@ func (server *Server) Run(ctx context.Context) (err error) { g, gctx := errgroup.WithContext(ctx) - server.database, err = database.Open(gctx, server.config.DB) + server.database, err = database.Open(gctx, server.config.DB.Conn) if err != nil { return fmt.Errorf("open database: %w", err) } defer server.database.Close() + // TODO: neater way to do this for transactions in querypool? + server.global.DB = &server.database + g.Go(func() error { return server.hookServer.Run(gctx) }) g.Go(func() error { return server.lmtpServer.Run(gctx) }) g.Go(func() error { return server.webServer.Run(gctx) }) diff --git a/forged/sql/queries/repos.sql b/forged/sql/queries/repos.sql new file mode 100644 index 0000000..cacc5b8 --- /dev/null +++ b/forged/sql/queries/repos.sql @@ -0,0 +1,9 @@ +-- name: InsertRepo :one +INSERT INTO repos (group_id, name, description, contrib_requirements) +VALUES ($1, $2, $3, $4) +RETURNING id; + +-- name: GetRepoByGroupAndName :one +SELECT id, name, COALESCE(description, '') AS description +FROM repos +WHERE group_id = $1 AND name = $2; diff --git a/forged/sql/schema.sql b/forged/sql/schema.sql index 2f5ef9a..72327a9 100644 --- a/forged/sql/schema.sql +++ b/forged/sql/schema.sql @@ -24,8 +24,9 @@ CREATE TABLE repos ( name TEXT NOT NULL, description TEXT, contrib_requirements contrib_requirement NOT NULL, - filesystem_path TEXT NOT NULL, -- does not have to be unique, double-mounting is allowed UNIQUE(group_id, name) + -- The filesystem path can be derived from the repo ID. + -- The config has repo_dir, then we can do repo_dir/<id>.git ); CREATE INDEX grepos_group_idx ON repos(group_id); @@ -113,7 +114,7 @@ CREATE TABLE tickets ( UNIQUE(tracker_id, tracker_local_id) ); -CREATE OR REPLACE FUNCTION create_tracker_ticket_sequence() +CREATE FUNCTION create_tracker_ticket_sequence() RETURNS TRIGGER AS $$ DECLARE seq_name TEXT := format('tracker_ticket_seq_%s', NEW.id); @@ -122,7 +123,7 @@ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION drop_tracker_ticket_sequence() +CREATE FUNCTION drop_tracker_ticket_sequence() RETURNS TRIGGER AS $$ DECLARE seq_name TEXT := format('tracker_ticket_seq_%s', OLD.id); @@ -131,17 +132,15 @@ BEGIN RETURN OLD; END; $$ LANGUAGE plpgsql; -DROP TRIGGER IF EXISTS after_insert_ticket_tracker ON ticket_trackers; CREATE TRIGGER after_insert_ticket_tracker AFTER INSERT ON ticket_trackers FOR EACH ROW EXECUTE FUNCTION create_tracker_ticket_sequence(); -DROP TRIGGER IF EXISTS before_delete_ticket_tracker ON ticket_trackers; CREATE TRIGGER before_delete_ticket_tracker BEFORE DELETE ON ticket_trackers FOR EACH ROW EXECUTE FUNCTION drop_tracker_ticket_sequence(); -CREATE OR REPLACE FUNCTION assign_tracker_local_id() +CREATE FUNCTION assign_tracker_local_id() RETURNS TRIGGER AS $$ DECLARE seq_name TEXT := format('tracker_ticket_seq_%s', NEW.tracker_id); @@ -152,7 +151,6 @@ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql; -DROP TRIGGER IF EXISTS before_insert_ticket ON tickets; CREATE TRIGGER before_insert_ticket BEFORE INSERT ON tickets FOR EACH ROW @@ -180,7 +178,7 @@ CREATE UNIQUE INDEX gmr_open_src_dst_uniq WHERE status = 'open'; CREATE INDEX gmr_repo_idx ON merge_requests(repo_id); CREATE INDEX gmr_creator_idx ON merge_requests(creator); -CREATE OR REPLACE FUNCTION create_repo_mr_sequence() +CREATE FUNCTION create_repo_mr_sequence() RETURNS TRIGGER AS $$ DECLARE seq_name TEXT := format('repo_mr_seq_%s', NEW.id); @@ -189,7 +187,7 @@ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION drop_repo_mr_sequence() +CREATE FUNCTION drop_repo_mr_sequence() RETURNS TRIGGER AS $$ DECLARE seq_name TEXT := format('repo_mr_seq_%s', OLD.id); @@ -198,17 +196,15 @@ BEGIN RETURN OLD; END; $$ LANGUAGE plpgsql; -DROP TRIGGER IF EXISTS after_insert_repo ON repos; CREATE TRIGGER after_insert_repo AFTER INSERT ON repos FOR EACH ROW EXECUTE FUNCTION create_repo_mr_sequence(); -DROP TRIGGER IF EXISTS before_delete_repo ON repos; CREATE TRIGGER before_delete_repo BEFORE DELETE ON repos FOR EACH ROW EXECUTE FUNCTION drop_repo_mr_sequence(); -CREATE OR REPLACE FUNCTION assign_repo_local_id() +CREATE FUNCTION assign_repo_local_id() RETURNS TRIGGER AS $$ DECLARE seq_name TEXT := format('repo_mr_seq_%s', NEW.repo_id); @@ -219,7 +215,6 @@ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql; -DROP TRIGGER IF EXISTS before_insert_merge_request ON merge_requests; CREATE TRIGGER before_insert_merge_request BEFORE INSERT ON merge_requests FOR EACH ROW diff --git a/forged/templates/group.tmpl b/forged/templates/group.tmpl index 31b7169..1f9609e 100644 --- a/forged/templates/group.tmpl +++ b/forged/templates/group.tmpl @@ -47,7 +47,7 @@ <th scope="row">Contrib</th> <td class="tdinput"> <select id="repo-contrib-input" name="repo_contrib"> - <option value="public">Public</option> + <option value="open">Public</option> <option value="ssh_pubkey">SSH public key</option> <option value="federated">Federated service</option> <option value="registered_user">Registered user</option> |