Skip to content

Commit

Permalink
Buf sync: Make sure git default branches is in sync with BSR's (#2328)
Browse files Browse the repository at this point in the history
When running a sync command, before syncing anything, make sure the git
repository's default branch name matches with the default branch for the
remote BSR repository.
  • Loading branch information
unmultimedio authored Aug 1, 2023
1 parent c1e64ab commit 081d1e4
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 7 deletions.
22 changes: 22 additions & 0 deletions private/buf/bufsync/bufsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package bufsync

import (
"context"
"errors"
"fmt"

"github.com/bufbuild/buf/private/bufpkg/bufmodule/bufmoduleref"
Expand All @@ -25,6 +26,9 @@ import (
"go.uber.org/zap"
)

// ErrModuleDoesNotExist is an error returned when looking for a remote module.
var ErrModuleDoesNotExist = errors.New("BSR module does not exist")

// ErrorHandler handles errors reported by the Syncer. If a non-nil
// error is returned by the handler, sync will abort in a partially-synced
// state.
Expand Down Expand Up @@ -144,6 +148,16 @@ func SyncerWithGitCommitChecker(checker SyncedGitCommitChecker) SyncerOption {
}
}

// SyncerWithModuleDefaultBranchGetter configures a getter for modules' default branch, to contrast
// a BSR repository default branch vs the local git repository branch. If left empty, the syncer
// skips this validation step.
func SyncerWithModuleDefaultBranchGetter(getter ModuleDefaultBranchGetter) SyncerOption {
return func(s *syncer) error {
s.moduleDefaultBranchGetter = getter
return nil
}
}

// SyncFunc is invoked by Syncer to process a sync point. If an error is returned,
// sync will abort.
type SyncFunc func(ctx context.Context, commit ModuleCommit) error
Expand All @@ -166,6 +180,14 @@ type SyncedGitCommitChecker func(
commitHashes map[string]struct{},
) (map[string]struct{}, error)

// ModuleDefaultBranchGetter is invoked before syncing, to make sure all modules that are about to
// be synced have a BSR default branch that matches the local git repo. If the BSR remote module
// does not exist, the implementation should return `ModuleDoesNotExistErr` error.
type ModuleDefaultBranchGetter func(
ctx context.Context,
module bufmoduleref.ModuleIdentity,
) (string, error)

// ModuleCommit is a module at a particular commit.
type ModuleCommit interface {
// Identity is the identity of the module, accounting for any configured override.
Expand Down
59 changes: 52 additions & 7 deletions private/buf/bufsync/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@ import (
"github.com/bufbuild/buf/private/pkg/storage"
"github.com/bufbuild/buf/private/pkg/storage/storagegit"
"github.com/bufbuild/buf/private/pkg/stringutil"
"go.uber.org/multierr"
"go.uber.org/zap"
)

type syncer struct {
logger *zap.Logger
repo git.Repository
storageGitProvider storagegit.Provider
errorHandler ErrorHandler
modulesToSync []Module
syncPointResolver SyncPointResolver
syncedGitCommitChecker SyncedGitCommitChecker
logger *zap.Logger
repo git.Repository
storageGitProvider storagegit.Provider
errorHandler ErrorHandler
modulesToSync []Module
syncPointResolver SyncPointResolver
syncedGitCommitChecker SyncedGitCommitChecker
moduleDefaultBranchGetter ModuleDefaultBranchGetter

// scanned information from the repo on sync start
tagsByCommitHash map[string][]string
Expand Down Expand Up @@ -117,6 +119,9 @@ func (s *syncer) Sync(ctx context.Context, syncFunc SyncFunc) error {
if err := s.scanRepo(); err != nil {
return fmt.Errorf("scan repo: %w", err)
}
if err := s.validateDefaultBranches(ctx); err != nil {
return err
}
allBranchesSyncPoints := make(map[string]map[Module]git.Hash)
for branch := range s.remoteBranches {
syncPoints, err := s.resolveSyncPoints(ctx, branch)
Expand All @@ -143,6 +148,46 @@ func (s *syncer) Sync(ctx context.Context, syncFunc SyncFunc) error {
return nil
}

// validateDefaultBranches checks that all modules to sync, are being synced to BSR repositories
// that have the same default git branch as this repo.
func (s *syncer) validateDefaultBranches(ctx context.Context) error {
expectedDefaultGitBranch := s.repo.BaseBranch()
if s.moduleDefaultBranchGetter == nil {
s.logger.Warn(
"default branch validation skipped for all modules",
zap.String("expected_default_branch", expectedDefaultGitBranch),
)
return nil
}
var validationErr error
for _, module := range s.modulesToSync {
bsrDefaultBranch, err := s.moduleDefaultBranchGetter(ctx, module.RemoteIdentity())
if err != nil {
if errors.Is(err, ErrModuleDoesNotExist) {
s.logger.Warn(
"default branch validation skipped",
zap.String("expected_default_branch", expectedDefaultGitBranch),
zap.String("module", module.RemoteIdentity().IdentityString()),
zap.Error(err),
)
continue
}
validationErr = multierr.Append(validationErr, fmt.Errorf("getting bsr module %q default branch: %w", module.RemoteIdentity().IdentityString(), err))
continue
}
if bsrDefaultBranch != expectedDefaultGitBranch {
validationErr = multierr.Append(
validationErr,
fmt.Errorf(
"remote module %q with default branch %q does not match the git repository's default branch %q, aborting sync",
module.RemoteIdentity().IdentityString(), bsrDefaultBranch, expectedDefaultGitBranch,
),
)
}
}
return validationErr
}

// syncBranch syncs all modules in a branch.
func (s *syncer) syncBranch(
ctx context.Context,
Expand Down
18 changes: 18 additions & 0 deletions private/buf/cmd/buf/command/alpha/repo/reposync/reposync.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ func sync(
syncerOptions := []bufsync.SyncerOption{
bufsync.SyncerWithResumption(syncPointResolver(clientConfig)),
bufsync.SyncerWithGitCommitChecker(syncGitCommitChecker(clientConfig)),
bufsync.SyncerWithModuleDefaultBranchGetter(defaultBranchGetter(clientConfig)),
}
for _, module := range modules {
var moduleIdentityOverride bufmoduleref.ModuleIdentity
Expand Down Expand Up @@ -284,6 +285,23 @@ func syncGitCommitChecker(clientConfig *connectclient.Config) bufsync.SyncedGitC
}
}

func defaultBranchGetter(clientConfig *connectclient.Config) bufsync.ModuleDefaultBranchGetter {
return func(ctx context.Context, module bufmoduleref.ModuleIdentity) (string, error) {
service := connectclient.Make(clientConfig, module.Remote(), registryv1alpha1connect.NewRepositoryServiceClient)
res, err := service.GetRepositoryByFullName(ctx, connect.NewRequest(&registryv1alpha1.GetRepositoryByFullNameRequest{
FullName: module.Owner() + "/" + module.Repository(),
}))
if err != nil {
if connect.CodeOf(err) == connect.CodeNotFound {
// Repo is not created
return "", bufsync.ErrModuleDoesNotExist
}
return "", fmt.Errorf("get repository by full name %q: %w", module.IdentityString(), err)
}
return res.Msg.Repository.DefaultBranch, nil
}
}

type syncErrorHandler struct {
logger *zap.Logger
}
Expand Down

0 comments on commit 081d1e4

Please sign in to comment.