Skip to content

plumbing: protocol/packp, client-side filter capability support #1000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions plumbing/protocol/packp/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package packp

import (
"errors"
"fmt"
"github.com/go-git/go-git/v5/plumbing"
"net/url"
"strings"
)

var ErrUnsupportedObjectFilterType = errors.New("unsupported object filter type")

// Filter values enable the partial clone capability which causes
// the server to omit objects that match the filter.
//
// See [Git's documentation] for more details.
//
// [Git's documentation]: https://github.com/git/git/blob/e02ecfcc534e2021aae29077a958dd11c3897e4c/Documentation/rev-list-options.txt#L948
type Filter string

type BlobLimitPrefix string

const (
BlobLimitPrefixNone BlobLimitPrefix = ""
BlobLimitPrefixKibi BlobLimitPrefix = "k"
BlobLimitPrefixMebi BlobLimitPrefix = "m"
BlobLimitPrefixGibi BlobLimitPrefix = "g"
)

// FilterBlobNone omits all blobs.
func FilterBlobNone() Filter {
return "blob:none"
}

// FilterBlobLimit omits blobs of size at least n bytes (when prefix is
// BlobLimitPrefixNone), n kibibytes (when prefix is BlobLimitPrefixKibi),
// n mebibytes (when prefix is BlobLimitPrefixMebi) or n gibibytes (when
// prefix is BlobLimitPrefixGibi). n can be zero, in which case all blobs
// will be omitted.
func FilterBlobLimit(n uint64, prefix BlobLimitPrefix) Filter {
return Filter(fmt.Sprintf("blob:limit=%d%s", n, prefix))
}

// FilterTreeDepth omits all blobs and trees whose depth from the root tree
// is larger or equal to depth.
func FilterTreeDepth(depth uint64) Filter {
return Filter(fmt.Sprintf("tree:%d", depth))
}

// FilterObjectType omits all objects which are not of the requested type t.
// Supported types are TagObject, CommitObject, TreeObject and BlobObject.
func FilterObjectType(t plumbing.ObjectType) (Filter, error) {
switch t {
case plumbing.TagObject:
fallthrough
case plumbing.CommitObject:
fallthrough
case plumbing.TreeObject:
fallthrough
case plumbing.BlobObject:
return Filter(fmt.Sprintf("object:type=%s", t.String())), nil
default:
return "", fmt.Errorf("%w: %s", ErrUnsupportedObjectFilterType, t.String())
}
}

// FilterCombine combines multiple Filter values together.
func FilterCombine(filters ...Filter) Filter {
var escapedFilters []string

for _, filter := range filters {
escapedFilters = append(escapedFilters, url.QueryEscape(string(filter)))
}

return Filter(fmt.Sprintf("combine:%s", strings.Join(escapedFilters, "+")))
}
58 changes: 58 additions & 0 deletions plumbing/protocol/packp/filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package packp

import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/stretchr/testify/require"
"testing"
)

func TestFilterBlobNone(t *testing.T) {
require.EqualValues(t, "blob:none", FilterBlobNone())
}

func TestFilterBlobLimit(t *testing.T) {
require.EqualValues(t, "blob:limit=0", FilterBlobLimit(0, BlobLimitPrefixNone))
require.EqualValues(t, "blob:limit=1000", FilterBlobLimit(1000, BlobLimitPrefixNone))
require.EqualValues(t, "blob:limit=4k", FilterBlobLimit(4, BlobLimitPrefixKibi))
require.EqualValues(t, "blob:limit=4m", FilterBlobLimit(4, BlobLimitPrefixMebi))
require.EqualValues(t, "blob:limit=4g", FilterBlobLimit(4, BlobLimitPrefixGibi))
}

func TestFilterTreeDepth(t *testing.T) {
require.EqualValues(t, "tree:0", FilterTreeDepth(0))
require.EqualValues(t, "tree:1", FilterTreeDepth(1))
require.EqualValues(t, "tree:2", FilterTreeDepth(2))
}

func TestFilterObjectType(t *testing.T) {
filter, err := FilterObjectType(plumbing.TagObject)
require.NoError(t, err)
require.EqualValues(t, "object:type=tag", filter)

filter, err = FilterObjectType(plumbing.CommitObject)
require.NoError(t, err)
require.EqualValues(t, "object:type=commit", filter)

filter, err = FilterObjectType(plumbing.TreeObject)
require.NoError(t, err)
require.EqualValues(t, "object:type=tree", filter)

filter, err = FilterObjectType(plumbing.BlobObject)
require.NoError(t, err)
require.EqualValues(t, "object:type=blob", filter)

_, err = FilterObjectType(plumbing.InvalidObject)
require.Error(t, err)

_, err = FilterObjectType(plumbing.OFSDeltaObject)
require.Error(t, err)
}

func TestFilterCombine(t *testing.T) {
require.EqualValues(t, "combine:tree%3A2+blob%3Anone",
FilterCombine(
FilterTreeDepth(2),
FilterBlobNone(),
),
)
}
1 change: 1 addition & 0 deletions plumbing/protocol/packp/ulreq.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type UploadRequest struct {
Wants []plumbing.Hash
Shallows []plumbing.Hash
Depth Depth
Filter Filter
}

// Depth values stores the desired depth of the requested packfile: see
Expand Down
11 changes: 11 additions & 0 deletions plumbing/protocol/packp/ulreq_encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ func (e *ulReqEncoder) encodeDepth() stateFn {
return nil
}

return e.encodeFilter
}

func (e *ulReqEncoder) encodeFilter() stateFn {
if filter := e.data.Filter; filter != "" {
if err := e.pe.Encodef("filter %s\n", filter); err != nil {
e.err = fmt.Errorf("encoding filter %s: %s", filter, err)
return nil
}
}

return e.encodeFlush
}

Expand Down
14 changes: 14 additions & 0 deletions plumbing/protocol/packp/ulreq_encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,20 @@ func (s *UlReqEncodeSuite) TestDepthReference(c *C) {
testUlReqEncode(c, ur, expected)
}

func (s *UlReqEncodeSuite) TestFilter(c *C) {
ur := NewUploadRequest()
ur.Wants = append(ur.Wants, plumbing.NewHash("1111111111111111111111111111111111111111"))
ur.Filter = FilterTreeDepth(0)

expected := []string{
"want 1111111111111111111111111111111111111111\n",
"filter tree:0\n",
pktline.FlushString,
}

testUlReqEncode(c, ur, expected)
}

func (s *UlReqEncodeSuite) TestAll(c *C) {
ur := NewUploadRequest()
ur.Wants = append(ur.Wants,
Expand Down