Skip to content

plumbing: packp, refactor AdvRefs and capability packages#1984

Closed
aymanbagabas wants to merge 13 commits into
go-git:transport-refactoringfrom
aymanbagabas:packp-refactoring
Closed

plumbing: packp, refactor AdvRefs and capability packages#1984
aymanbagabas wants to merge 13 commits into
go-git:transport-refactoringfrom
aymanbagabas:packp-refactoring

Conversation

@aymanbagabas

@aymanbagabas aymanbagabas commented Apr 12, 2026

Copy link
Copy Markdown
Member

This PR refactors the plumbing/protocol/packp package to simplify the AdvRefs encoder/decoder, clean up the capability package, and remove unnecessary constructors.

Motivation

AdvRefs loses information

The old AdvRefs stored references in two maps (References and Peeled) that lost wire-order information. Most consumers called MakeReferenceSlice() immediately—the maps were an intermediate representation nobody wanted. The git pack protocol defines advertised-refs as an ordered sequence where peeled refs follow their base ref. The current representation didn't preserve this.

Capability type forces conversions

Capability was a defined type requiring conversions like capability.Capability("custom-cap"). Protocol v2's arbitrary command capabilities multiply this friction. A type alias eliminates conversions while keeping the named constants.

capability.List validation is counterproductive

List.Add() and Set() returned errors that callers universally discarded. Every call site discards the error. The validation maps enforce rules that callers already know. Worse, the known map becomes a liability for v2: unknown capabilities bypass validation silently.

Constructors exist only to initialize maps

Every packp constructor (NewAdvRefs, NewUploadRequest, etc.) existed primarily to call capability.NewList(), which existed to make(map[...]). If List is zero-value safe, all constructors become unnecessary.

Changes

capability.List zero-value safety

  • Added lazy init to List.Add(), Set(), and other methods
  • Made List zero-value safe—first write creates the internal map
  • Removed NewList()—use &List{} or zero value

Capability type alias

  • Changed type Capability string to type Capability = string
  • Eliminated conversion friction for protocol v2
  • Removed validation maps and error types
  • Made Add/Set infallible (no error return)

Wire format moved out of List

  • Replaced List.Decode with package-level DecodeList
  • v0/v1 uses space-separated capabilities; v2 uses line-separated
  • Keeping List protocol-agnostic: it's now a pure container type

AdvRefs slice migration

  • Replaced References map[string]Hash and Peeled map[string]Hash with single References []*Reference slice
  • Added ReferenceName.IsPeeled() helper (consistent with IsTag(), IsBranch())
  • Removed NewAdvRefs(), AddReference(), MakeReferenceSlice(), IsEmpty()

AdvRefs encoder/decoder simplification

  • Removed state machine pattern from encoder (5 state functions → single Encode() method)
  • Removed state machine pattern from decoder (8 state functions → single Decode() method with loop)
  • Net reduction of 145 lines of code

Constructor removal and zero-value safety

  • Removed NewUploadRequest, NewUpdateRequests, NewReportStatus
  • Types are zero-value safe: nil slices work with append, Capabilities is value type

DepthRequest struct replaces Depth interface

The old Depth interface used a sum-type pattern (DepthCommits, DepthSince, DepthReference) that couldn't represent the full git protocol. Specifically:

  • Couldn't combine deepen-since with deepen-not (git allows this)
  • Couldn't specify multiple deepen-not refs (git uses an oidset)
  • Required a constructor just to set a non-nil default

The new DepthRequest struct matches how canonical git C handles it:

type DepthRequest struct {
    Commits int           // "deepen <n>" - 0 means no limit
    Since   time.Time     // "deepen-since <timestamp>"
    NotRefs []string      // Multiple "deepen-not <ref>" lines
}

This matches git's upload_pack_data struct which stores depth as separate fields, not a sum type. The zero value (all fields empty/zero) means "no depth constraint" which is the correct default for infinite depth.

Added validation: Following canonical git, the encoder and decoder now reject invalid combinations:

  • deepen <n> cannot be combined with deepen-since or deepen-not
  • Error message matches git: "deepen and deepen-since (or deepen-not) cannot be used together"

Benchmarks

Before (transport-migrate/base: 3 runs):

BenchmarkPlainClone-16    3    341770083 ns/op    3742216 B/op    34587 allocs/op
BenchmarkPlainClone-16    3    345509806 ns/op    3472776 B/op    34530 allocs/op
BenchmarkPlainClone-16    4    329814698 ns/op    3562374 B/op    34564 allocs/op

After (packp-refactoring: 3 runs):

BenchmarkPlainClone-16    3    346557236 ns/op    2984754 B/op    29572 allocs/op
BenchmarkPlainClone-16    3    335943014 ns/op    2855368 B/op    29477 allocs/op
BenchmarkPlainClone-16    3    349759278 ns/op    2876133 B/op    29320 allocs/op

Improvement:

  • Memory: ~3.6MB → ~2.9MB (~19% reduction in allocations)
  • Allocs: ~34,500 → ~29,500 (~14% reduction)

Migration Guide

Before After
packp.NewAdvRefs() &packp.AdvRefs{}
ar.References["name"] = hash ar.References = append(ar.References, plumbing.NewHashReference("name", hash))
capability.NewList() &capability.List{}
caps.Decode(data) capability.DecodeList(data, &caps)
depth := DepthCommits(3) depth := DepthRequest{Commits: 3}

Testing

All existing tests pass:

  • go test ./plumbing/protocol/packp/...
  • go test ./plumbing/protocol/capability/...
  • make validate passes (0 lint issues)

Add internal init() method that creates the map on first write. NewList()
is deprecated; its body now returns &List{}.
…dation

type Capability = string eliminates conversion friction for protocol v2.
Remove known/requiresArgument/multipleArgument maps and error types. Add/Set
no longer return errors; update all call sites.
…String

Remove method List.Decode, add package-level DecodeList for v0/v1
space-separated wire format. Keep List.String for encoding. Wire format
decoding now lives in the capability package as a free function rather
than a method, making List a pure container.
Move plumbing/protocol/packp/capability to plumbing/protocol/capability
to match git protocol structure and prepare for v2.
Remove deprecated NewList() in favor of the zero value. Fix capability
ordering to ensure deterministic encoding.
Replace AdvRefs.References (map[string]Hash) and Peeled (map[string]Hash)
with a single References []*plumbing.Reference slice. Remove NewAdvRefs(),
AddReference(), IsEmpty(), and MakeReferenceSlice(). Change Capabilities
from *capability.List to capability.List.

This makes AdvRefs zero-value safe: slices are appendable from nil, and
Capabilities is a value type.

Add IsPeeled(), RefsToMap(), PeeledToMap() helpers to plumbing/reference.go.
DecodeList and IsEmpty get nil-receiver guards.
Remove NewUploadRequest, NewUpdateRequests, and NewReportStatus. Their
types are now zero-value safe: slices are appendable from nil, and
Capabilities is a value type. The encoder guards against nil Depth.
Set Depth to DepthCommits(0) in the decoder so decoded requests always
have a non-nil Depth.
Replace the Depth sum-type interface (DepthCommits, DepthSince,
DepthReference) with a single DepthRequest struct containing Commits,
Since, and NotRefs fields. This matches git upload_pack_data struct,
supports the full protocol (deepen-since + deepen-not combination,
multiple deepen-not refs), and makes the zero value safe.

Update encoder, decoder, and all consumers.
…plumbing

Remove exported IsPeeled, RefsToMap, and PeeledToMap from plumbing/reference.go.
Move isPeeled and peeledToMap as internal functions in the packp package.
Update tests to iterate References directly instead of using helper functions.
Move the peeled check from internal packp function to ReferenceName.IsPeeled(),
consistent with other reference name predicates like IsTag() and IsBranch().
Remove the internal isPeeled function and update all call sites to use
ref.Name().IsPeeled() instead.
Remove state machine pattern from AdvRefs encoder and decoder. The
original implementation had complex state functions (encodeFirstLine,
encodeRefs, encodeShallow, encodeFlush for encoder; decodeFirstHash,
decodeFirstRef, decodeCaps, decodeOtherRefs, decodeShallow for decoder).

New encoder: single Encode method with sequential steps.
New decoder: single Decode method with sequential loop.

Add ReferenceName.IsPeeled helper and use it throughout.
Move isPeeled and peeledToMap as internal functions in packp package.
Remove exported IsPeeled, RefsToMap, PeeledToMap from plumbing.
Prevent combining deepen <n> with deepen-since or deepen-not, following
canonical git's validation in upload-pack.c:
`deepen and deepen-since (or deepen-not) cannot be used together`

Add validation to both encoder and decoder, with tests for all
invalid combinations.
@pjbgf pjbgf deleted the branch go-git:transport-refactoring April 13, 2026 14:29
@pjbgf pjbgf closed this Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants