From 081d1e499cd60e337995f7f0efb3e49d78b780c0 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Tue, 1 Aug 2023 11:42:16 -0500 Subject: [PATCH] Buf sync: Make sure git default branches is in sync with BSR's (#2328) 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. --- private/buf/bufsync/bufsync.go | 22 +++++++ private/buf/bufsync/syncer.go | 59 ++++++++++++++++--- .../command/alpha/repo/reposync/reposync.go | 18 ++++++ 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/private/buf/bufsync/bufsync.go b/private/buf/bufsync/bufsync.go index 34c2be642b..2a9da8e276 100644 --- a/private/buf/bufsync/bufsync.go +++ b/private/buf/bufsync/bufsync.go @@ -16,6 +16,7 @@ package bufsync import ( "context" + "errors" "fmt" "github.com/bufbuild/buf/private/bufpkg/bufmodule/bufmoduleref" @@ -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. @@ -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 @@ -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. diff --git a/private/buf/bufsync/syncer.go b/private/buf/bufsync/syncer.go index 694ed80d0a..ad77f0eef0 100644 --- a/private/buf/bufsync/syncer.go +++ b/private/buf/bufsync/syncer.go @@ -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 @@ -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) @@ -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, diff --git a/private/buf/cmd/buf/command/alpha/repo/reposync/reposync.go b/private/buf/cmd/buf/command/alpha/repo/reposync/reposync.go index 7801e8f9d7..24aec224b2 100644 --- a/private/buf/cmd/buf/command/alpha/repo/reposync/reposync.go +++ b/private/buf/cmd/buf/command/alpha/repo/reposync/reposync.go @@ -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 @@ -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(®istryv1alpha1.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 }