plumbing: packp, refactor AdvRefs and capability packages#1987
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the Git pack-protocol plumbing to make capability handling zero-value safe and protocol-agnostic, and to preserve advertised-ref ordering by moving AdvRefs to a slice-based representation. It also removes several constructors whose primary job was map initialization and replaces the old depth “sum type” with a struct that can represent more protocol combinations.
Changes:
- Move capabilities to
plumbing/protocol/capability, makeListzero-value safe, and makeCapabilityastringalias; update call sites accordingly. - Refactor
packp.AdvRefsto store references as an ordered slice (including peeled refs) and simplify AdvRefs encode/decode. - Replace the
Depthinterface withDepthRequestand add encoder/decoder validation for invalid deepen combinations; update transport server/client code.
Reviewed changes
Copilot reviewed 47 out of 48 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| remote.go | Switch capability import to new plumbing/protocol/capability package. |
| remote_test.go | Update tests for new capability import and AdvRefs zero-value usage. |
| plumbing/transport/url_test.go | Update capability list construction to zero-value-safe List. |
| plumbing/transport/upload_pack.go | Update to zero-value-safe request types and DepthRequest handling. |
| plumbing/transport/upload_pack_test.go | Adjust tests for constructor removals and new capability import. |
| plumbing/transport/serve.go | Update AdvertiseRefs capability setup; migrate AdvRefs reference storage to slice. |
| plumbing/transport/send_pack_test.go | Update capability list construction and imports. |
| plumbing/transport/receive_pack.go | Remove constructors for UpdateRequests/ReportStatus; update capability import. |
| plumbing/transport/push.go | Update SendPack signatures to accept capability List by value; remove constructors. |
| plumbing/transport/pack_stream.go | Store capabilities by value; switch to ResolvedReferences() for remote refs. |
| plumbing/transport/pack_session.go | Update capability import path. |
| plumbing/transport/negotiate.go | Update to new capability list type and DepthRequest. |
| plumbing/transport/negotiate_test.go | Update tests for zero-value-safe capability list. |
| plumbing/transport/http/handshake.go | Update AdvRefs creation, capabilities storage, and resolved refs retrieval. |
| plumbing/transport/http/dumb.go | Update HEAD handling to new AdvRefs API and slice-based refs. |
| plumbing/transport/fetch.go | Update FetchPack signature to accept capability List by value. |
| plumbing/transport/build_update_requests_test.go | Update tests to use new capability list type/value semantics. |
| plumbing/reference.go | Add ReferenceName.IsPeeled() helper for ^{} suffix handling. |
| plumbing/protocol/packp/updreq.go | Make UpdateRequests zero-value safe; capabilities stored by value; remove constructor. |
| plumbing/protocol/packp/updreq_encode.go | Pass capability list by pointer where needed after value-type change. |
| plumbing/protocol/packp/updreq_encode_test.go | Update tests to remove constructor usage and new capability import. |
| plumbing/protocol/packp/updreq_decode.go | Switch to capability.DecodeList for parsing capabilities. |
| plumbing/protocol/packp/updreq_decode_test.go | Update decode tests for zero-value safe UpdateRequests. |
| plumbing/protocol/packp/ulreq.go | Replace Depth interface with DepthRequest; capabilities stored by value; remove constructor. |
| plumbing/protocol/packp/ulreq_encode.go | Encode DepthRequest combinations; add validation and support multiple deepen-not. |
| plumbing/protocol/packp/ulreq_encode_test.go | Update tests for DepthRequest; add tests for invalid deepen combinations. |
| plumbing/protocol/packp/ulreq_decode.go | Decode into DepthRequest; add validation for invalid deepen combinations; switch to DecodeList. |
| plumbing/protocol/packp/ulreq_decode_test.go | Update decode assertions for DepthRequest; add invalid-combination tests. |
| plumbing/protocol/packp/srvresp_test.go | Update (commented) tests to new capability list construction. |
| plumbing/protocol/packp/report_status.go | Remove constructor; document zero-value safety. |
| plumbing/protocol/packp/report_status_test.go | Update tests to instantiate ReportStatus directly. |
| plumbing/protocol/packp/inforefs.go | Change InfoRefs to store references as an ordered slice (incl. peeled). |
| plumbing/protocol/packp/common.go | Remove AdvRefs-specific constants now handled via new helpers/logic. |
| plumbing/protocol/packp/capability/list.go | Remove legacy capability list implementation under packp/capability. |
| plumbing/protocol/packp/capability/list_test.go | Remove tests for deleted legacy capability list. |
| plumbing/protocol/packp/advrefs.go | Migrate AdvRefs to slice-based refs; add head resolution helpers and resolved refs output. |
| plumbing/protocol/packp/advrefs_test.go | Update tests for new AdvRefs APIs (resolved refs/head heuristics). |
| plumbing/protocol/packp/advrefs_encode.go | Simplify AdvRefs encoding; generate peeled output based on ^{} refs. |
| plumbing/protocol/packp/advrefs_encode_test.go | Update encode tests for new AdvRefs representation. |
| plumbing/protocol/packp/advrefs_decode.go | Simplify AdvRefs decoding; decode capabilities via new DecodeList. |
| plumbing/protocol/packp/advrefs_decode_test.go | Update decode tests for slice-based refs and new head accessor. |
| plumbing/protocol/capability/list.go | Introduce new protocol-level capability List and DecodeList. |
| plumbing/protocol/capability/list_test.go | Add tests for new capability list (currently not exercising DecodeList). |
| plumbing/protocol/capability/capability.go | Make Capability a string alias; remove validation tables. |
| plumbing/protocol/capability/capability_test.go | Add tests for DefaultAgent env var behavior. |
| internal/transport/test/upload_pack.go | Update capability import path. |
| internal/transport/test/receive_pack.go | Update capability import path. |
| backend/http_test.go | Update capability import path for expected advertise-refs output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // firstRef returns the reference to use as the first line (HEAD or first available). | ||
| func (a *AdvRefs) firstRef() (string, plumbing.Hash) { | ||
| for _, ref := range a.References { | ||
| if ref.Name().IsPeeled() { | ||
| continue | ||
| } | ||
| if ref.Name() == plumbing.HEAD { | ||
| return ref.Name().String(), ref.Hash() | ||
| } | ||
| } | ||
| sort.Strings(ret) | ||
|
|
||
| return ret | ||
| } | ||
|
|
||
| func encodeFlush(e *advRefsEncoder) encoderStateFn { | ||
| e.err = pktline.WriteFlush(e.w) | ||
| return nil | ||
| for _, ref := range a.References { | ||
| if ref.Name().IsPeeled() { | ||
| continue | ||
| } | ||
| return ref.Name().String(), ref.Hash() | ||
| } | ||
| return "", plumbing.ZeroHash |
There was a problem hiding this comment.
firstRef() selects the first advertised ref based on the current slice order, but Encode sorts refs for output. If HEAD is absent (e.g. receive-pack), this can make the first pkt-line nondeterministic and can also drop the peeled line for that first ref (because the peeled emission only happens in the sorted loop that skips firstName). Consider choosing the first ref from the same sorted set used for encoding, and ensure a peeled ^{} line is emitted immediately after the first ref when applicable.
cad62f8 to
57f02a3
Compare
pjbgf
left a comment
There was a problem hiding this comment.
@aymanbagabas thanks for working on this. Here's some feedback on the refactoring.
| } | ||
|
|
||
| // Add adds a capability, values are optional | ||
| func (l *List) Add(c string, values ...string) { |
There was a problem hiding this comment.
The latest changes dropped some of the validation. The specs are fairly strict around capabilities:
server MUST NOT advertise capabilities it does not understand.
[session-id] ... must fit within a packet-line, and must not contain non-printable or whitespace characters
Let's bring back validation and ensure that we check:
- Argument-less caps
- Caps that require arguments
- Any argument specific validation defined by the spec
- Only known caps are accepted
Adding tests to confirm the above.
There was a problem hiding this comment.
I added a capability.Validate method to validate v0/v1 capabilities, added the missing session-id capability with tests, and updated client-side and server-side capability validation during handshake.
| req.Depth.DeepenNot = append(req.Depth.DeepenNot, string(line)) | ||
| deepenRevList = true |
There was a problem hiding this comment.
Upstream does some validation here for ambiguity. Probably not something we'd at decoding time, but may be worth including as part of this PR.
There was a problem hiding this comment.
IIRC, we still don't fully support deepen-not. We can revisit this later when we add support.
9c2ba12 to
dbbab8e
Compare
dbbab8e to
3c2a32e
Compare
Add internal init() method that creates the map on first write. NewList()
is deprecated; its body now returns &List{}.
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
…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. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
…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. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Move plumbing/protocol/packp/capability to plumbing/protocol/capability to match git protocol structure and prepare for v2. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Remove deprecated NewList() in favor of the zero value. Fix capability ordering to ensure deterministic encoding. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
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. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
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. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
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. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
…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. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
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. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
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. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
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. Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
3c2a32e to
7a9706e
Compare
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
…atching protocol lines Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
…achine Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
…ents Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
7a9706e to
75fdf13
Compare
| ar := s.testDecodeOK(payloads) | ||
| s.Nil(ar.Head) | ||
| _, err := ar.Head() | ||
| s.ErrorIs(plumbing.ErrReferenceNotFound, err) |
There was a problem hiding this comment.
| s.ErrorIs(plumbing.ErrReferenceNotFound, err) | |
| s.ErrorIs(err, plumbing.ErrReferenceNotFound) |
pjbgf
left a comment
There was a problem hiding this comment.
@aymanbagabas thanks for working on this. 🙇
This PR refactors the
plumbing/protocol/packppackage to simplify the AdvRefs encoder/decoder, clean up the capability package, and remove unnecessary constructors.Motivation
AdvRefs loses information
The old
AdvRefsstored references in two maps (ReferencesandPeeled) that lost wire-order information. Most consumers calledMakeReferenceSlice()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
Capabilitywas a defined type requiring conversions likecapability.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()andSet()returned errors that callers universally discarded. Every call site discards the error. The validation maps enforce rules that callers already know. Worse, theknownmap 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 callcapability.NewList(), which existed tomake(map[...]). IfListis zero-value safe, all constructors become unnecessary.Changes
capability.List zero-value safety
List.Add(),Set(), and other methodsListzero-value safe—first write creates the internal mapNewList()—use&List{}or zero valueCapability type alias
type Capability stringtotype Capability = stringAdd/Setinfallible (no error return)Wire format moved out of List
List.Decodewith package-levelDecodeListListprotocol-agnostic: it's now a pure container typeAdvRefs slice migration
References map[string]HashandPeeled map[string]Hashwith singleReferences []*ReferencesliceReferenceName.IsPeeled()helper (consistent withIsTag(),IsBranch())NewAdvRefs(),AddReference(),MakeReferenceSlice(),IsEmpty()AdvRefs encoder/decoder simplification
Encode()method)Decode()method with loop)Constructor removal and zero-value safety
NewUploadRequest,NewUpdateRequests,NewReportStatusappend,Capabilitiesis value typeDepthRequest struct replaces Depth interface
The old
Depthinterface used a sum-type pattern (DepthCommits,DepthSince,DepthReference) that couldn't represent the full git protocol. Specifically:deepen-sincewithdeepen-not(git allows this)deepen-notrefs (git uses an oidset)The new
DepthRequeststruct matches how canonical git C handles it:This matches git's
upload_pack_datastruct 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 withdeepen-sinceordeepen-not"deepen and deepen-since (or deepen-not) cannot be used together"Benchmarks
Before (transport-migrate/base: 3 runs):
After (packp-refactoring: 3 runs):
Improvement:
Migration Guide
packp.NewAdvRefs()&packp.AdvRefs{}ar.References["name"] = hashar.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 validatepasses (0 lint issues)Supersedes: #1984