ref: c91ccc8d139dbf967b73262265712b9ee37cdbf1
parent: 7f0a20840fa3efc51c2a2fc80c1f82e030e91f44
author: Runxi Yu <me@runxiyu.org>
date: Fri Jan 30 17:02:35 EST 2026
protostream: Add a helper package to frame protocol-v2 responses This should take care of sideband-all
--- /dev/null
+++ b/internal/protostream/response.go
@@ -1,0 +1,143 @@
+// Package protostream provides helpers for framing protocol-v2 responses.
+package protostream
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/pktline"
+)
+
+// StreamCode identifies the multiplexed stream in a side-band pkt-line payload.
+type StreamCode byte
+
+const (
+ StreamData StreamCode = 1
+ StreamProgress StreamCode = 2
+ StreamError StreamCode = 3
+)
+
+const (
+ // maxPktLineData is the maximum pkt-line payload size (len excludes header).
+ // See gitprotocol-common(5); the maximum pkt-line length is 65520, including header.
+ maxPktLineData = 65516
+ // maxSidebandData is max pkt-line payload minus 1 byte for the stream code.
+ maxSidebandData = maxPktLineData - 1
+)
+
+// ResponseOptions configures how responses are framed.
+type ResponseOptions struct {+ // NoProgress suppresses progress messages (stream code 2).
+ NoProgress bool
+ // SidebandAll enables side-banding for all non-flush/delim/response-end pkt-lines.
+ SidebandAll bool
+}
+
+// ResponseWriter writes protocol-v2 responses with optional side-band multiplexing.
+type ResponseWriter struct {+ pw *pktline.Writer
+ opts ResponseOptions
+}
+
+// NewResponseWriter returns a ResponseWriter wrapping pw.
+func NewResponseWriter(pw *pktline.Writer, opts ResponseOptions) *ResponseWriter {+ return &ResponseWriter{+ pw: pw,
+ opts: opts,
+ }
+}
+
+// WriteLine writes a pkt-line payload. If SidebandAll is enabled, the payload
+// is sent on StreamData.
+func (rw *ResponseWriter) WriteLine(payload []byte) error {+ if rw.opts.SidebandAll {+ return rw.writeStream(StreamData, payload, false)
+ }
+ return rw.pw.WriteLine(payload)
+}
+
+// Flush writes a flush-pkt.
+func (rw *ResponseWriter) Flush() error {+ return rw.pw.Flush()
+}
+
+// Delim writes a delim-pkt.
+func (rw *ResponseWriter) Delim() error {+ return rw.pw.Delim()
+}
+
+// ResponseEnd writes a response-end pkt.
+func (rw *ResponseWriter) ResponseEnd() error {+ return rw.pw.ResponseEnd()
+}
+
+// PackWriter returns an io.Writer that emits pack data on StreamData.
+func (rw *ResponseWriter) PackWriter() io.Writer {+ return packWriter{rw: rw}+}
+
+// WritePack writes pack data on StreamData.
+func (rw *ResponseWriter) WritePack(p []byte) error {+ if len(p) == 0 {+ return nil
+ }
+ return rw.writeStream(StreamData, p, false)
+}
+
+// WriteProgress writes a progress message on StreamProgress.
+func (rw *ResponseWriter) WriteProgress(p []byte) error {+ if rw.opts.NoProgress || len(p) == 0 {+ return nil
+ }
+ return rw.writeStream(StreamProgress, p, false)
+}
+
+// WriteError writes a fatal error message on StreamError.
+func (rw *ResponseWriter) WriteError(p []byte) error {+ if len(p) == 0 {+ return nil
+ }
+ return rw.writeStream(StreamError, p, false)
+}
+
+// Keepalive writes a side-band progress keepalive ("0005\\2").+func (rw *ResponseWriter) Keepalive() error {+ if rw.opts.NoProgress {+ return nil
+ }
+ return rw.writeStream(StreamProgress, nil, true)
+}
+
+func (rw *ResponseWriter) writeStream(code StreamCode, p []byte, allowEmpty bool) error {+ if !allowEmpty && len(p) == 0 {+ return nil
+ }
+
+ buf := make([]byte, 1+maxSidebandData)
+ for len(p) > 0 || allowEmpty {+ n := len(p)
+ if n > maxSidebandData {+ n = maxSidebandData
+ }
+ buf[0] = byte(code)
+ if n > 0 {+ copy(buf[1:], p[:n])
+ }
+ if err := rw.pw.WriteLine(buf[:1+n]); err != nil {+ return err
+ }
+ p = p[n:]
+ allowEmpty = false
+ }
+ return nil
+}
+
+type packWriter struct {+ rw *ResponseWriter
+}
+
+func (pw packWriter) Write(p []byte) (int, error) {+ if err := pw.rw.WritePack(p); err != nil {+ return 0, err
+ }
+ return len(p), nil
+}
--
⑨