all repos

mugit @ d0b468313e7de5fa180542b8b4dcc73914350bee

馃惍 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
Authored at: 2026-02-12 23:46:49 +0200
Change ID: tnsvzwwsnzlkqkrmtyoouuqnmuuskzlt
Parent: cce3826
D internal/git/gitservice/gitservice.go
路路路
        1
        
        -package gitservice

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"bytes"

      
        5
        
        -	"fmt"

      
        6
        
        -	"io"

      
        7
        
        -	"os/exec"

      
        8
        
        -	"sync"

      
        9
        
        -	"syscall"

      
        10
        
        -)

      
        11
        
        -

      
        12
        
        -// InfoRefs executes git-upload-pack --advertise-refs for smart-HTTP discovery.

      
        13
        
        -func InfoRefs(dir string, out io.Writer) error {

      
        14
        
        -	cmd := exec.Command("git",

      
        15
        
        -		"upload-pack",

      
        16
        
        -		"--stateless-rpc",

      
        17
        
        -		"--advertise-refs",

      
        18
        
        -		".")

      
        19
        
        -	cmd.Dir = dir

      
        20
        
        -	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

      
        21
        
        -

      
        22
        
        -	stdout, err := cmd.StdoutPipe()

      
        23
        
        -	if err != nil {

      
        24
        
        -		return fmt.Errorf("stdout pipe: %w", err)

      
        25
        
        -	}

      
        26
        
        -	cmd.Stderr = cmd.Stdout

      
        27
        
        -

      
        28
        
        -	if err := cmd.Start(); err != nil {

      
        29
        
        -		return fmt.Errorf("start git-upload-pack: %w", err)

      
        30
        
        -	}

      
        31
        
        -

      
        32
        
        -	if err := PackLine(out, "# service=git-upload-pack\n"); err != nil {

      
        33
        
        -		return fmt.Errorf("write pack line: %w", err)

      
        34
        
        -	}

      
        35
        
        -	if err := PackFlush(out); err != nil {

      
        36
        
        -		return fmt.Errorf("flush pack: %w", err)

      
        37
        
        -	}

      
        38
        
        -

      
        39
        
        -	var buf bytes.Buffer

      
        40
        
        -	if _, err := io.Copy(&buf, stdout); err != nil {

      
        41
        
        -		return fmt.Errorf("copy stdout to buffer: %w", err)

      
        42
        
        -	}

      
        43
        
        -

      
        44
        
        -	if err := cmd.Wait(); err != nil {

      
        45
        
        -		return fmt.Errorf("git-upload-pack: %w", err)

      
        46
        
        -	}

      
        47
        
        -

      
        48
        
        -	if _, err := io.Copy(out, &buf); err != nil {

      
        49
        
        -		return fmt.Errorf("copy buffer to output: %w", err)

      
        50
        
        -	}

      
        51
        
        -

      
        52
        
        -	return nil

      
        53
        
        -}

      
        54
        
        -

      
        55
        
        -// UploadPack executes git-upload-pack for smart-HTTP git fetch/clone.

      
        56
        
        -// StatelessRPC should be true in case it's used over http, and false for ssh.

      
        57
        
        -func UploadPack(dir string, statelessRPC bool, in io.Reader, out io.Writer) error {

      
        58
        
        -	return gitCmd("upload-pack", config{

      
        59
        
        -		Dir:          dir,

      
        60
        
        -		StatelessRPC: statelessRPC,

      
        61
        
        -		AllowFilter:  true,

      
        62
        
        -		Stdin:        in,

      
        63
        
        -		Stdout:       out,

      
        64
        
        -	})

      
        65
        
        -}

      
        66
        
        -

      
        67
        
        -func ReceivePack(dir string, in io.Reader, out, errout io.Writer) error {

      
        68
        
        -	return gitCmd("receive-pack", config{

      
        69
        
        -		Dir:    dir,

      
        70
        
        -		Stdin:  in,

      
        71
        
        -		Stdout: out,

      
        72
        
        -		Stderr: errout,

      
        73
        
        -	})

      
        74
        
        -}

      
        75
        
        -

      
        76
        
        -type config struct {

      
        77
        
        -	Dir          string

      
        78
        
        -	StatelessRPC bool

      
        79
        
        -	AllowFilter  bool

      
        80
        
        -	ExtraArgs    []string

      
        81
        
        -	Stdin        io.Reader

      
        82
        
        -	Stdout       io.Writer

      
        83
        
        -	Stderr       io.Writer

      
        84
        
        -}

      
        85
        
        -

      
        86
        
        -func gitCmd(service string, c config) error {

      
        87
        
        -	args := []string{}

      
        88
        
        -	if c.AllowFilter {

      
        89
        
        -		args = append(args, "-c", "uploadpack.allowFilter=true")

      
        90
        
        -	}

      
        91
        
        -

      
        92
        
        -	args = append(args, service)

      
        93
        
        -	if c.StatelessRPC {

      
        94
        
        -		args = append(args, "--stateless-rpc")

      
        95
        
        -	}

      
        96
        
        -

      
        97
        
        -	args = append(args, c.ExtraArgs...)

      
        98
        
        -	args = append(args, ".")

      
        99
        
        -

      
        100
        
        -	cmd := exec.Command("git", args...)

      
        101
        
        -	cmd.Dir = c.Dir

      
        102
        
        -	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

      
        103
        
        -

      
        104
        
        -	var (

      
        105
        
        -		err   error

      
        106
        
        -		stdin io.WriteCloser

      
        107
        
        -	)

      
        108
        
        -

      
        109
        
        -	if c.Stdin != nil {

      
        110
        
        -		stdin, err = cmd.StdinPipe()

      
        111
        
        -		if err != nil {

      
        112
        
        -			return err

      
        113
        
        -		}

      
        114
        
        -	}

      
        115
        
        -

      
        116
        
        -	stdout, err := cmd.StdoutPipe()

      
        117
        
        -	if err != nil {

      
        118
        
        -		return err

      
        119
        
        -	}

      
        120
        
        -	if c.Stderr != nil {

      
        121
        
        -		cmd.Stderr = c.Stderr

      
        122
        
        -	} else {

      
        123
        
        -		cmd.Stderr = cmd.Stdout

      
        124
        
        -	}

      
        125
        
        -

      
        126
        
        -	if err := cmd.Start(); err != nil {

      
        127
        
        -		return fmt.Errorf("start %s: %w", service, err)

      
        128
        
        -	}

      
        129
        
        -

      
        130
        
        -	if c.Stdin != nil {

      
        131
        
        -		// Don't add to WaitGroup - stdin closes when client closes,

      
        132
        
        -		// shouldn't block waiting for output to finish

      
        133
        
        -		go func() {

      
        134
        
        -			defer stdin.Close()

      
        135
        
        -			io.Copy(stdin, c.Stdin)

      
        136
        
        -		}()

      
        137
        
        -	}

      
        138
        
        -

      
        139
        
        -	var wg sync.WaitGroup

      
        140
        
        -	var stdoutErr error

      
        141
        
        -

      
        142
        
        -	wg.Go(func() {

      
        143
        
        -		_, stdoutErr = io.Copy(c.Stdout, stdout)

      
        144
        
        -	})

      
        145
        
        -

      
        146
        
        -	wg.Wait()

      
        147
        
        -

      
        148
        
        -	if stdoutErr != nil {

      
        149
        
        -		return fmt.Errorf("copy stdout: %w", stdoutErr)

      
        150
        
        -	}

      
        151
        
        -

      
        152
        
        -	if err := cmd.Wait(); err != nil {

      
        153
        
        -		return fmt.Errorf("%s: %w", service, err)

      
        154
        
        -	}

      
        155
        
        -	return nil

      
        156
        
        -}

      
        157
        
        -

      
        158
        
        -// PackLine writes a pkt-line formatted string.

      
        159
        
        -func PackLine(w io.Writer, s string) error {

      
        160
        
        -	_, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)

      
        161
        
        -	return err

      
        162
        
        -}

      
        163
        
        -

      
        164
        
        -// PackFlush writes a flush packet.

      
        165
        
        -func PackFlush(w io.Writer) error {

      
        166
        
        -	_, err := fmt.Fprint(w, "0000")

      
        167
        
        -	return err

      
        168
        
        -}

      
        169
        
        -

      
        170
        
        -// PackSideband writes a message to sideband channel (displays as "remote: <msg>" in git client).

      
        171
        
        -// Channel 2 = progress/info, Channel 3 = error.

      
        172
        
        -func PackSideband(w io.Writer, channel byte, msg string) error {

      
        173
        
        -	return PackLine(w, string(channel)+msg)

      
        174
        
        -}

      
        175
        
        -

      
        176
        
        -// PackError writes an ERR packet for protocol-level errors.

      
        177
        
        -// Git displays this as: fatal: remote error: <msg>

      
        178
        
        -func PackError(w io.Writer, msg string) error {

      
        179
        
        -	return PackLine(w, "ERR "+msg)

      
        180
        
        -}

      
A internal/git/gitx/gitx.go
路路路
        
        1
        +package gitx

      
        
        2
        +

      
        
        3
        +var gitEnv = []string{

      
        
        4
        +	"GIT_CONFIG_GLOBAL=/dev/null",

      
        
        5
        +	"GIT_CONFIG_SYSTEM=/dev/null",

      
        
        6
        +}

      
A internal/git/gitx/pack.go
路路路
        
        1
        +package gitx

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"fmt"

      
        
        6
        +	"io"

      
        
        7
        +	"os/exec"

      
        
        8
        +	"syscall"

      
        
        9
        +)

      
        
        10
        +

      
        
        11
        +// InfoRefs executes git-upload-pack --advertise-refs for smart-HTTP discovery.

      
        
        12
        +func InfoRefs(ctx context.Context, repoDir string, out io.Writer) error {

      
        
        13
        +	cmd := exec.CommandContext(ctx, "git", []string{

      
        
        14
        +		"-c", "uploadpack.allowFilter=true",

      
        
        15
        +		"upload-pack",

      
        
        16
        +		"--stateless-rpc",

      
        
        17
        +		"--advertise-refs",

      
        
        18
        +		".",

      
        
        19
        +	}...)

      
        
        20
        +	cmd.Dir = repoDir

      
        
        21
        +	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

      
        
        22
        +	cmd.Env = gitEnv

      
        
        23
        +

      
        
        24
        +	cmd.Stdout = out

      
        
        25
        +	cmd.Stderr = out // TODO: Check if this is correct.

      
        
        26
        +

      
        
        27
        +	if err := PackLine(out, "# service=git-upload-pack\n"); err != nil {

      
        
        28
        +		return fmt.Errorf("write pack line: %w", err)

      
        
        29
        +	}

      
        
        30
        +	if err := PackFlush(out); err != nil {

      
        
        31
        +		return fmt.Errorf("flush pack: %w", err)

      
        
        32
        +	}

      
        
        33
        +

      
        
        34
        +	if err := cmd.Run(); err != nil {

      
        
        35
        +		return fmt.Errorf("git-upload-pack: %w", err)

      
        
        36
        +	}

      
        
        37
        +

      
        
        38
        +	return nil

      
        
        39
        +}

      
        
        40
        +

      
        
        41
        +// UploadPack executes git-upload-pack for smart-HTTP git fetch/clone.

      
        
        42
        +// StatelessRPC should be true in case it's used over http, and false for ssh.

      
        
        43
        +func UploadPack(ctx context.Context, repoDir string, statelessRPC bool, in io.Reader, out io.Writer) error {

      
        
        44
        +	args := []string{"-c", "uploadpack.allowFilter=true", "upload-pack"}

      
        
        45
        +	if statelessRPC {

      
        
        46
        +		args = append(args, "--stateless-rpc")

      
        
        47
        +	}

      
        
        48
        +	args = append(args, ".")

      
        
        49
        +

      
        
        50
        +	cmd := exec.CommandContext(ctx, "git", args...)

      
        
        51
        +	cmd.Dir = repoDir

      
        
        52
        +	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

      
        
        53
        +	cmd.Env = gitEnv

      
        
        54
        +

      
        
        55
        +	cmd.Stdin = in

      
        
        56
        +	cmd.Stdout = out

      
        
        57
        +	cmd.Stderr = out // TODO: Check if this is correct.

      
        
        58
        +

      
        
        59
        +	if err := cmd.Run(); err != nil {

      
        
        60
        +		return fmt.Errorf("git-upload-pack: %w", err)

      
        
        61
        +	}

      
        
        62
        +

      
        
        63
        +	return nil

      
        
        64
        +}

      
        
        65
        +

      
        
        66
        +// ReceivePack executes git-receive-pack for git push.

      
        
        67
        +func ReceivePack(ctx context.Context, repoDir string, in io.Reader, out, errout io.Writer) error {

      
        
        68
        +	cmd := exec.CommandContext(ctx, "git", "receive-pack", ".")

      
        
        69
        +	cmd.Dir = repoDir

      
        
        70
        +	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

      
        
        71
        +	cmd.Env = gitEnv

      
        
        72
        +

      
        
        73
        +	cmd.Stdin = in

      
        
        74
        +	cmd.Stdout = out

      
        
        75
        +	cmd.Stderr = errout

      
        
        76
        +

      
        
        77
        +	if err := cmd.Run(); err != nil {

      
        
        78
        +		return fmt.Errorf("git-receive-pack: %w", err)

      
        
        79
        +	}

      
        
        80
        +

      
        
        81
        +	return nil

      
        
        82
        +}

      
A internal/git/gitx/protocol.go
路路路
        
        1
        +package gitx

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +	"io"

      
        
        6
        +)

      
        
        7
        +

      
        
        8
        +// PackLine writes a pkt-line formatted string.

      
        
        9
        +func PackLine(w io.Writer, s string) error {

      
        
        10
        +	_, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)

      
        
        11
        +	return err

      
        
        12
        +}

      
        
        13
        +

      
        
        14
        +// PackFlush writes a flush packet.

      
        
        15
        +func PackFlush(w io.Writer) error {

      
        
        16
        +	_, err := fmt.Fprint(w, "0000")

      
        
        17
        +	return err

      
        
        18
        +}

      
        
        19
        +

      
        
        20
        +// PackError writes an ERR packet for protocol-level errors.

      
        
        21
        +// Git displays this as: fatal: remote error: <msg>

      
        
        22
        +func PackError(w io.Writer, msg string) error {

      
        
        23
        +	return PackLine(w, "ERR "+msg)

      
        
        24
        +}

      
M internal/handlers/git.go
路路路
        7
        7
         	"net/http"

      
        8
        8
         

      
        9
        9
         	securejoin "github.com/cyphar/filepath-securejoin"

      
        10
        
        -	"olexsmir.xyz/mugit/internal/git/gitservice"

      
        
        10
        +	"olexsmir.xyz/mugit/internal/git/gitx"

      
        11
        11
         )

      
        12
        12
         

      
        13
        13
         // multiplex, check if the request smells like gitprotocol-http(5), if so, it

      路路路
        37
        37
         		return

      
        38
        38
         	}

      
        39
        39
         

      
        40
        
        -	w.Header().Set("content-type", "application/x-git-upload-pack-advertisement")

      
        41
        
        -	w.WriteHeader(http.StatusOK)

      
        42
        
        -

      
        43
        40
         	repoPath := repoNameToPath(name)

      
        44
        41
         	path, err := securejoin.SecureJoin(h.c.Repo.Dir, repoPath)

      
        45
        42
         	if err != nil {

      路路路
        48
        45
         		return

      
        49
        46
         	}

      
        50
        47
         

      
        51
        
        -	if err := gitservice.InfoRefs(path, w); err != nil {

      
        
        48
        +	w.Header().Set("content-type", "application/x-git-upload-pack-advertisement")

      
        
        49
        +	w.WriteHeader(http.StatusOK)

      
        
        50
        +	if err := gitx.InfoRefs(r.Context(), path, w); err != nil {

      
        52
        51
         		w.WriteHeader(http.StatusInternalServerError)

      
        53
        52
         		slog.Error("git: info/refs", "err", err)

      
        54
        53
         		return

      路路路
        63
        62
         		return

      
        64
        63
         	}

      
        65
        64
         

      
        66
        
        -	w.Header().Set("content-type", "application/x-git-upload-pack-result")

      
        67
        
        -	w.Header().Set("Connection", "Keep-Alive")

      
        68
        
        -	w.Header().Set("Transfer-Encoding", "chunked")

      
        69
        
        -	w.WriteHeader(http.StatusOK)

      
        70
        
        -

      
        71
        65
         	reader := io.Reader(r.Body)

      
        72
        66
         	if r.Header.Get("Content-Encoding") == "gzip" {

      
        73
        67
         		gr, gerr := gzip.NewReader(r.Body)

      
        74
        68
         		if gerr != nil {

      
        75
        
        -			w.WriteHeader(http.StatusInternalServerError)

      
        
        69
        +			http.Error(w, "invalid gzip encoding", http.StatusBadRequest)

      
        76
        70
         			slog.Error("git: gzip reader", "err", gerr)

      
        77
        71
         			return

      
        78
        72
         		}

      路路路
        83
        77
         	repoPath := repoNameToPath(name)

      
        84
        78
         	path, err := securejoin.SecureJoin(h.c.Repo.Dir, repoPath)

      
        85
        79
         	if err != nil {

      
        86
        
        -		w.WriteHeader(http.StatusBadRequest)

      
        87
        
        -		slog.Error("git: info/refs", "err", err)

      
        
        80
        +		http.Error(w, "invalid path", http.StatusBadRequest)

      
        
        81
        +		slog.Error("git: upload-pack path", "err", err)

      
        88
        82
         		return

      
        89
        83
         	}

      
        90
        84
         

      
        91
        
        -	if err := gitservice.UploadPack(path, true, reader, newFlushWriter(w)); err != nil {

      
        92
        
        -		w.WriteHeader(http.StatusInternalServerError)

      
        
        85
        +	w.Header().Set("content-type", "application/x-git-upload-pack-result")

      
        
        86
        +	w.Header().Set("Connection", "Keep-Alive")

      
        
        87
        +	w.Header().Set("Transfer-Encoding", "chunked")

      
        
        88
        +	w.WriteHeader(http.StatusOK)

      
        
        89
        +

      
        
        90
        +	if err := gitx.UploadPack(r.Context(), path, true, reader, newFlushWriter(w)); err != nil {

      
        
        91
        +		// Don't call w.WriteHeader here - connection already started!

      
        93
        92
         		slog.Error("git: upload-pack", "err", err)

      
        94
        93
         		return

      
        95
        94
         	}

      
M internal/ssh/ssh.go
路路路
        11
        11
         	"github.com/gliderlabs/ssh"

      
        12
        12
         	"olexsmir.xyz/mugit/internal/config"

      
        13
        13
         	"olexsmir.xyz/mugit/internal/git"

      
        14
        
        -	"olexsmir.xyz/mugit/internal/git/gitservice"

      
        
        14
        +	"olexsmir.xyz/mugit/internal/git/gitx"

      
        15
        15
         

      
        16
        16
         	gossh "golang.org/x/crypto/ssh"

      
        17
        17
         )

      路路路
        67
        67
         }

      
        68
        68
         

      
        69
        69
         func (s *Server) handler(sess ssh.Session) {

      
        
        70
        +	ctx := sess.Context()

      
        70
        71
         	authorized := sess.Context().Value(authorizedKey).(bool)

      
        71
        72
         

      
        72
        73
         	cmd := sess.Command()

      路路路
        108
        109
         			return

      
        109
        110
         		}

      
        110
        111
         

      
        111
        
        -		if err := gitservice.UploadPack(fullPath, false, sess, sess); err != nil {

      
        
        112
        +		if err := gitx.UploadPack(ctx, fullPath, false, sess, sess); err != nil {

      
        112
        113
         			s.error(sess, err)

      
        113
        114
         			return

      
        114
        115
         		}

      路路路
        119
        120
         			return

      
        120
        121
         		}

      
        121
        122
         

      
        122
        
        -		if err := gitservice.ReceivePack(fullPath, sess, sess, sess.Stderr()); err != nil {

      
        
        123
        +		if err := gitx.ReceivePack(ctx, fullPath, sess, sess, sess.Stderr()); err != nil {

      
        123
        124
         			s.error(sess, err)

      
        124
        125
         			return

      
        125
        126
         		}

      路路路
        127
        128
         

      
        128
        129
         	default:

      
        129
        130
         		slog.Error("ssh unsupported command", "cmd", cmd)

      
        130
        
        -		gitservice.PackError(sess, "Unsupported command.")

      
        
        131
        +		gitx.PackError(sess, "Unsupported command.")

      
        131
        132
         		sess.Exit(1)

      
        132
        133
         	}

      
        133
        134
         }

      路路路
        146
        147
         }

      
        147
        148
         

      
        148
        149
         func (s *Server) repoNotFound(sess ssh.Session) {

      
        149
        
        -	gitservice.PackError(sess, "Repository not found.")

      
        
        150
        +	gitx.PackError(sess, "Repository not found.")

      
        150
        151
         	sess.Exit(1)

      
        151
        152
         }

      
        152
        153
         

      
        153
        154
         func (s *Server) unauthorized(sess ssh.Session) {

      
        154
        
        -	gitservice.PackError(sess, "You are not authorized to push to this repository.")

      
        
        155
        +	gitx.PackError(sess, "You are not authorized to push to this repository.")

      
        155
        156
         	sess.Exit(1)

      
        156
        157
         }

      
        157
        158
         

      
        158
        159
         func (s *Server) error(sess ssh.Session, err error) {

      
        159
        160
         	slog.Error("error on ssh side", "err", err)

      
        160
        
        -	gitservice.PackError(sess, "Unexpected server error.")

      
        
        161
        +	gitx.PackError(sess, "Unexpected server error.")

      
        161
        162
         	sess.Exit(1)

      
        162
        163
         }

      
        163
        164