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:

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

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,