6 files changed,
134 insertions(+),
202 deletions(-)
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/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