Skip to content
10 changes: 10 additions & 0 deletions private/buf/buffetch/internal/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ func NewCannotSpecifyCommitOrTagWithRefError() error {
return errors.New(`cannot specify "commit" or "tag" with "ref"`)
}

// NewCannotSpecifyMergeBaseWithOtherGitOptionsError is a fetch error.
func NewCannotSpecifyMergeBaseWithOtherGitOptionsError() error {
return errors.New(`"merge_base" cannot be specified with "branch", "commit", "tag", or "ref"`)
}

// NewMergeBaseOnlyForLocalGitError is a fetch error.
func NewMergeBaseOnlyForLocalGitError() error {
return errors.New(`"merge_base" is only supported for local git references (e.g. ".git#merge_base=main")`)
}

// NewDepthParseError is a fetch error.
func NewDepthParseError(s string) error {
return fmt.Errorf(`could not parse "depth" value %q`, s)
Expand Down
9 changes: 9 additions & 0 deletions private/buf/buffetch/internal/git_ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type gitRef struct {
recurseSubmodules bool
subDirPath string
filter string
gitMergeBase string
}

func newGitRef(
Expand All @@ -53,6 +54,7 @@ func newGitRef(
recurseSubmodules bool,
subDirPath string,
filter string,
gitMergeBase string,
) (*gitRef, error) {
gitScheme, path, err := getGitSchemeAndPath(format, path)
if err != nil {
Expand All @@ -77,6 +79,7 @@ func newGitRef(
depth,
subDirPath,
filter,
gitMergeBase,
), nil
}

Expand All @@ -89,6 +92,7 @@ func newDirectGitRef(
depth uint32,
subDirPath string,
filter string,
gitMergeBase string,
) *gitRef {
return &gitRef{
format: format,
Expand All @@ -99,6 +103,7 @@ func newDirectGitRef(
recurseSubmodules: recurseSubmodules,
subDirPath: subDirPath,
filter: filter,
gitMergeBase: gitMergeBase,
}
}

Expand Down Expand Up @@ -134,6 +139,10 @@ func (r *gitRef) Filter() string {
return r.filter
}

func (r *gitRef) GitMergeBase() string {
return r.gitMergeBase
}

func (*gitRef) ref() {}
func (*gitRef) bucketRef() {}
func (*gitRef) gitRef() {}
Expand Down
15 changes: 14 additions & 1 deletion private/buf/buffetch/internal/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ type GitRef interface {
SubDirPath() string
// Filter spec to use, see the --filter option in git rev-list.
Filter() string
// GitMergeBase returns the ref to compute the merge-base with (via "git merge-base HEAD <ref>").
// Returns empty string if not set.
// Only supported for local git references.
GitMergeBase() string
gitRef()
}

Expand All @@ -221,7 +225,7 @@ func NewGitRef(
subDirPath string,
filter string,
) (GitRef, error) {
return newGitRef("", path, gitName, depth, recurseSubmodules, subDirPath, filter)
return newGitRef("", path, gitName, depth, recurseSubmodules, subDirPath, filter, "")
}

// ModuleRef is a module reference.
Expand Down Expand Up @@ -357,6 +361,7 @@ func NewDirectParsedGitRef(
depth uint32,
subDirPath string,
filter string,
gitMergeBase string,
) ParsedGitRef {
return newDirectGitRef(
format,
Expand All @@ -367,6 +372,7 @@ func NewDirectParsedGitRef(
depth,
subDirPath,
filter,
gitMergeBase,
)
}

Expand Down Expand Up @@ -565,6 +571,13 @@ type RawRef struct {
// Only set for git formats.
// The filter spec to use, see the --filter option in git rev-list.
GitFilter string
// Only set for git formats.
// Specifies a branch or ref to compute the git merge-base with, relative to HEAD.
// When set, buf will run "git merge-base HEAD <GitMergeBase>" and use the resulting
// commit as the checkout target.
// Cannot be specified with GitBranch, GitCommitOrTag, or GitRef.
// Only supported for local git references.
GitMergeBase string
// Only set for archive formats.
ArchiveStripComponents uint32
// Only set for proto file ref format.
Expand Down
26 changes: 25 additions & 1 deletion private/buf/buffetch/internal/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,30 @@ func (r *reader) getGitBucket(
if err != nil {
return nil, nil, err
}
gitName := gitRef.GitName()
if mergeBaseRef := gitRef.GitMergeBase(); mergeBaseRef != "" {
if gitRef.GitScheme() != GitSchemeLocal {
return nil, nil, NewMergeBaseOnlyForLocalGitError()
}
// gitRef.Path() is the normalized path to the git repository (e.g. ".git").
// We need the directory containing it to run "git merge-base".
localPath := normalpath.Unnormalize(gitRef.Path())
absPath, err := filepath.Abs(localPath)
if err != nil {
return nil, nil, fmt.Errorf("could not resolve local git path: %w", err)
}
// Use the parent directory if the path is a .git directory,
// otherwise use the path directly (worktree or bare repo).
gitDir := absPath
if filepath.Base(absPath) == ".git" {
gitDir = filepath.Dir(absPath)
}
mergeBaseCommit, err := git.GetMergeBase(ctx, container, gitDir, mergeBaseRef)
if err != nil {
return nil, nil, err
}
gitName = git.NewRefName(mergeBaseCommit)
}
readWriteBucket := storagemem.NewReadWriteBucket()
if err := r.gitCloner.CloneToBucket(
ctx,
Expand All @@ -371,7 +395,7 @@ func (r *reader) getGitBucket(
gitRef.Depth(),
readWriteBucket,
git.CloneToBucketOptions{
Name: gitRef.GitName(),
Name: gitName,
RecurseSubmodules: gitRef.RecurseSubmodules(),
SubDir: gitRef.SubDirPath(),
Filter: gitRef.Filter(),
Expand Down
12 changes: 9 additions & 3 deletions private/buf/buffetch/internal/ref_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ func (a *refParser) getRawRef(
rawRef.GitCommitOrTag = value
case "ref":
rawRef.GitRef = value
case "merge_base":
rawRef.GitMergeBase = value
case "filter":
rawRef.GitFilter = value
case "depth":
Expand Down Expand Up @@ -191,8 +193,8 @@ func (a *refParser) getRawRef(
if rawRef.Format == "git" && rawRef.GitDepth == 0 {
// Default to 1
rawRef.GitDepth = 1
if rawRef.GitRef != "" {
// Default to 50 when using ref
if rawRef.GitRef != "" || rawRef.GitMergeBase != "" {
// Default to 50 when using ref or merge_base
rawRef.GitDepth = 50
}
}
Expand Down Expand Up @@ -347,8 +349,11 @@ func (a *refParser) validateRawRef(
if rawRef.GitRef != "" && rawRef.GitCommitOrTag != "" {
return NewCannotSpecifyCommitOrTagWithRefError()
}
if rawRef.GitMergeBase != "" && (rawRef.GitBranch != "" || rawRef.GitCommitOrTag != "" || rawRef.GitRef != "") {
return NewCannotSpecifyMergeBaseWithOtherGitOptionsError()
}
} else {
if rawRef.GitBranch != "" || rawRef.GitCommitOrTag != "" || rawRef.GitRef != "" || rawRef.GitRecurseSubmodules || rawRef.GitDepth > 0 {
if rawRef.GitBranch != "" || rawRef.GitCommitOrTag != "" || rawRef.GitRef != "" || rawRef.GitMergeBase != "" || rawRef.GitRecurseSubmodules || rawRef.GitDepth > 0 {
return NewOptionsInvalidForFormatError(rawRef.Format, displayName, "git options set")
}
}
Expand Down Expand Up @@ -522,6 +527,7 @@ func getGitRef(
rawRef.GitRecurseSubmodules,
rawRef.SubDirPath,
rawRef.GitFilter,
rawRef.GitMergeBase,
)
}

Expand Down
38 changes: 38 additions & 0 deletions private/buf/buffetch/internal/ref_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
package internal

import (
"context"
"testing"

"github.com/bufbuild/buf/private/pkg/slogtestext"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetRawPathAndOptionsError(t *testing.T) {
Expand Down Expand Up @@ -85,3 +88,38 @@ func testGetRawPathAndOptionsError(
assert.EqualError(t, err, expectedErr.Error())
})
}

func TestRefParserGitMergeBaseValidation(t *testing.T) {
t.Parallel()
parser := newRefParser(slogtestext.NewLogger(t), WithGitFormat("git"))
ctx := context.Background()

t.Run("valid_merge_base", func(t *testing.T) {
t.Parallel()
parsedRef, err := parser.getParsedRef(ctx, "path/to/repo#format=git,merge_base=main", nil)
require.NoError(t, err)
gitRef, ok := parsedRef.(GitRef)
require.True(t, ok, "expected GitRef")
assert.Equal(t, "main", gitRef.GitMergeBase())
assert.Equal(t, uint32(50), gitRef.Depth())
assert.Nil(t, gitRef.GitName())
})

t.Run("merge_base_with_branch", func(t *testing.T) {
t.Parallel()
_, err := parser.getParsedRef(ctx, "path/to/repo#format=git,merge_base=main,branch=feature", nil)
assert.EqualError(t, err, NewCannotSpecifyMergeBaseWithOtherGitOptionsError().Error())
})

t.Run("merge_base_with_ref", func(t *testing.T) {
t.Parallel()
_, err := parser.getParsedRef(ctx, "path/to/repo#format=git,merge_base=main,ref=abc123", nil)
assert.EqualError(t, err, NewCannotSpecifyMergeBaseWithOtherGitOptionsError().Error())
})

t.Run("merge_base_with_commit", func(t *testing.T) {
t.Parallel()
_, err := parser.getParsedRef(ctx, "path/to/repo#format=git,merge_base=main,commit=abc123", nil)
assert.EqualError(t, err, NewCannotSpecifyMergeBaseWithOtherGitOptionsError().Error())
})
}
Loading
Loading