all repos

mugit @ d0b4683

🐮 git server that your cow will love
6 files changed, 134 insertions(+), 202 deletions(-)
git: refactor gitserver again; and rename
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-02-13 01:58:03 +0200
Change ID: tnsvzwwsnzlkqkrmtyoouuqnmuuskzlt
Parent: cce3826
D

@@ -1,180 +0,0 @@

-package gitservice - -import ( - "bytes" - "fmt" - "io" - "os/exec" - "sync" - "syscall" -) - -// InfoRefs executes git-upload-pack --advertise-refs for smart-HTTP discovery. -func InfoRefs(dir string, out io.Writer) error { - cmd := exec.Command("git", - "upload-pack", - "--stateless-rpc", - "--advertise-refs", - ".") - cmd.Dir = dir - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - - stdout, err := cmd.StdoutPipe() - if err != nil { - return fmt.Errorf("stdout pipe: %w", err) - } - cmd.Stderr = cmd.Stdout - - if err := cmd.Start(); err != nil { - return fmt.Errorf("start git-upload-pack: %w", err) - } - - if err := PackLine(out, "# service=git-upload-pack\n"); err != nil { - return fmt.Errorf("write pack line: %w", err) - } - if err := PackFlush(out); err != nil { - return fmt.Errorf("flush pack: %w", err) - } - - var buf bytes.Buffer - if _, err := io.Copy(&buf, stdout); err != nil { - return fmt.Errorf("copy stdout to buffer: %w", err) - } - - if err := cmd.Wait(); err != nil { - return fmt.Errorf("git-upload-pack: %w", err) - } - - if _, err := io.Copy(out, &buf); err != nil { - return fmt.Errorf("copy buffer to output: %w", err) - } - - return nil -} - -// UploadPack executes git-upload-pack for smart-HTTP git fetch/clone. -// StatelessRPC should be true in case it's used over http, and false for ssh. -func UploadPack(dir string, statelessRPC bool, in io.Reader, out io.Writer) error { - return gitCmd("upload-pack", config{ - Dir: dir, - StatelessRPC: statelessRPC, - AllowFilter: true, - Stdin: in, - Stdout: out, - }) -} - -func ReceivePack(dir string, in io.Reader, out, errout io.Writer) error { - return gitCmd("receive-pack", config{ - Dir: dir, - Stdin: in, - Stdout: out, - Stderr: errout, - }) -} - -type config struct { - Dir string - StatelessRPC bool - AllowFilter bool - ExtraArgs []string - Stdin io.Reader - Stdout io.Writer - Stderr io.Writer -} - -func gitCmd(service string, c config) error { - args := []string{} - if c.AllowFilter { - args = append(args, "-c", "uploadpack.allowFilter=true") - } - - args = append(args, service) - if c.StatelessRPC { - args = append(args, "--stateless-rpc") - } - - args = append(args, c.ExtraArgs...) - args = append(args, ".") - - cmd := exec.Command("git", args...) - cmd.Dir = c.Dir - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - - var ( - err error - stdin io.WriteCloser - ) - - if c.Stdin != nil { - stdin, err = cmd.StdinPipe() - if err != nil { - return err - } - } - - stdout, err := cmd.StdoutPipe() - if err != nil { - return err - } - if c.Stderr != nil { - cmd.Stderr = c.Stderr - } else { - cmd.Stderr = cmd.Stdout - } - - if err := cmd.Start(); err != nil { - return fmt.Errorf("start %s: %w", service, err) - } - - if c.Stdin != nil { - // Don't add to WaitGroup - stdin closes when client closes, - // shouldn't block waiting for output to finish - go func() { - defer stdin.Close() - io.Copy(stdin, c.Stdin) - }() - } - - var wg sync.WaitGroup - var stdoutErr error - - wg.Go(func() { - _, stdoutErr = io.Copy(c.Stdout, stdout) - }) - - wg.Wait() - - if stdoutErr != nil { - return fmt.Errorf("copy stdout: %w", stdoutErr) - } - - if err := cmd.Wait(); err != nil { - return fmt.Errorf("%s: %w", service, err) - } - return nil -} - -// PackLine writes a pkt-line formatted string. -func PackLine(w io.Writer, s string) error { - _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s) - return err -} - -// PackFlush writes a flush packet. -func PackFlush(w io.Writer) error { - _, err := fmt.Fprint(w, "0000") - return err -} - -// PackSideband writes a message to sideband channel (displays as "remote: <msg>" in git client). -// Channel 2 = progress/info, Channel 3 = error. -func PackSideband(w io.Writer, channel byte, msg string) error { - return PackLine(w, string(channel)+msg) -} - -// PackError writes an ERR packet for protocol-level errors. -// Git displays this as: fatal: remote error: <msg> -func PackError(w io.Writer, msg string) error { - return PackLine(w, "ERR "+msg) -}
A internal/git/gitx/gitx.go

@@ -0,0 +1,6 @@

+package gitx + +var gitEnv = []string{ + "GIT_CONFIG_GLOBAL=/dev/null", + "GIT_CONFIG_SYSTEM=/dev/null", +}
A internal/git/gitx/pack.go

@@ -0,0 +1,82 @@

+package gitx + +import ( + "context" + "fmt" + "io" + "os/exec" + "syscall" +) + +// InfoRefs executes git-upload-pack --advertise-refs for smart-HTTP discovery. +func InfoRefs(ctx context.Context, repoDir string, out io.Writer) error { + cmd := exec.CommandContext(ctx, "git", []string{ + "-c", "uploadpack.allowFilter=true", + "upload-pack", + "--stateless-rpc", + "--advertise-refs", + ".", + }...) + cmd.Dir = repoDir + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Env = gitEnv + + cmd.Stdout = out + cmd.Stderr = out // TODO: Check if this is correct. + + if err := PackLine(out, "# service=git-upload-pack\n"); err != nil { + return fmt.Errorf("write pack line: %w", err) + } + if err := PackFlush(out); err != nil { + return fmt.Errorf("flush pack: %w", err) + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("git-upload-pack: %w", err) + } + + return nil +} + +// UploadPack executes git-upload-pack for smart-HTTP git fetch/clone. +// StatelessRPC should be true in case it's used over http, and false for ssh. +func UploadPack(ctx context.Context, repoDir string, statelessRPC bool, in io.Reader, out io.Writer) error { + args := []string{"-c", "uploadpack.allowFilter=true", "upload-pack"} + if statelessRPC { + args = append(args, "--stateless-rpc") + } + args = append(args, ".") + + cmd := exec.CommandContext(ctx, "git", args...) + cmd.Dir = repoDir + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Env = gitEnv + + cmd.Stdin = in + cmd.Stdout = out + cmd.Stderr = out // TODO: Check if this is correct. + + if err := cmd.Run(); err != nil { + return fmt.Errorf("git-upload-pack: %w", err) + } + + return nil +} + +// ReceivePack executes git-receive-pack for git push. +func ReceivePack(ctx context.Context, repoDir string, in io.Reader, out, errout io.Writer) error { + cmd := exec.CommandContext(ctx, "git", "receive-pack", ".") + cmd.Dir = repoDir + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Env = gitEnv + + cmd.Stdin = in + cmd.Stdout = out + cmd.Stderr = errout + + if err := cmd.Run(); err != nil { + return fmt.Errorf("git-receive-pack: %w", err) + } + + return nil +}
A internal/git/gitx/protocol.go

@@ -0,0 +1,24 @@

+package gitx + +import ( + "fmt" + "io" +) + +// PackLine writes a pkt-line formatted string. +func PackLine(w io.Writer, s string) error { + _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s) + return err +} + +// PackFlush writes a flush packet. +func PackFlush(w io.Writer) error { + _, err := fmt.Fprint(w, "0000") + return err +} + +// PackError writes an ERR packet for protocol-level errors. +// Git displays this as: fatal: remote error: <msg> +func PackError(w io.Writer, msg string) error { + return PackLine(w, "ERR "+msg) +}
M internal/handlers/git.go

@@ -7,7 +7,7 @@ "log/slog"

"net/http" securejoin "github.com/cyphar/filepath-securejoin" - "olexsmir.xyz/mugit/internal/git/gitservice" + "olexsmir.xyz/mugit/internal/git/gitx" ) // multiplex, check if the request smells like gitprotocol-http(5), if so, it

@@ -37,9 +37,6 @@ h.write404(w, err)

return } - w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") - w.WriteHeader(http.StatusOK) - repoPath := repoNameToPath(name) path, err := securejoin.SecureJoin(h.c.Repo.Dir, repoPath) if err != nil {

@@ -48,7 +45,9 @@ slog.Error("git: info/refs", "err", err)

return } - if err := gitservice.InfoRefs(path, w); err != nil { + w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") + w.WriteHeader(http.StatusOK) + if err := gitx.InfoRefs(r.Context(), path, w); err != nil { w.WriteHeader(http.StatusInternalServerError) slog.Error("git: info/refs", "err", err) return

@@ -63,16 +62,11 @@ h.write404(w, err)

return } - w.Header().Set("content-type", "application/x-git-upload-pack-result") - w.Header().Set("Connection", "Keep-Alive") - w.Header().Set("Transfer-Encoding", "chunked") - w.WriteHeader(http.StatusOK) - reader := io.Reader(r.Body) if r.Header.Get("Content-Encoding") == "gzip" { gr, gerr := gzip.NewReader(r.Body) if gerr != nil { - w.WriteHeader(http.StatusInternalServerError) + http.Error(w, "invalid gzip encoding", http.StatusBadRequest) slog.Error("git: gzip reader", "err", gerr) return }

@@ -83,13 +77,18 @@

repoPath := repoNameToPath(name) path, err := securejoin.SecureJoin(h.c.Repo.Dir, repoPath) if err != nil { - w.WriteHeader(http.StatusBadRequest) - slog.Error("git: info/refs", "err", err) + http.Error(w, "invalid path", http.StatusBadRequest) + slog.Error("git: upload-pack path", "err", err) return } - if err := gitservice.UploadPack(path, true, reader, newFlushWriter(w)); err != nil { - w.WriteHeader(http.StatusInternalServerError) + w.Header().Set("content-type", "application/x-git-upload-pack-result") + w.Header().Set("Connection", "Keep-Alive") + w.Header().Set("Transfer-Encoding", "chunked") + w.WriteHeader(http.StatusOK) + + if err := gitx.UploadPack(r.Context(), path, true, reader, newFlushWriter(w)); err != nil { + // Don't call w.WriteHeader here - connection already started! slog.Error("git: upload-pack", "err", err) return }
M internal/ssh/ssh.go

@@ -11,7 +11,7 @@ securejoin "github.com/cyphar/filepath-securejoin"

"github.com/gliderlabs/ssh" "olexsmir.xyz/mugit/internal/config" "olexsmir.xyz/mugit/internal/git" - "olexsmir.xyz/mugit/internal/git/gitservice" + "olexsmir.xyz/mugit/internal/git/gitx" gossh "golang.org/x/crypto/ssh" )

@@ -67,6 +67,7 @@ return true

} func (s *Server) handler(sess ssh.Session) { + ctx := sess.Context() authorized := sess.Context().Value(authorizedKey).(bool) cmd := sess.Command()

@@ -108,7 +109,7 @@ s.repoNotFound(sess)

return } - if err := gitservice.UploadPack(fullPath, false, sess, sess); err != nil { + if err := gitx.UploadPack(ctx, fullPath, false, sess, sess); err != nil { s.error(sess, err) return }

@@ -119,7 +120,7 @@ s.unauthorized(sess)

return } - if err := gitservice.ReceivePack(fullPath, sess, sess, sess.Stderr()); err != nil { + if err := gitx.ReceivePack(ctx, fullPath, sess, sess, sess.Stderr()); err != nil { s.error(sess, err) return }

@@ -127,7 +128,7 @@ sess.Exit(0)

default: slog.Error("ssh unsupported command", "cmd", cmd) - gitservice.PackError(sess, "Unsupported command.") + gitx.PackError(sess, "Unsupported command.") sess.Exit(1) } }

@@ -146,18 +147,18 @@ return nil

} func (s *Server) repoNotFound(sess ssh.Session) { - gitservice.PackError(sess, "Repository not found.") + gitx.PackError(sess, "Repository not found.") sess.Exit(1) } func (s *Server) unauthorized(sess ssh.Session) { - gitservice.PackError(sess, "You are not authorized to push to this repository.") + gitx.PackError(sess, "You are not authorized to push to this repository.") sess.Exit(1) } func (s *Server) error(sess ssh.Session, err error) { slog.Error("error on ssh side", "err", err) - gitservice.PackError(sess, "Unexpected server error.") + gitx.PackError(sess, "Unexpected server error.") sess.Exit(1) }