gitproto: show pack-encode progress during materialized push · Entire
gitproto: show pack-encode progress during materialized push
8bf686d→main·
Soph·1mo ago·1 file·+100 added/-2
Spooling the receive-pack body to a temp file (see previous commit) introduced a silent gap between "starting push" and "uploading" that can run into minutes for large repos. Add a transient in-place progress line that updates every 500ms while encoding, finalized with a permanent "encoded pack" line on completion.
The encoder has two phases visible to the caller — delta selection (no writes) and pack write (steady stream). Distinguish them in the output using the 12-byte pack header as the phase boundary:
target: selecting deltas, elapsed 50starget: encoding pack: 46.7 MB, elapsed 1m10starget: encoded pack: 47.3 MB in 1m12s
Without the phase distinction the byte counter would sit near zero through the long selection phase ("encoding pack: 6 KB, elapsed 50s") and look like a hang or measurement bug. Splitting it makes both phases legible.
Writes go through conn.ProgressWriter() with a "target: " prefix, so
they route through the existing sessionStderr → setTransient path that
already handles sideband progress from upload-pack and receive-pack
("Compressing objects: X%\r" etc.). Visually consistent with what
users already see during fetch and push.
Off in non-verbose mode (progressSink returns nil and the progress goroutine never starts), so quiet runs stay quiet.
Sessions
fc719cc95e77View transcript
Changes
1
internal/gitproto
Mpush.go+100/-2
7 unmodified lines
// code snippets...
return postReceivePack(ctx, conn, req, spooled, verbose, onRejection)
}
// countingWriter wraps an io.Writer and tracks total bytes written.
// Reads of the count are safe to call concurrently with Write.
type countingWriter struct {
w io.Writer
n atomic.Int64
}
func (cw *countingWriter) Write(p []byte) (int, error) {
n, err := cw.w.Write(p)
cw.n.Add(int64(n))
return n, err
}
func (cw *countingWriter) Count() int64 { return cw.n.Load() }
// startPackEncodeProgress emits in-place progress updates while
// materialized push is spooling its body. The output distinguishes
// two phases of go-git's encoder:
//
// - "selecting deltas, elapsed X" while the delta selector walks
// the object graph (no bytes flow during this phase)
// - "encoding pack: N MB, elapsed X" once the selector finishes and
// the encoder starts writing pack bytes
//
// Phase detection uses the 12-byte pack header as the boundary: any
// post-baseline write beyond that means delta selection is done.
// baseline is the byte count at the start of encoding (typically the
// size of the update-request bytes already written to the same writer).
//
// Returns a stop function that finalizes the line with a permanent
// "encoded pack" summary; safe to call exactly once, typically via
// defer. When dest is nil (non-verbose mode) returns a no-op stop, so
// callers don't need to special-case verbosity.
func startPackEncodeProgress(cw *countingWriter, baseline int64, dest io.Writer) func() {
if dest == nil {
return func() {}
}
//...
}
// humanizeBytes renders n in IEC units with one decimal place for KB+
// (e.g. "47.3 MB"). Anything below 1 KB is shown as raw bytes.
func humanizeBytes(n int64) string {
//...
}
// PushPack pushes a pack stream (relay) to the target.
func PushPack(
ctx context.Context,