gitproto: send empty pack on pack-less ref creates · Entire
gitproto: send empty pack on pack-less ref creates
462a28b→main·Soph·1mo ago·2 files·+144 added/-4 removed
PushCommands sent ref create/update commands with no packfile, relying on the server tolerating a pack-less create. Some receive-pack implementations read a pack header for every non-delete command and fail with a truncated-pack error ("read packfile header: EOF") when the request body ends after the commands. This wedged batched bootstraps at the subsumed-branch finalize step.
Attach a valid empty packfile (12-byte header, zero objects, trailing checksum) whenever the request contains a non-delete command. Delete-only pushes stay pack-less, as git requires. The trailer follows the target's advertised object-format capability, so SHA-256 targets get a 32-byte trailer. The two possible empty packs are precomputed once at package load rather than per call.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Changes
internal/gitproto
Mpush.go+49/-4
Mpush_test.go+95
2 unmodified lines
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
33 unmodified lines
55
56
57
55
58
59
60
61
62
510 unmodified lines
573
574
575
572
576
577
578
579
580
581
582
583
584
585
586
587
588
2 unmodified lines
591
592
593
581
594
595
596
597
585
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
2 unmodified lines
import (
"bytes"
"context"
"crypto"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
"sync/atomic"
"time"
"github.com/go-git/go-git/v6/plumbing"
"github.com/go-git/go-git/v6/plumbing/format/packfile"
"github.com/go-git/go-git/v6/plumbing/hash"
"github.com/go-git/go-git/v6/plumbing/protocol/capability"
"github.com/go-git/go-git/v6/plumbing/protocol/packp"
"github.com/go-git/go-git/v6/plumbing/protocol/packp/sideband"
)
return PushPack(ctx, p.Conn, p.Adv, commands, pack, p.Verbose, p.OnRejection)
// PushCommands sends ref-only updates without a pack.
// PushCommands sends ref-only updates. Creates/updates carry an empty pack;
// delete-only pushes carry no pack. See the package-level PushCommands.
func (p *Pusher) PushCommands(ctx context.Context, commands []PushCommand) error {
return PushCommands(ctx, p.Conn, p.Adv, commands, p.Verbose, p.OnRejection)
}
// PushCommands sends ref update commands without a pack (for ref-only changes).
// PushCommands sends ref update commands that move no new objects to the
// target — the referenced objects already exist there.
// A create/update command still carries a valid empty pack (12-byte header,
// zero objects, trailing checksum). Pack-less creates are legal git, but some
// receive-pack implementations read a pack header for every non-delete command
// and fail with a truncated-pack error when the request body ends after the
// commands; an explicit empty pack satisfies them and stays valid for servers
// that tolerate the pack-less form. Delete-only pushes carry no pack, as git
// requires.
func PushCommands(
ctx context.Context,
conn Conn,
adv *packp.AdvRefs,
commands []PushCommand,
verbose bool,
onRejection func(plumbing.ReferenceName, string),
) error {
req, _, _, err := buildUpdateRequest(adv, commands, verbose)
req, _, hasUpdates, err := buildUpdateRequest(adv, commands, verbose)
if err != nil {
return err
}
return sendReceivePack(ctx, conn, req, nil, verbose, onRejection)
var packData io.Reader
if hasUpdates {
packData = bytes.NewReader(emptyPack(adv))
}
return sendReceivePack(ctx, conn, req, packData, verbose, onRejection)
}
// emptyPackHeader is the fixed 12-byte prefix of any packfile with zero
// objects: the "PACK" signature, version 2, and an object count of 0.
var emptyPackHeader = []byte{'P', 'A', 'C', 'K', 0, 0, 0, 2, 0, 0, 0, 0}
// A valid empty pack is emptyPackHeader followed by the trailing checksum over
// it. The bytes depend only on the hash algorithm, so the two possibilities are
// computed once at package load rather than on every PushCommands call.
var (
emptyPackSHA1 = buildEmptyPack(crypto.SHA1)
emptyPackSHA256 = buildEmptyPack(crypto.SHA256)
)
func buildEmptyPack(algo crypto.Hash) []byte {
h := hash.New(algo)
_, _ = h.Write(emptyPackHeader)
return append(slices.Clone(emptyPackHeader), h.Sum(nil)...)
}
// emptyPack returns a valid packfile containing zero objects whose trailing
// checksum matches the target's advertised object format: SHA-256 repositories
// get a 32-byte trailer; everything else uses the 20-byte SHA-1 trailer.
func emptyPack(adv *packp.AdvRefs) []byte {
if vals := adv.Capabilities.Get(capability.ObjectFormat); len(vals) > 0 && vals[0] == "sha256" {
return emptyPackSHA256
}
return emptyPackSHA1
}
func progressWriter(verbose bool, dest io.Writer) io.Writer {