# gitproto: show pack-encode progress during materialized push

`8bf686d`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·

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 50s`
- `target: encoding pack: 46.7 MB, elapsed 1m10s`
- `target: 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

```go
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,
```
