aboutsummaryrefslogtreecommitdiff
path: root/git_hooks_handle_other.go
diff options
context:
space:
mode:
Diffstat (limited to 'git_hooks_handle_other.go')
-rw-r--r--git_hooks_handle_other.go336
1 files changed, 0 insertions, 336 deletions
diff --git a/git_hooks_handle_other.go b/git_hooks_handle_other.go
deleted file mode 100644
index da40bb6..0000000
--- a/git_hooks_handle_other.go
+++ /dev/null
@@ -1,336 +0,0 @@
-// SPDX-License-Identifier: AGPL-3.0-only
-// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
-//
-//go:build !linux
-
-package forge
-
-import (
- "bytes"
- "context"
- "encoding/binary"
- "errors"
- "fmt"
- "io"
- "net"
- "path/filepath"
- "strconv"
- "strings"
-
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
- "github.com/jackc/pgx/v5"
- "go.lindenii.runxiyu.org/forge/internal/ansiec"
- "go.lindenii.runxiyu.org/forge/internal/misc"
-)
-
-// hooksHandler handles a connection from hookc via the
-// unix socket.
-func (s *Server) hooksHandler(conn net.Conn) {
- var ctx context.Context
- var cancel context.CancelFunc
- var err error
- var cookie []byte
- var packPass packPass
- var sshStderr io.Writer
- var hookRet byte
-
- defer conn.Close()
- ctx, cancel = context.WithCancel(context.Background())
- defer cancel()
-
- // TODO: ucred-like checks
-
- cookie = make([]byte, 64)
- if _, err = conn.Read(cookie); err != nil {
- if _, err = conn.Write([]byte{1}); err != nil {
- return
- }
- writeRedError(conn, "\nFailed to read cookie: %v", err)
- return
- }
-
- {
- var ok bool
- packPass, ok = s.packPasses.Load(misc.BytesToString(cookie))
- if !ok {
- if _, err = conn.Write([]byte{1}); err != nil {
- return
- }
- writeRedError(conn, "\nInvalid handler cookie")
- return
- }
- }
-
- sshStderr = packPass.session.Stderr()
-
- _, _ = sshStderr.Write([]byte{'\n'})
-
- hookRet = func() byte {
- var argc64 uint64
- if err = binary.Read(conn, binary.NativeEndian, &argc64); err != nil {
- writeRedError(sshStderr, "Failed to read argc: %v", err)
- return 1
- }
- var args []string
- for range argc64 {
- var arg bytes.Buffer
- for {
- nextByte := make([]byte, 1)
- n, err := conn.Read(nextByte)
- if err != nil || n != 1 {
- writeRedError(sshStderr, "Failed to read arg: %v", err)
- return 1
- }
- if nextByte[0] == 0 {
- break
- }
- arg.WriteByte(nextByte[0])
- }
- args = append(args, arg.String())
- }
-
- gitEnv := make(map[string]string)
- for {
- var envLine bytes.Buffer
- for {
- nextByte := make([]byte, 1)
- n, err := conn.Read(nextByte)
- if err != nil || n != 1 {
- writeRedError(sshStderr, "Failed to read environment variable: %v", err)
- return 1
- }
- if nextByte[0] == 0 {
- break
- }
- envLine.WriteByte(nextByte[0])
- }
- if envLine.Len() == 0 {
- break
- }
- kv := envLine.String()
- parts := strings.SplitN(kv, "=", 2)
- if len(parts) < 2 {
- writeRedError(sshStderr, "Invalid environment variable line: %v", kv)
- return 1
- }
- gitEnv[parts[0]] = parts[1]
- }
-
- var stdin bytes.Buffer
- if _, err = io.Copy(&stdin, conn); err != nil {
- writeRedError(conn, "Failed to read to the stdin buffer: %v", err)
- }
-
- switch filepath.Base(args[0]) {
- case "pre-receive":
- if packPass.directAccess {
- return 0
- }
- allOK := true
- for {
- var line, oldOID, rest, newIOID, refName string
- var found bool
- var oldHash, newHash plumbing.Hash
- var oldCommit, newCommit *object.Commit
- var pushOptCount int
-
- pushOptCount, err = strconv.Atoi(gitEnv["GIT_PUSH_OPTION_COUNT"])
- if err != nil {
- writeRedError(sshStderr, "Failed to parse GIT_PUSH_OPTION_COUNT: %v", err)
- return 1
- }
-
- // TODO: Allow existing users (even if they are already federated or registered) to add a federated user ID... though perhaps this should be in the normal SSH interface instead of the git push interface?
- // Also it'd be nice to be able to combine users or whatever
- if packPass.contribReq == "federated" && packPass.userType != "federated" && packPass.userType != "registered" {
- if pushOptCount == 0 {
- writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
- return 1
- }
- for pushOptIndex := range pushOptCount {
- pushOpt, ok := gitEnv[fmt.Sprintf("GIT_PUSH_OPTION_%d", pushOptIndex)]
- if !ok {
- writeRedError(sshStderr, "Failed to get push option %d", pushOptIndex)
- return 1
- }
- if strings.HasPrefix(pushOpt, "fedid=") {
- fedUserID := strings.TrimPrefix(pushOpt, "fedid=")
- service, username, found := strings.Cut(fedUserID, ":")
- if !found {
- writeRedError(sshStderr, "Invalid federated user identifier %#v does not contain a colon", fedUserID)
- return 1
- }
-
- ok, err := s.fedauth(ctx, packPass.userID, service, username, packPass.pubkey)
- if err != nil {
- writeRedError(sshStderr, "Failed to verify federated user identifier %#v: %v", fedUserID, err)
- return 1
- }
- if !ok {
- writeRedError(sshStderr, "Failed to verify federated user identifier %#v: you don't seem to be on the list", fedUserID)
- return 1
- }
-
- break
- }
- if pushOptIndex == pushOptCount-1 {
- writeRedError(sshStderr, "This repo requires contributors to be either federated or registered users. You must supply your federated user ID as a push option. For example, git push -o fedid=sr.ht:runxiyu")
- return 1
- }
- }
- }
-
- line, err = stdin.ReadString('\n')
- if errors.Is(err, io.EOF) {
- break
- } else if err != nil {
- writeRedError(sshStderr, "Failed to read pre-receive line: %v", err)
- return 1
- }
- line = line[:len(line)-1]
-
- oldOID, rest, found = strings.Cut(line, " ")
- if !found {
- writeRedError(sshStderr, "Invalid pre-receive line: %v", line)
- return 1
- }
-
- newIOID, refName, found = strings.Cut(rest, " ")
- if !found {
- writeRedError(sshStderr, "Invalid pre-receive line: %v", line)
- return 1
- }
-
- if strings.HasPrefix(refName, "refs/heads/contrib/") {
- if allZero(oldOID) { // New branch
- fmt.Fprintln(sshStderr, ansiec.Blue+"POK"+ansiec.Reset, refName)
- var newMRLocalID int
-
- if packPass.userID != 0 {
- 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,
- "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)
- }
- if err != nil {
- writeRedError(sshStderr, "Error creating merge request: %v", err)
- return 1
- }
- 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)
-
- s.ircBot.Send("PRIVMSG #chat :New merge request at " + mergeRequestWebURL)
- } else { // Existing contrib branch
- var existingMRUser int
- var isAncestor bool
-
- 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)
- if err != nil {
- if errors.Is(err, pgx.ErrNoRows) {
- writeRedError(sshStderr, "No existing merge request for existing contrib branch: %v", err)
- } else {
- writeRedError(sshStderr, "Error querying for existing merge request: %v", err)
- }
- return 1
- }
- if existingMRUser == 0 {
- allOK = false
- fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(branch belongs to unowned MR)")
- continue
- }
-
- if existingMRUser != packPass.userID {
- allOK = false
- fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(branch belongs another user's MR)")
- continue
- }
-
- oldHash = plumbing.NewHash(oldOID)
-
- if oldCommit, err = packPass.repo.CommitObject(oldHash); err != nil {
- writeRedError(sshStderr, "Daemon failed to get old commit: %v", err)
- return 1
- }
-
- // Potential BUG: I'm not sure if new_commit is guaranteed to be
- // detectable as they haven't been merged into the main repo's
- // objects yet. But it seems to work, and I don't think there's
- // any reason for this to only work intermitently.
- newHash = plumbing.NewHash(newIOID)
- if newCommit, err = packPass.repo.CommitObject(newHash); err != nil {
- writeRedError(sshStderr, "Daemon failed to get new commit: %v", err)
- return 1
- }
-
- if isAncestor, err = oldCommit.IsAncestor(newCommit); err != nil {
- writeRedError(sshStderr, "Daemon failed to check if old commit is ancestor: %v", err)
- return 1
- }
-
- if !isAncestor {
- // TODO: Create MR snapshot ref instead
- allOK = false
- fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(force pushes are not supported yet)")
- continue
- }
-
- fmt.Fprintln(sshStderr, ansiec.Blue+"POK"+ansiec.Reset, refName)
- }
- } else { // Non-contrib branch
- allOK = false
- fmt.Fprintln(sshStderr, ansiec.Red+"NAK"+ansiec.Reset, refName, "(you cannot push to branches outside of contrib/*)")
- }
- }
-
- fmt.Fprintln(sshStderr)
- if allOK {
- fmt.Fprintln(sshStderr, "Overall "+ansiec.Green+"ACK"+ansiec.Reset+" (all checks passed)")
- return 0
- }
- fmt.Fprintln(sshStderr, "Overall "+ansiec.Red+"NAK"+ansiec.Reset+" (one or more branches failed checks)")
- return 1
- default:
- fmt.Fprintln(sshStderr, ansiec.Red+"Invalid hook:", args[0]+ansiec.Reset)
- return 1
- }
- }()
-
- fmt.Fprintln(sshStderr)
-
- _, _ = conn.Write([]byte{hookRet})
-}
-
-// 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 {
- for {
- conn, err := listener.Accept()
- if err != nil {
- return err
- }
- go s.hooksHandler(conn)
- }
-}
-
-// allZero returns true if all runes in a given string are '0'. The comparison
-// is not constant time and must not be used in contexts where time-based side
-// channel attacks are a concern.
-func allZero(s string) bool {
- for _, r := range s {
- if r != '0' {
- return false
- }
- }
- return true
-}