diff --git a/catalog/action.go b/catalog/action.go new file mode 100644 index 00000000000..81879edddfe --- /dev/null +++ b/catalog/action.go @@ -0,0 +1,49 @@ +package catalog + +import ( + "fmt" + "regexp" +) + +type Action struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + On struct { + PreMerge *ActionOn `yaml:"pre-merge"` + PreCommit *ActionOn `yaml:"pre-commit"` + } `yaml:"on"` + Hooks []ActionHook `yaml:"hooks"` +} + +type ActionOn struct { + Branches []string `yaml:"branches"` +} + +type ActionHook struct { + ID string `yaml:"id"` + Type string `yaml:"type"` + Description string `yaml:"description"` + Properties map[string]string `yaml:"properties"` +} + +var reHookID = regexp.MustCompile(`^[_a-zA-Z][\-_a-zA-Z0-9]{1,255}$`) + +func (a *Action) Validate() error { + if a.On.PreMerge == nil && a.On.PreCommit == nil { + return fmt.Errorf("%w 'on' is required", ErrInvalidAction) + } + ids := make(map[string]struct{}) + for i, hook := range a.Hooks { + if !reHookID.MatchString(hook.ID) { + return fmt.Errorf("hook[%d] missing ID: %w", i, ErrInvalidAction) + } + if _, found := ids[hook.ID]; found { + return fmt.Errorf("hook[%d] duplicate ID '%s': %w", i, hook.ID, ErrInvalidAction) + } + ids[hook.ID] = struct{}{} + if hook.Type != "webhook" { + return fmt.Errorf("hook[%d] '%s' unknown type: %w", i, hook.ID, ErrInvalidAction) + } + } + return nil +} diff --git a/catalog/action_test.go b/catalog/action_test.go new file mode 100644 index 00000000000..2dd0084e375 --- /dev/null +++ b/catalog/action_test.go @@ -0,0 +1,41 @@ +package catalog + +import ( + "errors" + "io/ioutil" + "path" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestAction_Validate(t *testing.T) { + tests := []struct { + name string + filename string + wantErr error + }{ + {name: "full", filename: "action_full.yaml", wantErr: nil}, + {name: "required", filename: "action_required.yaml", wantErr: nil}, + {name: "duplicate id", filename: "action_duplicate_id.yaml", wantErr: ErrInvalidAction}, + {name: "invalid id", filename: "action_invalid_id.yaml", wantErr: ErrInvalidAction}, + {name: "invalid hook type", filename: "action_invalid_type.yaml", wantErr: ErrInvalidAction}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actionData, err := ioutil.ReadFile(path.Join("testdata", tt.filename)) + if err != nil { + t.Fatalf("Failed to load testdata %s, err=%s", tt.filename, err) + } + var act Action + err = yaml.Unmarshal(actionData, &act) + if err != nil { + t.Fatalf("Unmarshal action err=%s", err) + } + err = act.Validate() + if !errors.Is(err, tt.wantErr) { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/catalog/cataloger.go b/catalog/cataloger.go index c442d174cae..0c944208f52 100644 --- a/catalog/cataloger.go +++ b/catalog/cataloger.go @@ -96,8 +96,6 @@ type Cataloger interface { Merge(ctx context.Context, repository, destinationBranch, sourceRef, committer, message string, metadata Metadata) (*MergeResult, error) - Hooks() *CatalogerHooks - // dump/load metadata DumpCommits(ctx context.Context, repositoryID string) (string, error) DumpBranches(ctx context.Context, repositoryID string) (string, error) @@ -105,26 +103,3 @@ type Cataloger interface { io.Closer } - -type PostCommitFunc func(ctx context.Context, repo, branch string, commitLog CommitLog) error -type PostMergeFunc func(ctx context.Context, repo, branch string, mergeResult MergeResult) error - -// CatalogerHooks describes the hooks available for some operations on the catalog. Hooks are -// called after the transaction ends; if they return an error they do not affect commit/merge. -type CatalogerHooks struct { - // PostCommit hooks are called at the end of a commit. - PostCommit []PostCommitFunc - - // PostMerge hooks are called at the end of a merge. - PostMerge []PostMergeFunc -} - -func (h *CatalogerHooks) AddPostCommit(f PostCommitFunc) *CatalogerHooks { - h.PostCommit = append(h.PostCommit, f) - return h -} - -func (h *CatalogerHooks) AddPostMerge(f PostMergeFunc) *CatalogerHooks { - h.PostMerge = append(h.PostMerge, f) - return h -} diff --git a/catalog/entry_catalog.go b/catalog/entry_catalog.go index c1e2904b193..d69601e6a70 100644 --- a/catalog/entry_catalog.go +++ b/catalog/entry_catalog.go @@ -128,9 +128,11 @@ func NewEntryCatalog(cfg *config.Config, db db.Database) (*EntryCatalog, error) stagingManager := staging.NewManager(db) refManager := ref.NewPGRefManager(db, ident.NewHexAddressProvider()) branchLocker := ref.NewBranchLocker(db) - return &EntryCatalog{ - Store: graveler.NewGraveler(branchLocker, committedManager, stagingManager, refManager), - }, nil + store := graveler.NewGraveler(branchLocker, committedManager, stagingManager, refManager) + entryCatalog := &EntryCatalog{Store: store} + store.SetPreCommitHook(entryCatalog.preCommitHook) + store.SetPreMergeHook(entryCatalog.preMergeHook) + return entryCatalog, nil } func (e *EntryCatalog) AddCommitToBranchHead(ctx context.Context, repositoryID graveler.RepositoryID, branchID graveler.BranchID, commit graveler.Commit) (graveler.CommitID, error) { @@ -501,3 +503,11 @@ func (e *EntryCatalog) DumpBranches(ctx context.Context, repositoryID graveler.R func (e *EntryCatalog) DumpTags(ctx context.Context, repositoryID graveler.RepositoryID) (*graveler.MetaRangeID, error) { return e.Store.DumpTags(ctx, repositoryID) } + +func (e *EntryCatalog) preCommitHook(ctx context.Context, repositoryID graveler.RepositoryID, branchID graveler.BranchID, commit graveler.Commit) error { + return nil +} + +func (e *EntryCatalog) preMergeHook(ctx context.Context, repositoryID graveler.RepositoryID, destination graveler.BranchID, source graveler.Ref, commit graveler.Commit) error { + return nil +} diff --git a/catalog/errors.go b/catalog/errors.go index 3324ffb2736..2f98ae45401 100644 --- a/catalog/errors.go +++ b/catalog/errors.go @@ -8,29 +8,14 @@ import ( ) var ( - ErrInvalidReference = errors.New("invalid reference") - ErrInvalidMetadataSrcFormat = errors.New("invalid metadata src format") - ErrExpired = errors.New("expired from storage") - ErrByteSliceTypeAssertion = errors.New("type assertion to []byte failed") - ErrFeatureNotSupported = errors.New("feature not supported") - ErrBranchNotFound = fmt.Errorf("branch %w", db.ErrNotFound) - ErrCommitNotFound = fmt.Errorf("commit %w", db.ErrNotFound) - ErrRepositoryNotFound = fmt.Errorf("repository %w", db.ErrNotFound) - ErrEntryNotFound = fmt.Errorf("entry %w", db.ErrNotFound) - ErrUnexpected = errors.New("unexpected error") - ErrReadEntryTimeout = errors.New("read entry timeout") - ErrInvalidValue = errors.New("invalid value") - ErrNonDirectNotSupported = errors.New("non direct diff not supported") - ErrSameBranchMergeNotSupported = errors.New("same branch merge not supported") - ErrLineageCorrupted = errors.New("lineage corrupted") - ErrOperationNotPermitted = errors.New("operation not permitted") - ErrNothingToCommit = errors.New("nothing to commit") - ErrInvalidLockValue = errors.New("invalid lock value") - ErrNoDifferenceWasFound = errors.New("no difference was found") - ErrConflictFound = errors.New("conflict found") - ErrUnsupportedRelation = errors.New("unsupported relation") - ErrUnsupportedDelimiter = errors.New("unsupported delimiter") - ErrBadTypeConversion = errors.New("bad type") - ErrExportFailed = errors.New("export failed") - ErrRollbackWithActiveBranch = fmt.Errorf("%w: rollback with active branch", ErrFeatureNotSupported) + ErrInvalidMetadataSrcFormat = errors.New("invalid metadata src format") + ErrExpired = errors.New("expired from storage") + ErrFeatureNotSupported = errors.New("feature not supported") + ErrBranchNotFound = fmt.Errorf("branch %w", db.ErrNotFound) + ErrRepositoryNotFound = fmt.Errorf("repository %w", db.ErrNotFound) + ErrInvalidValue = errors.New("invalid value") + ErrNoDifferenceWasFound = errors.New("no difference was found") + ErrConflictFound = errors.New("conflict found") + ErrUnsupportedRelation = errors.New("unsupported relation") + ErrInvalidAction = errors.New("invalid action") ) diff --git a/catalog/fake_graveler_test.go b/catalog/fake_graveler_test.go index 9615be74486..c2580131648 100644 --- a/catalog/fake_graveler_test.go +++ b/catalog/fake_graveler_test.go @@ -16,6 +16,8 @@ type FakeGraveler struct { RepositoryIteratorFactory func() graveler.RepositoryIterator BranchIteratorFactory func() graveler.BranchIterator TagIteratorFactory func() graveler.TagIterator + preCommitHook graveler.PreCommitFunc + preMergeHook graveler.PreMergeFunc } func (g *FakeGraveler) DumpCommits(ctx context.Context, repositoryID graveler.RepositoryID) (*graveler.MetaRangeID, error) { @@ -200,6 +202,22 @@ func (g *FakeGraveler) Compare(_ context.Context, _ graveler.RepositoryID, _, _ return g.DiffIteratorFactory(), nil } +func (g *FakeGraveler) PreCommitHook() graveler.PreCommitFunc { + return g.preCommitHook +} + +func (g *FakeGraveler) SetPreCommitHook(fn graveler.PreCommitFunc) { + g.preCommitHook = fn +} + +func (g *FakeGraveler) PreMergeHook() graveler.PreMergeFunc { + return g.preMergeHook +} + +func (g *FakeGraveler) SetPreMergeHook(fn graveler.PreMergeFunc) { + g.preMergeHook = fn +} + func (g *FakeGraveler) AddCommitToBranchHead(ctx context.Context, repositoryID graveler.RepositoryID, branchID graveler.BranchID, commit graveler.Commit) (graveler.CommitID, error) { panic("implement me") } diff --git a/catalog/rocks_cataloger.go b/catalog/rocks_cataloger.go index 76eb267900f..8c448046da1 100644 --- a/catalog/rocks_cataloger.go +++ b/catalog/rocks_cataloger.go @@ -16,7 +16,6 @@ import ( type cataloger struct { EntryCatalog *EntryCatalog log logging.Logger - hooks CatalogerHooks } const ( @@ -37,7 +36,6 @@ func NewCataloger(db db.Database, cfg *config.Config) (Cataloger, error) { return &cataloger{ EntryCatalog: entryCatalog, log: logging.Default(), - hooks: CatalogerHooks{}, }, nil } @@ -631,10 +629,6 @@ func (c *cataloger) Merge(ctx context.Context, repository string, destinationBra }, nil } -func (c *cataloger) Hooks() *CatalogerHooks { - return &c.hooks -} - func (c *cataloger) DumpCommits(ctx context.Context, repositoryID string) (string, error) { metaRangeID, err := c.EntryCatalog.DumpCommits(ctx, graveler.RepositoryID(repositoryID)) if err != nil { diff --git a/catalog/testdata/action_duplicate_id.yaml b/catalog/testdata/action_duplicate_id.yaml new file mode 100644 index 00000000000..e46e8561be1 --- /dev/null +++ b/catalog/testdata/action_duplicate_id.yaml @@ -0,0 +1,13 @@ +on: + pre-merge: + branches: + - master +hooks: + - id: hook1 + type: webhook + properties: + url: "https://api.lakefs.io/webhook1?t=1za2PbkZK1bd4prMuTDr6BeEQwWYcX2R" + - id: hook1 + type: webhook + properties: + url: "https://api.lakefs.io/webhook1?t=1za2PbkZK1bd4prMuTDr6BeEQwWYcX2R" \ No newline at end of file diff --git a/catalog/testdata/action_full.yaml b/catalog/testdata/action_full.yaml new file mode 100644 index 00000000000..ddf63242d1c --- /dev/null +++ b/catalog/testdata/action_full.yaml @@ -0,0 +1,21 @@ +name: Good merge +description: set of checks to verify that branch is good +on: + pre-merge: + branches: + - master + - stage + pre-commit: + branches: + - feature-* +hooks: + - id: no_temp + type: webhook + description: checking no temporary files found + properties: + url: "https://api.lakefs.io/webhook1?t=1za2PbkZK1bd4prMuTDr6BeEQwWYcX2R" + - id: no_freeze + type: webhook + description: check production is not in dev freeze + properties: + url: "https://api.lakefs.io/webhook2?t=1za2PbkZK1bd4prMuTDr6BeEQwWYcX2R" \ No newline at end of file diff --git a/catalog/testdata/action_invalid_id.yaml b/catalog/testdata/action_invalid_id.yaml new file mode 100644 index 00000000000..b647ebc7197 --- /dev/null +++ b/catalog/testdata/action_invalid_id.yaml @@ -0,0 +1,9 @@ +on: + pre-merge: + branches: + - master +hooks: + - id: not valid to use space + type: webhook + properties: + url: "https://api.lakefs.io/webhook1?t=1za2PbkZK1bd4prMuTDr6BeEQwWYcX2R" \ No newline at end of file diff --git a/catalog/testdata/action_invalid_type.yaml b/catalog/testdata/action_invalid_type.yaml new file mode 100644 index 00000000000..7a0a8afea0f --- /dev/null +++ b/catalog/testdata/action_invalid_type.yaml @@ -0,0 +1,7 @@ +on: + pre-merge: + branches: + - master +hooks: + - id: no_temp + type: command \ No newline at end of file diff --git a/catalog/testdata/action_required.yaml b/catalog/testdata/action_required.yaml new file mode 100644 index 00000000000..b24f629941a --- /dev/null +++ b/catalog/testdata/action_required.yaml @@ -0,0 +1,9 @@ +on: + pre-merge: + branches: + - master +hooks: + - id: no_temp + type: webhook + properties: + url: "https://api.lakefs.io/webhook1?t=1za2PbkZK1bd4prMuTDr6BeEQwWYcX2R" \ No newline at end of file diff --git a/db/errors.go b/db/errors.go index 6a8881840ff..ff72246280d 100644 --- a/db/errors.go +++ b/db/errors.go @@ -11,5 +11,4 @@ var ( ErrNotFound = fmt.Errorf("not found: %w", pgx.ErrNoRows) ErrAlreadyExists = errors.New("already exists") ErrSerialization = errors.New("serialization error") - ErrNotASlice = errors.New("results must be a pointer to a slice") ) diff --git a/go.mod b/go.mod index ae6db597190..c5f6e27a400 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,6 @@ require ( github.com/jackc/pgconn v1.8.0 github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 github.com/jackc/pgproto3/v2 v2.0.6 - github.com/jackc/pgtype v1.6.2 // indirect github.com/jackc/pgx/v4 v4.10.1 github.com/jamiealquiza/tachymeter v2.0.0+incompatible github.com/jedib0t/go-pretty v4.3.0+incompatible @@ -89,5 +88,6 @@ require ( google.golang.org/api v0.36.0 google.golang.org/protobuf v1.25.0 gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 pgregory.net/rapid v0.4.0 // indirect ) diff --git a/graveler/errors.go b/graveler/errors.go index 9072f95e639..7044d26e3e8 100644 --- a/graveler/errors.go +++ b/graveler/errors.go @@ -43,6 +43,7 @@ var ( ErrAddCommitNoParent = errors.New("added commit must have a parent") ErrMultipleParents = errors.New("cannot have more than a single parent") ErrRevertParentOutOfRange = errors.New("given commit does not have the given parent number") + ErrAbortedByHook = errors.New("aborted by hook") ) // wrappedError is an error for wrapping another error while ignoring its message. diff --git a/graveler/graveler.go b/graveler/graveler.go index 3c3901f687e..2238b17f99c 100644 --- a/graveler/graveler.go +++ b/graveler/graveler.go @@ -7,12 +7,12 @@ import ( "strings" "time" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/timestamppb" - "github.com/google/uuid" + "github.com/hashicorp/go-multierror" "github.com/treeverse/lakefs/ident" "github.com/treeverse/lakefs/logging" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) // Basic Types @@ -207,7 +207,9 @@ type CommitParams struct { Metadata Metadata } -// Interfaces +type PreCommitFunc func(ctx context.Context, repositoryID RepositoryID, branch BranchID, commit Commit) error +type PreMergeFunc func(ctx context.Context, repositoryID RepositoryID, destination BranchID, source Ref, commit Commit) error + type KeyValueStore interface { // Get returns value from repository / reference by key, nil value is a valid value for tombstone // returns error if value does not exist @@ -314,6 +316,18 @@ type VersionController interface { // Compare returns the difference between the commit where 'to' was last synced into 'from', and the most recent commit of `from`. // This is similar to a three-dot (from...to) diff in git. Compare(ctx context.Context, repositoryID RepositoryID, from, to Ref) (DiffIterator, error) + + // PreCommitHook get current pre-commit hook function + PreCommitHook() PreCommitFunc + + // SetPreCommitHook set pre-commit hook function + SetPreCommitHook(fn PreCommitFunc) + + // PreMergeHook get pre-merge hook function + PreMergeHook() PreMergeFunc + + // SetPreMergeHook set pre-merge hook function + SetPreMergeHook(fn PreMergeFunc) } type Dumper interface { @@ -570,6 +584,8 @@ type Graveler struct { StagingManager StagingManager RefManager RefManager branchLocker BranchLocker + preCommitFn PreCommitFunc + preMergeFn PreMergeFunc log logging.Logger } @@ -892,6 +908,23 @@ func (g *Graveler) Commit(ctx context.Context, repositoryID RepositoryID, branch if err != nil { return "", fmt.Errorf("get branch: %w", err) } + + // fill commit information - use for pre-commit and after adding the commit information used by commit + commit := Commit{ + Committer: params.Committer, + Message: params.Message, + CreationDate: time.Now(), + Metadata: params.Metadata, + } + if branch.CommitID != "" { + commit.Parents = CommitParents{branch.CommitID} + } + + err = g.callPreCommitHooks(ctx, repositoryID, branchID, commit) + if err != nil { + return "", fmt.Errorf("pre-commit hooks: %w", err) + } + var branchMetaRangeID MetaRangeID if branch.CommitID != "" { commit, err := g.RefManager.GetCommit(ctx, repositoryID, branch.CommitID) @@ -900,28 +933,18 @@ func (g *Graveler) Commit(ctx context.Context, repositoryID RepositoryID, branch } branchMetaRangeID = commit.MetaRangeID } - changes, err := g.StagingManager.List(ctx, branch.StagingToken) if err != nil { return "", fmt.Errorf("staging list: %w", err) } - metaRangeID, _, err := g.CommittedManager.Apply(ctx, repo.StorageNamespace, branchMetaRangeID, changes) + defer changes.Close() + + commit.MetaRangeID, _, err = g.CommittedManager.Apply(ctx, repo.StorageNamespace, branchMetaRangeID, changes) if err != nil { return "", fmt.Errorf("commit: %w", err) } - // fill and add commit - commit := Commit{ - Committer: params.Committer, - Message: params.Message, - MetaRangeID: metaRangeID, - CreationDate: time.Now(), - Metadata: params.Metadata, - } - if branch.CommitID != "" { - commit.Parents = CommitParents{branch.CommitID} - } - + // add commit newCommit, err := g.RefManager.AddCommit(ctx, repositoryID, commit) if err != nil { return "", fmt.Errorf("add commit: %w", err) @@ -1243,11 +1266,15 @@ func (g *Graveler) Merge(ctx context.Context, repositoryID RepositoryID, destina commit := Commit{ Committer: commitParams.Committer, Message: commitParams.Message, - MetaRangeID: metaRangeID, CreationDate: time.Now(), + MetaRangeID: metaRangeID, Parents: []CommitID{fromCommit.CommitID, toCommit.CommitID}, Metadata: commitParams.Metadata, } + err = g.callPreMergeHook(ctx, repositoryID, destination, fromCommit.CommitID.Ref(), commit) + if err != nil { + return "", err + } commitID, err := g.RefManager.AddCommit(ctx, repositoryID, commit) if err != nil { return "", fmt.Errorf("add commit: %w", err) @@ -1335,6 +1362,22 @@ func (g *Graveler) Compare(ctx context.Context, repositoryID RepositoryID, from, return g.CommittedManager.Compare(ctx, repo.StorageNamespace, toCommit.MetaRangeID, fromCommit.MetaRangeID, baseCommit.MetaRangeID) } +func (g *Graveler) PreCommitHook() PreCommitFunc { + return g.preCommitFn +} + +func (g *Graveler) SetPreCommitHook(fn PreCommitFunc) { + g.preCommitFn = fn +} + +func (g *Graveler) PreMergeHook() PreMergeFunc { + return g.preMergeFn +} + +func (g *Graveler) SetPreMergeHook(fn PreMergeFunc) { + g.preMergeFn = fn +} + func (g *Graveler) getCommitsForMerge(ctx context.Context, repositoryID RepositoryID, from Ref, to Ref) (*CommitRecord, *CommitRecord, *Commit, error) { fromCommit, err := g.getCommitRecordFromRef(ctx, repositoryID, from) if err != nil { @@ -1619,3 +1662,46 @@ func (c *commitValueIterator) Err() error { func (c *commitValueIterator) Close() { c.src.Close() } + +func newErrAbortedByHook(err error) error { + if err == nil { + return ErrAbortedByHook + } + merr := multierror.Append(ErrAbortedByHook, err) + merr.ErrorFormat = func(errs []error) string { + const minErrorLen = 2 + if len(errs) < minErrorLen { + return multierror.ListFormatFunc(errs) + } + var details string + if len(errs) == minErrorLen { + details = errs[1].Error() + } else { + details = multierror.ListFormatFunc(errs[1:]) + } + return errs[0].Error() + ": " + details + } + return merr +} + +func (g *Graveler) callPreCommitHooks(ctx context.Context, repositoryID RepositoryID, branchID BranchID, commit Commit) error { + if g.preCommitFn == nil { + return nil + } + err := g.preCommitFn(ctx, repositoryID, branchID, commit) + if err != nil { + return newErrAbortedByHook(err) + } + return nil +} + +func (g *Graveler) callPreMergeHook(ctx context.Context, repositoryID RepositoryID, destination BranchID, source Ref, commit Commit) error { + if g.preMergeFn == nil { + return nil + } + err := g.preMergeFn(ctx, repositoryID, destination, source, commit) + if err != nil { + return newErrAbortedByHook(err) + } + return nil +} diff --git a/graveler/graveler_test.go b/graveler/graveler_test.go index bf35e01e59f..5721f47596c 100644 --- a/graveler/graveler_test.go +++ b/graveler/graveler_test.go @@ -457,6 +457,205 @@ func TestGraveler_Commit(t *testing.T) { }) } } +func TestGraveler_PreCommitHook(t *testing.T) { + // prepare graveler + conn, _ := tu.GetDB(t, databaseURI) + branchLocker := ref.NewBranchLocker(conn) + const expectedRangeID = graveler.MetaRangeID("expectedRangeID") + const expectedCommitID = graveler.CommitID("expectedCommitId") + committedManager := &testutil.CommittedFake{MetaRangeID: expectedRangeID} + stagingManager := &testutil.StagingFake{ValueIterator: testutil.NewValueIteratorFake(nil)} + refManager := &testutil.RefsFake{ + CommitID: expectedCommitID, + Branch: &graveler.Branch{CommitID: expectedCommitID}, + Commits: map[graveler.CommitID]*graveler.Commit{expectedCommitID: {MetaRangeID: expectedRangeID}}, + } + // tests + errSomethingBad := errors.New("something bad") + const commitRepositoryID = "repoID" + const commitBranchID = "branchID" + const commitCommitter = "committer" + const commitMessage = "message" + commitMetadata := graveler.Metadata{"key1": "val1"} + tests := []struct { + name string + hook bool + err error + }{ + { + name: "without hook", + hook: false, + err: nil, + }, + { + name: "hook no error", + hook: true, + err: nil, + }, + { + name: "hook error", + hook: true, + err: errSomethingBad, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // setup + ctx := context.Background() + g := graveler.NewGraveler(branchLocker, committedManager, stagingManager, refManager) + var ( + called bool + hookRepositoryID graveler.RepositoryID + hookBranch graveler.BranchID + hookCommit graveler.Commit + ) + if tt.hook { + hookErr := tt.err + g.SetPreCommitHook(func(ctx context.Context, repositoryID graveler.RepositoryID, branch graveler.BranchID, commit graveler.Commit) error { + called = true + hookRepositoryID = repositoryID + hookBranch = branch + hookCommit = commit + return hookErr + }) + } + // call commit + _, err := g.Commit(ctx, commitRepositoryID, commitBranchID, graveler.CommitParams{ + Committer: commitCommitter, + Message: commitMessage, + Metadata: commitMetadata, + }) + // check err composition + if !errors.Is(err, tt.err) { + t.Fatalf("Commit err=%v, expected=%v", err, tt.err) + } + if err != nil && !errors.Is(err, graveler.ErrAbortedByHook) { + t.Fatalf("Commit err=%v, expected ErrAbortedByHook", err) + } + if tt.hook != called { + t.Fatalf("Commit invalid pre-hook call, %t expected=%t", called, tt.hook) + } + if !called { + return + } + if hookRepositoryID != commitRepositoryID { + t.Errorf("Hook repository '%s', expected '%s'", hookRepositoryID, commitRepositoryID) + } + if hookBranch != commitBranchID { + t.Errorf("Hook branch '%s', expected '%s'", hookBranch, commitBranchID) + } + if hookCommit.Message != commitMessage { + t.Errorf("Hook commit message '%s', expected '%s'", hookCommit.Message, commitMessage) + } + if diff := deep.Equal(hookCommit.Metadata, commitMetadata); diff != nil { + t.Error("Hook commit metadata diff:", diff) + } + }) + } +} + +func TestGraveler_PreMergeHook(t *testing.T) { + // prepare graveler + conn, _ := tu.GetDB(t, databaseURI) + branchLocker := ref.NewBranchLocker(conn) + const expectedRangeID = graveler.MetaRangeID("expectedRangeID") + const expectedCommitID = graveler.CommitID("expectedCommitId") + committedManager := &testutil.CommittedFake{MetaRangeID: expectedRangeID} + stagingManager := &testutil.StagingFake{ValueIterator: testutil.NewValueIteratorFake(nil)} + refManager := &testutil.RefsFake{ + CommitID: expectedCommitID, + Branch: &graveler.Branch{CommitID: expectedCommitID}, + Commits: map[graveler.CommitID]*graveler.Commit{expectedCommitID: {MetaRangeID: expectedRangeID}}, + } + // tests + errSomethingBad := errors.New("first error") + const mergeRepositoryID = "repoID" + const mergeDestination = "destinationID" + const commitCommitter = "committer" + const mergeMessage = "message" + mergeMetadata := graveler.Metadata{"key1": "val1"} + tests := []struct { + name string + hook bool + err error + }{ + { + name: "without hook", + hook: false, + err: nil, + }, + { + name: "hook no error", + hook: true, + err: nil, + }, + { + name: "hook error", + hook: true, + err: errSomethingBad, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // setup + ctx := context.Background() + g := graveler.NewGraveler(branchLocker, committedManager, stagingManager, refManager) + var ( + called bool + hookRepositoryID graveler.RepositoryID + hookDestination graveler.BranchID + hookSource graveler.Ref + hookCommit graveler.Commit + ) + if tt.hook { + hookErr := tt.err + g.SetPreMergeHook(func(ctx context.Context, repositoryID graveler.RepositoryID, destination graveler.BranchID, source graveler.Ref, commit graveler.Commit) error { + called = true + hookRepositoryID = repositoryID + hookDestination = destination + hookSource = source + hookCommit = commit + return hookErr + }) + } + // call merge + _, _, err := g.Merge(ctx, mergeRepositoryID, mergeDestination, expectedCommitID.Ref(), graveler.CommitParams{ + Committer: commitCommitter, + Message: mergeMessage, + Metadata: mergeMetadata, + }) + // verify we got an error + if !errors.Is(err, tt.err) { + t.Fatalf("Merge err=%v, pre-merge error expected=%v", err, tt.err) + } + if err != nil && !errors.Is(err, graveler.ErrAbortedByHook) { + t.Fatalf("Merge err=%v, pre-merge error expected ErrAbortedByHook", err) + } + // verify that calls made until the first error + if tt.hook != called { + t.Fatalf("Merge hook called=%t, expected=%t", called, tt.hook) + } + if !called { + return + } + if hookRepositoryID != mergeRepositoryID { + t.Errorf("Hook repository '%s', expected '%s'", hookRepositoryID, mergeRepositoryID) + } + if hookDestination != mergeDestination { + t.Errorf("Hook destination '%s', expected '%s'", hookDestination, mergeDestination) + } + if hookSource.String() != expectedCommitID.String() { + t.Errorf("Hook source '%s', expected '%s'", hookSource, expectedCommitID) + } + if hookCommit.Message != mergeMessage { + t.Errorf("Hook merge message '%s', expected '%s'", hookCommit.Message, mergeMessage) + } + if diff := deep.Equal(hookCommit.Metadata, mergeMetadata); diff != nil { + t.Error("Hook merge metadata diff:", diff) + } + }) + } +} func TestGraveler_AddCommitToBranchHead(t *testing.T) { conn, _ := tu.GetDB(t, databaseURI)