# Share helpers that were copy-pasted across packages

`4b107a2`·

Soph·2w ago·10 files·+163 added/-228 removed

- closeOnceReadCloser existed verbatim in the bootstrap, incremental, and replicate strategies; move it to gitproto as CloseOnce next to LimitPackReader (all strategies already depend on gitproto) and add the unit test it never had.
- The byte formatter existed three times: syncer.formatBytes and bootstrap.humanBytes were identical, and gitproto.humanizeBytes was a slightly cruder variant (fixed one-decimal output, capped at GB). Keep the precision-tiered implementation as gitproto.HumanBytes and use it everywhere; pack-encode progress now formats like the rest of the progress output.
- runSync and runReplicate carried the same plan-classification switch; extract classifyPlans. Replicate derives its relay subset from the returned push plans.
- auth.Method duplicated gitproto.AuthMethod (identical single-method interfaces); alias it so the contract is declared once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

## Sessions

96d8a7027ab3View transcript

## Changes

10

- internal
  
  - auth
    
    - Mauth.go+6/-7
  
  - gitproto
    
    - Mconvert.go+33
    
    - Mconvert_test.go+57
    
    - Mpush.go+22/-18
  
  - strategy
    
    - bootstrap
      
      - Mbootstrap.go+7/-59
    
    - incremental
      
      - Mincremental.go+2/-30
    
    - replicate
      
      - Mreplicate.go+1/-31
  
  - syncer
    
    - Mprogress.go+5/-25
    
    - Mprogress_test.go-22
    
    - Msyncer.go+30/-36

```
2 unmodified lines

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"os/exec"
	"strings"

transporthttp "github.com/go-git/go-git/v6/plumbing/transport/http"

"entire.io/entire/git-sync/internal/gitproto"
)

const defaultGitUsername = "git"

// Method authorizes outbound HTTP requests for a remote. It is satisfied
// by *transporthttp.BasicAuth and *transporthttp.TokenAuth, whose Authorizer
// methods replaced the Method interface that go-git removed in v6 alpha.2.
type Method interface {
	Authorizer(req *http.Request) error
}
// Method authorizes outbound HTTP requests for a remote. It aliases
// gitproto.AuthMethod so values returned by Resolve flow into gitproto
// connection constructors without conversion.
type Method = gitproto.AuthMethod

// Endpoint holds the authentication-related fields for a remote.
type Endpoint struct {

Minternal/auth/auth.go+6/-7

```
3 unmodified lines

```go
}

// CloseOnce wraps a ReadCloser so repeated Close calls only close the
// underlying reader once. Strategies use it for pack readers that are closed
// both by PushPack and by the caller's retry/error cleanup, so double closes
// do not surface spurious failures. Passing an already-wrapped or nil reader
// returns it unchanged.
func CloseOnce(rc io.ReadCloser) io.ReadCloser {
	if rc == nil {
		return nil
	}
	if _, ok := rc.(*closeOnceReadCloser); ok {
		return rc
	}
	return &closeOnceReadCloser{ReadCloser: rc}
}

type closeOnceReadCloser struct {
	io.ReadCloser

once sync.Once
}

func (c *closeOnceReadCloser) Close() error {
	var err error
	c.once.Do(func() {
		err = c.ReadCloser.Close()
	})
	if err != nil {
		return fmt.Errorf("close pack reader: %w", err)
	}
	return nil
}

// LimitPackReader wraps a ReadCloser with a byte limit. Shared across strategies.
func LimitPackReader(r io.ReadCloser, maxBytes int64) io.ReadCloser {
	if maxBytes <= 0 {
```

// Further code continues here, removed for brevity.
