ref: 33fda1b8e4da0ad9d4208a8b8249c8d7b305f4ae
parent: f329f9e6ddc0f47a29df41082f4f12b9466bdac7
author: Runxi Yu <me@runxiyu.org>
date: Sat Mar 7 20:56:58 EST 2026
cmd/receivepack9418: Init
--- /dev/null
+++ b/cmd/receivepack9418/conn.go
@@ -1,0 +1,65 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "log"
+ "net"
+ "strings"
+
+ "codeberg.org/lindenii/furgit/receivepack"
+)
+
+func (srv *server) handleConn(conn net.Conn) {+ defer func() { _ = conn.Close() }()+
+ reader := bufio.NewReader(conn)
+ writer := bufio.NewWriter(conn)
+
+ req, err := readGitProtoRequest(reader)
+ if err != nil {+ writeErrPkt(writer, fmt.Sprintf("invalid initial request: %v", err))+ _ = writer.Flush()
+ log.Printf("receivepack9418: %s: invalid initial request: %v", conn.RemoteAddr(), err)+ return
+ }
+
+ if req.Command != "git-receive-pack" {+ writeErrPkt(writer, fmt.Sprintf("unsupported command %q", req.Command))+ _ = writer.Flush()
+ log.Printf("receivepack9418: %s: unsupported command %q", conn.RemoteAddr(), req.Command)+ return
+ }
+
+ gitProtocol := strings.Join(req.ExtraParameters, ":")
+
+ opts := receivepack.Options{+ GitProtocol: gitProtocol,
+ Algorithm: srv.repo.Algorithm(),
+ Refs: srv.repo.Refs(),
+ ExistingObjects: srv.repo.Objects(),
+ ObjectsRoot: srv.objectsRoot,
+ }
+
+ err = receivepack.ReceivePack(context.Background(), writer, reader, opts)
+ if err != nil {+ _ = writer.Flush()
+ log.Printf(
+ "receivepack9418: %s: receive-pack failed (path=%q host=%q extras=%v): %v",
+ conn.RemoteAddr(),
+ req.Pathname,
+ req.Host,
+ req.ExtraParameters,
+ err,
+ )
+
+ return
+ }
+
+ err = writer.Flush()
+ if err != nil {+ log.Printf("receivepack9418: %s: flush failed: %v", conn.RemoteAddr(), err)+ return
+ }
+}
--- /dev/null
+++ b/cmd/receivepack9418/errpkt.go
@@ -1,0 +1,18 @@
+package main
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/format/pktline"
+)
+
+func writeErrPkt(w io.Writer, message string) {+ payload := []byte("ERR " + message + "\n")+
+ frame, err := pktline.AppendData(nil, payload)
+ if err != nil {+ return
+ }
+
+ _, _ = w.Write(frame)
+}
--- /dev/null
+++ b/cmd/receivepack9418/gitproto.go
@@ -1,0 +1,23 @@
+package main
+
+import (
+ "fmt"
+ "io"
+
+ "codeberg.org/lindenii/furgit/format/pktline"
+)
+
+func readGitProtoRequest(r io.Reader) (gitProtoRequest, error) {+ dec := pktline.NewDecoder(r, pktline.ReadOptions{})+
+ frame, err := dec.ReadFrame()
+ if err != nil {+ return gitProtoRequest{}, err+ }
+
+ if frame.Type != pktline.PacketData {+ return gitProtoRequest{}, fmt.Errorf("expected initial pkt-line data, got %v", frame.Type)+ }
+
+ return parseGitProtoRequestPayload(frame.Payload)
+}
--- /dev/null
+++ b/cmd/receivepack9418/main.go
@@ -1,0 +1,23 @@
+// Command receivepack9418 serves one fixed repository over git:// receive-pack on TCP 9418.
+package main
+
+import (
+ "flag"
+ "log"
+)
+
+func main() {+ listenAddr := flag.String("listen", ":9418", "listen address")+ repoPath := flag.String("repo", "", "path to git dir (.git or bare repo root)")+
+ flag.Parse()
+
+ if *repoPath == "" {+ log.Fatal("must provide -repo <path-to-git-dir>")+ }
+
+ err := run(*listenAddr, *repoPath)
+ if err != nil {+ log.Fatalf("run: %v", err)+ }
+}
--- /dev/null
+++ b/cmd/receivepack9418/request.go
@@ -1,0 +1,60 @@
+package main
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "strings"
+)
+
+type gitProtoRequest struct {+ Command string
+ Pathname string
+ Host string
+ ExtraParameters []string
+}
+
+func parseGitProtoRequestPayload(payload []byte) (gitProtoRequest, error) {+ parts := bytes.Split(payload, []byte{0})+ if len(parts) == 0 || len(parts[0]) == 0 {+ return gitProtoRequest{}, errors.New("missing command/path segment")+ }
+
+ commandPath := string(parts[0])
+ command, pathname, ok := strings.Cut(commandPath, " ")
+ if !ok || command == "" || pathname == "" {+ return gitProtoRequest{}, fmt.Errorf("malformed command/path segment %q", commandPath)+ }
+
+ req := gitProtoRequest{+ Command: command,
+ Pathname: pathname,
+ }
+
+ i := 1
+ if i < len(parts) && strings.HasPrefix(string(parts[i]), "host=") {+ req.Host = strings.TrimPrefix(string(parts[i]), "host=")
+ i++
+ }
+
+ // No tail left.
+ if i >= len(parts) {+ return req, nil
+ }
+
+ // If there is tail, grammar requires one empty field before extras.
+ if len(parts[i]) != 0 {+ return gitProtoRequest{}, fmt.Errorf("unexpected token %q after host/path", string(parts[i]))+ }
+
+ i++
+ for ; i < len(parts); i++ {+ if len(parts[i]) == 0 {+ continue
+ }
+
+ req.ExtraParameters = append(req.ExtraParameters, string(parts[i]))
+ }
+
+ return req, nil
+}
--- /dev/null
+++ b/cmd/receivepack9418/run.go
@@ -1,0 +1,64 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "net"
+ "os"
+
+ "codeberg.org/lindenii/furgit/repository"
+)
+
+func run(listenAddr, repoPath string) error {+ repoRoot, err := os.OpenRoot(repoPath)
+ if err != nil {+ return fmt.Errorf("open repo root: %w", err)+ }
+ defer func() { _ = repoRoot.Close() }()+
+ repo, err := repository.Open(repoRoot)
+ if err != nil {+ return fmt.Errorf("open repository: %w", err)+ }
+ defer func() { _ = repo.Close() }()+
+ objectsRoot, err := repoRoot.OpenRoot("objects")+ if err != nil {+ return fmt.Errorf("open objects root: %w", err)+ }
+ defer func() { _ = objectsRoot.Close() }()+
+ srv := &server{+ repo: repo,
+ objectsRoot: objectsRoot,
+ }
+
+ ln, err := net.Listen("tcp", listenAddr)+ if err != nil {+ return fmt.Errorf("listen %q: %w", listenAddr, err)+ }
+ defer func() { _ = ln.Close() }()+
+ log.Printf("receivepack9418: listening on %s", listenAddr)+ log.Printf("receivepack9418: repository=%s algorithm=%s", repoPath, repo.Algorithm())+
+ for {+ conn, err := ln.Accept()
+ if err != nil {+ if errors.Is(err, net.ErrClosed) {+ return nil
+ }
+
+ var nerr net.Error
+ if errors.As(err, &nerr) && nerr.Temporary() {+ log.Printf("receivepack9418: temporary accept error: %v", err)+ continue
+ }
+
+ return fmt.Errorf("accept: %w", err)+ }
+
+ go srv.handleConn(conn)
+ }
+}
--- /dev/null
+++ b/cmd/receivepack9418/server.go
@@ -1,0 +1,12 @@
+package main
+
+import (
+ "os"
+
+ "codeberg.org/lindenii/furgit/repository"
+)
+
+type server struct {+ repo *repository.Repository
+ objectsRoot *os.Root
+}
--
⑨