diff --git a/app/api/controller/pullreq/codeowner.go b/app/api/controller/pullreq/codeowner.go new file mode 100644 index 000000000..41af43887 --- /dev/null +++ b/app/api/controller/pullreq/codeowner.go @@ -0,0 +1,95 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pullreq + +import ( + "context" + "errors" + "fmt" + + "github.com/harness/gitness/app/api/usererror" + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/app/services/codeowners" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) CodeOwners( + ctx context.Context, + session *auth.Session, + repoRef string, + pullreqNum int64, +) (types.CodeOwnerEvaluation, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) + if err != nil { + return types.CodeOwnerEvaluation{}, fmt.Errorf("failed to acquire access to repo: %w", err) + } + pr, err := c.pullreqStore.FindByNumber(ctx, repo.ID, pullreqNum) + if err != nil { + return types.CodeOwnerEvaluation{}, fmt.Errorf("failed to get pull request by number: %w", err) + } + + filteredCodeOwners, err := c.codeOwners.GetApplicableCodeOwnersForPR(ctx, repo, pr) + if errors.Is(codeowners.ErrNotFound, err) { + return types.CodeOwnerEvaluation{}, usererror.ErrNotFound + } + if codeowners.IsTooLargeError(err) { + return types.CodeOwnerEvaluation{}, usererror.UnprocessableEntityf(err.Error()) + } + if err != nil { + return types.CodeOwnerEvaluation{}, fmt.Errorf("failed to get codeOwners: %w", err) + } + + if len(filteredCodeOwners.Entries) == 0 { + return types.CodeOwnerEvaluation{ + EvaluationEntries: nil, + FileSha: filteredCodeOwners.FileSHA, + }, nil + } + + reviewers, err := c.reviewerStore.List(ctx, pullreqNum) + if err != nil { + return types.CodeOwnerEvaluation{}, fmt.Errorf("failed to get reviewers by pr: %w", err) + } + + ownerEvaluation, err := c.codeOwners.Evaluate(ctx, filteredCodeOwners, reviewers) + if err != nil { + return types.CodeOwnerEvaluation{}, err + } + + return types.CodeOwnerEvaluation{ + EvaluationEntries: mapCodeOwnerEvaluation(ownerEvaluation), + FileSha: ownerEvaluation.FileSha, + }, nil +} + +func mapCodeOwnerEvaluation(ownerEvaluation *codeowners.Evaluation) []types.CodeOwnerEvaluationEntry { + codeOwnerEvaluationEntries := make([]types.CodeOwnerEvaluationEntry, len(ownerEvaluation.EvaluationEntries)) + for i, entry := range ownerEvaluation.EvaluationEntries { + ownerEvaluations := make([]types.OwnerEvaluation, len(entry.OwnerEvaluations)) + for j, owner := range entry.OwnerEvaluations { + ownerEvaluations[j] = types.OwnerEvaluation{ + Owner: owner.Owner, + ReviewDecision: owner.ReviewDecision, + ReviewSHA: owner.ReviewSHA, + } + } + codeOwnerEvaluationEntries[i] = types.CodeOwnerEvaluationEntry{ + Pattern: entry.Pattern, + OwnerEvaluations: ownerEvaluations, + } + } + return codeOwnerEvaluationEntries +} diff --git a/app/api/controller/pullreq/controller.go b/app/api/controller/pullreq/controller.go index 25084edd9..e44e5f741 100644 --- a/app/api/controller/pullreq/controller.go +++ b/app/api/controller/pullreq/controller.go @@ -24,6 +24,7 @@ import ( "github.com/harness/gitness/app/auth/authz" pullreqevents "github.com/harness/gitness/app/events/pullreq" "github.com/harness/gitness/app/services/codecomments" + "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/pullreq" "github.com/harness/gitness/app/sse" @@ -58,6 +59,7 @@ type Controller struct { pullreqService *pullreq.Service protectionManager *protection.Manager sseStreamer sse.Streamer + codeOwners *codeowners.Service } func NewController( @@ -81,6 +83,7 @@ func NewController( pullreqService *pullreq.Service, protectionManager *protection.Manager, sseStreamer sse.Streamer, + codeowners *codeowners.Service, ) *Controller { return &Controller{ tx: tx, @@ -103,6 +106,7 @@ func NewController( pullreqService: pullreqService, protectionManager: protectionManager, sseStreamer: sseStreamer, + codeOwners: codeowners, } } diff --git a/app/api/controller/pullreq/merge.go b/app/api/controller/pullreq/merge.go index fc253beda..797978896 100644 --- a/app/api/controller/pullreq/merge.go +++ b/app/api/controller/pullreq/merge.go @@ -25,6 +25,7 @@ import ( "github.com/harness/gitness/app/auth" "github.com/harness/gitness/app/bootstrap" pullreqevents "github.com/harness/gitness/app/events/pullreq" + "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/gitrpc" gitrpcenum "github.com/harness/gitness/gitrpc/enum" @@ -142,6 +143,19 @@ func (c *Controller) Merge( return nil, nil, fmt.Errorf("failed to fetch protection rules for the repository: %w", err) } + ownersForPR, err := c.codeOwners.GetApplicableCodeOwnersForPR(ctx, sourceRepo, pr) + if codeowners.IsTooLargeError(err) { + return nil, nil, usererror.UnprocessableEntityf(err.Error()) + } + if err != nil { + return nil, nil, fmt.Errorf("failed to find codeOwners for PR: %w", err) + } + + codeOwnerWithApproval, err := c.codeOwners.Evaluate(ctx, ownersForPR, reviewers) + if err != nil { + return nil, nil, fmt.Errorf("failed to get code owners with approval: %w", err) + } + ruleOut, violations, err := protectionRules.CanMerge(ctx, protection.CanMergeInput{ Actor: &session.Principal, IsSpaceOwner: isSpaceOwner, @@ -151,6 +165,7 @@ func (c *Controller) Merge( Reviewers: reviewers, Method: in.Method, CheckResults: checkResults, + CodeOwners: codeOwnerWithApproval, }) if err != nil { return nil, nil, fmt.Errorf("failed to verify protection rules: %w", err) diff --git a/app/api/controller/pullreq/wire.go b/app/api/controller/pullreq/wire.go index fc22410a6..8de92e567 100644 --- a/app/api/controller/pullreq/wire.go +++ b/app/api/controller/pullreq/wire.go @@ -18,6 +18,7 @@ import ( "github.com/harness/gitness/app/auth/authz" pullreqevents "github.com/harness/gitness/app/events/pullreq" "github.com/harness/gitness/app/services/codecomments" + "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/pullreq" "github.com/harness/gitness/app/sse" @@ -45,6 +46,7 @@ func ProvideController(tx dbtx.Transactor, urlProvider url.Provider, authorizer rpcClient gitrpc.Interface, eventReporter *pullreqevents.Reporter, mtxManager lock.MutexManager, codeCommentMigrator *codecomments.Migrator, pullreqService *pullreq.Service, ruleManager *protection.Manager, sseStreamer sse.Streamer, + codeOwners *codeowners.Service, ) *Controller { return NewController(tx, urlProvider, authorizer, pullReqStore, pullReqActivityStore, @@ -55,5 +57,5 @@ func ProvideController(tx dbtx.Transactor, urlProvider url.Provider, authorizer checkStore, rpcClient, eventReporter, mtxManager, codeCommentMigrator, - pullreqService, ruleManager, sseStreamer) + pullreqService, ruleManager, sseStreamer, codeOwners) } diff --git a/app/api/handler/pullreq/codeowner.go b/app/api/handler/pullreq/codeowner.go new file mode 100644 index 000000000..097bbf239 --- /dev/null +++ b/app/api/handler/pullreq/codeowner.go @@ -0,0 +1,50 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pullreq + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/pullreq" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleCodeOwner(pullreqCtrl *pullreq.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + pullreqNumber, err := request.GetPullReqNumberFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + owners, err := pullreqCtrl.CodeOwners(ctx, session, repoRef, pullreqNumber) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, owners) + } +} diff --git a/app/api/openapi/pullreq.go b/app/api/openapi/pullreq.go index dd62043ba..8a529abe7 100644 --- a/app/api/openapi/pullreq.go +++ b/app/api/openapi/pullreq.go @@ -541,4 +541,18 @@ func pullReqOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&fileViewDelete, new(usererror.Error), http.StatusForbidden) _ = reflector.Spec.AddOperation(http.MethodDelete, "/repos/{repo_ref}/pullreq/{pullreq_number}/file-views/{file_path}", fileViewDelete) + + codeOwners := openapi3.Operation{} + codeOwners.WithTags("pullreq") + codeOwners.WithMapOfAnything(map[string]interface{}{"operationId": "codeownersPullReq"}) + _ = reflector.SetRequest(&codeOwners, new(pullReqRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&codeOwners, types.CodeOwnerEvaluation{}, http.StatusOK) + _ = reflector.SetJSONResponse(&codeOwners, new(usererror.Error), http.StatusUnprocessableEntity) + _ = reflector.SetJSONResponse(&codeOwners, new(usererror.Error), http.StatusNotFound) + _ = reflector.SetJSONResponse(&codeOwners, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&codeOwners, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&codeOwners, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&codeOwners, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodGet, + "/repos/{repo_ref}/pullreq/{pullreq_number}/codeowners", codeOwners) } diff --git a/app/api/usererror/translate.go b/app/api/usererror/translate.go index bcaf1a650..72f422efa 100644 --- a/app/api/usererror/translate.go +++ b/app/api/usererror/translate.go @@ -73,7 +73,7 @@ func Translate(err error) *Error { case errors.Is(err, blob.ErrNotFound): return ErrNotFound case errors.As(err, &maxBytesErr): - return ErrRequestTooLargeF("The request is too large. maximum allowed size is %d bytes", maxBytesErr.Limit) + return RequestTooLargef("The request is too large. maximum allowed size is %d bytes", maxBytesErr.Limit) // gitrpc errors case errors.As(err, &gitrpcError): diff --git a/app/api/usererror/usererror.go b/app/api/usererror/usererror.go index e8dad0d82..99a3cf77c 100644 --- a/app/api/usererror/usererror.go +++ b/app/api/usererror/usererror.go @@ -121,10 +121,16 @@ func BadRequestf(format string, args ...any) *Error { return Newf(http.StatusBadRequest, format, args...) } -func ErrRequestTooLargeF(format string, args ...any) *Error { +// RequestTooLargef returns a new user facing request too large error. +func RequestTooLargef(format string, args ...any) *Error { return Newf(http.StatusRequestEntityTooLarge, format, args...) } +// UnprocessableEntityf returns a new user facing unprocessable entity error. +func UnprocessableEntityf(format string, args ...any) *Error { + return Newf(http.StatusUnprocessableEntity, format, args...) +} + // BadRequestWithPayload returns a new user facing bad request error with payload. func BadRequestWithPayload(message string, values ...map[string]any) *Error { return NewWithPayload(http.StatusBadRequest, message, values...) diff --git a/app/router/api.go b/app/router/api.go index 7d8f193eb..3d73273cd 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -500,6 +500,7 @@ func SetupPullReq(r chi.Router, pullreqCtrl *pullreq.Controller) { r.Get("/", handlerpullreq.HandleFileViewList(pullreqCtrl)) r.Delete("/*", handlerpullreq.HandleFileViewDelete(pullreqCtrl)) }) + r.Get("/codeowners", handlerpullreq.HandleCodeOwner(pullreqCtrl)) }) }) } diff --git a/app/services/codeowners/service.go b/app/services/codeowners/service.go index e75a837ee..54273c445 100644 --- a/app/services/codeowners/service.go +++ b/app/services/codeowners/service.go @@ -17,34 +17,70 @@ package codeowners import ( "bufio" "context" + "errors" "fmt" "io" "strings" "github.com/harness/gitness/app/store" "github.com/harness/gitness/gitrpc" + gitness_store "github.com/harness/gitness/store" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + + "github.com/bmatcuk/doublestar/v4" + "github.com/rs/zerolog/log" ) const ( + oneMegabyte = 1048576 // maxGetContentFileSize specifies the maximum number of bytes a file content response contains. // If a file is any larger, the content is truncated. - maxGetContentFileSize = 1 << 20 // 1 MB + maxGetContentFileSize = oneMegabyte * 4 // 4 MB ) +var ( + ErrNotFound = errors.New("file not found") +) + +// TooLargeError represents an error if codeowners file is too large. +type TooLargeError struct { + FileSize int64 +} + +func IsTooLargeError(err error) bool { + return errors.Is(err, &TooLargeError{}) +} + +func (e *TooLargeError) Error() string { + return fmt.Sprintf( + "The repository's CODEOWNERS file size %.2fMB exceeds the maximum supported size of %dMB", + float32(e.FileSize)/oneMegabyte, + maxGetContentFileSize/oneMegabyte, + ) +} + +//nolint:errorlint // the purpose of this method is to check whether the target itself if of this type. +func (e *TooLargeError) Is(target error) bool { + _, ok := target.(*TooLargeError) + return ok +} + type Config struct { FilePath string } type Service struct { - repoStore store.RepoStore - git gitrpc.Interface - config Config + repoStore store.RepoStore + git gitrpc.Interface + principalStore store.PrincipalStore + config Config } -type codeOwnerFile struct { - Content string - SHA string +type File struct { + Content string + SHA string + TotalSize int64 } type CodeOwners struct { @@ -57,28 +93,52 @@ type Entry struct { Owners []string } +type Evaluation struct { + EvaluationEntries []EvaluationEntry + FileSha string +} + +type EvaluationEntry struct { + Pattern string + OwnerEvaluations []OwnerEvaluation +} + +type OwnerEvaluation struct { + Owner types.PrincipalInfo + ReviewDecision enum.PullReqReviewDecision + ReviewSHA string +} + func New( repoStore store.RepoStore, git gitrpc.Interface, config Config, -) (*Service, error) { + principalStore store.PrincipalStore, +) *Service { service := &Service{ - repoStore: repoStore, - git: git, - config: config, + repoStore: repoStore, + git: git, + config: config, + principalStore: principalStore, } - return service, nil + return service } -func (s *Service) Get(ctx context.Context, - repoID int64) (*CodeOwners, error) { - repo, err := s.repoStore.Find(ctx, repoID) - if err != nil { - return nil, fmt.Errorf("unable to retrieve repo %w", err) +func (s *Service) get( + ctx context.Context, + repo *types.Repository, + ref string, +) (*CodeOwners, error) { + codeOwnerFile, err := s.getCodeOwnerFile(ctx, repo, ref) + // no codeowner file + if gitrpc.ErrorStatus(err) == gitrpc.StatusPathNotFound { + return nil, ErrNotFound } - codeOwnerFile, err := s.getCodeOwnerFile(ctx, repo) if err != nil { - return nil, fmt.Errorf("unable to get codeowner file %w", err) + return nil, fmt.Errorf("unable to get codeowner file: %w", err) + } + if codeOwnerFile.TotalSize > maxGetContentFileSize { + return nil, &TooLargeError{FileSize: codeOwnerFile.TotalSize} } owner, err := s.parseCodeOwner(codeOwnerFile.Content) @@ -124,17 +184,21 @@ func (s *Service) parseCodeOwner(codeOwnersContent string) ([]Entry, error) { return codeOwners, nil } -func (s *Service) getCodeOwnerFile(ctx context.Context, +func (s *Service) getCodeOwnerFile( + ctx context.Context, repo *types.Repository, -) (*codeOwnerFile, error) { + ref string, +) (*File, error) { params := gitrpc.CreateRPCReadParams(repo) + if ref == "" { + ref = "refs/heads/" + repo.DefaultBranch + } node, err := s.git.GetTreeNode(ctx, &gitrpc.GetTreeNodeParams{ ReadParams: params, - GitREF: "refs/heads/" + repo.DefaultBranch, + GitREF: ref, Path: s.config.FilePath, }) if err != nil { - // TODO: check for path not found and return empty codeowners return nil, fmt.Errorf("unable to retrieve codeowner file %w", err) } @@ -160,8 +224,118 @@ func (s *Service) getCodeOwnerFile(ctx context.Context, return nil, fmt.Errorf("failed to read blob content: %w", err) } - return &codeOwnerFile{ - Content: string(content), - SHA: output.SHA, + return &File{ + Content: string(content), + SHA: output.SHA, + TotalSize: output.Size, }, nil } + +func (s *Service) GetApplicableCodeOwnersForPR( + ctx context.Context, + repo *types.Repository, + pr *types.PullReq, +) (*CodeOwners, error) { + codeOwners, err := s.get(ctx, repo, pr.TargetBranch) + if err != nil { + return nil, err + } + + var filteredEntries []Entry + diffFileStats, err := s.git.DiffFileNames(ctx, &gitrpc.DiffParams{ + ReadParams: gitrpc.CreateRPCReadParams(repo), + BaseRef: pr.MergeBaseSHA, + HeadRef: pr.SourceSHA, + }) + if err != nil { + return nil, fmt.Errorf("failed to get diff file stat: %w", err) + } + + for _, entry := range codeOwners.Entries { + ok, err := contains(diffFileStats.Files, entry.Pattern) + if err != nil { + return nil, err + } + if ok { + filteredEntries = append(filteredEntries, entry) + } + } + return &CodeOwners{ + FileSHA: codeOwners.FileSHA, + Entries: filteredEntries, + }, err +} + +func (s *Service) Evaluate( + ctx context.Context, + owners *CodeOwners, + reviewer []*types.PullReqReviewer, +) (*Evaluation, error) { + flattenedReviewers := flattenReviewers(reviewer) + evaluationEntries := make([]EvaluationEntry, len(owners.Entries)) + + for i, entry := range owners.Entries { + ownerEvaluations := make([]OwnerEvaluation, len(entry.Owners)) + for j, owner := range entry.Owners { + if pullreqReviewer, ok := flattenedReviewers[owner]; ok { + ownerEvaluations[j] = OwnerEvaluation{ + Owner: pullreqReviewer.Reviewer, + ReviewDecision: pullreqReviewer.ReviewDecision, + ReviewSHA: pullreqReviewer.SHA, + } + continue + } + principal, err := s.principalStore.FindByEmail(ctx, owner) + if errors.Is(err, gitness_store.ErrResourceNotFound) { + log.Ctx(ctx).Info().Msgf("user %s not found in database hence skipping for code owner: %v", + owner, err) + continue + } + if err != nil { + return &Evaluation{}, fmt.Errorf("error finding user by email: %w", err) + } + ownerEvaluations[j] = OwnerEvaluation{ + Owner: *principal.ToPrincipalInfo(), + } + } + evaluationEntries[i] = EvaluationEntry{ + Pattern: entry.Pattern, + OwnerEvaluations: ownerEvaluations, + } + } + + return &Evaluation{ + EvaluationEntries: evaluationEntries, + FileSha: owners.FileSHA, + }, nil +} + +func flattenReviewers(reviewers []*types.PullReqReviewer) map[string]*types.PullReqReviewer { + r := make(map[string]*types.PullReqReviewer) + for _, reviewer := range reviewers { + r[reviewer.Reviewer.Email] = reviewer + } + return r +} + +// We match a pattern list against a target +// doubleStar match allows to match / separated path wisely. +// A path foo/bar will match against pattern ** or foo/* +// Also, for a directory ending with / we have to return true for all files in that directory, +// hence we append ** for it. +func contains(patterns []string, target string) (bool, error) { + for _, pattern := range patterns { + // in case of / ending rule, owner owns the whole directory hence append ** + if strings.HasSuffix(pattern, "/") { + pattern += "**" + } + match, err := doublestar.PathMatch(pattern, target) + if err != nil { + return false, fmt.Errorf("failed to match pattern due to error: %w", err) + } + if match { + return true, nil + } + } + return false, nil +} diff --git a/app/services/codeowners/service_test.go b/app/services/codeowners/service_test.go index f871bd676..45c437e92 100644 --- a/app/services/codeowners/service_test.go +++ b/app/services/codeowners/service_test.go @@ -15,6 +15,7 @@ package codeowners import ( + "context" "reflect" "testing" @@ -23,14 +24,11 @@ import ( ) func TestService_ParseCodeOwner(t *testing.T) { - content1 := `**/contracts/openapi/v1/ mankrit.singh@harness.io ashish.sanodia@harness.io - ` - content2 := `**/contracts/openapi/v1/ mankrit.singh@harness.io ashish.sanodia@harness.io -/scripts/api mankrit.singh@harness.io ashish.sanodia@harness.io` - content3 := `# codeowner file -**/contracts/openapi/v1/ mankrit.singh@harness.io ashish.sanodia@harness.io -# -/scripts/api mankrit.singh@harness.io ashish.sanodia@harness.io` + content1 := "**/contracts/openapi/v1/ mankrit.singh@harness.io ashish.sanodia@harness.io\n" + content2 := "**/contracts/openapi/v1/ mankrit.singh@harness.io ashish.sanodia@harness.io\n" + + "/scripts/api mankrit.singh@harness.io ashish.sanodia@harness.io" + content3 := "# codeowner file \n**/contracts/openapi/v1/ mankrit.singh@harness.io ashish.sanodia@harness.io\n" + + "#\n/scripts/api mankrit.singh@harness.io ashish.sanodia@harness.io" type fields struct { repoStore store.RepoStore git gitrpc.Interface @@ -100,3 +98,209 @@ func TestService_ParseCodeOwner(t *testing.T) { }) } } + +func Test_contains(t *testing.T) { + pattern1 := [1]string{"*"} + pattern2 := [1]string{"**"} + pattern3 := [1]string{"abc/xyz"} + pattern4 := [2]string{"abc/xyz", "*"} + pattern5 := [2]string{"abc/xyz", "**"} + pattern6 := [1]string{"doc/frotz"} + pattern7 := [1]string{"?ilename"} + pattern8 := [1]string{"**/foo"} + pattern9 := [1]string{"foo/**"} + pattern10 := [1]string{"a/**/b"} + pattern11 := [1]string{"foo/*"} + pattern12 := [1]string{"*.txt"} + pattern13 := [1]string{"/scripts/"} + + type args struct { + ctx context.Context + slice []string + target string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Test * pattern", + args: args{ + ctx: nil, + slice: pattern1[:], + target: "random", + }, + want: true, + }, + { + name: "Test ** pattern", + args: args{ + ctx: nil, + slice: pattern2[:], + target: "random/xyz", + }, + want: true, + }, + { + name: "Test ** pattern on fixed path", + args: args{ + ctx: nil, + slice: pattern1[:], + target: "abhinav/path", + }, + want: false, + }, + { + name: "Test abc/xyz pattern", + args: args{ + ctx: nil, + slice: pattern3[:], + target: "abc/xyz", + }, + want: true, + }, + { + name: "Test abc/xyz pattern negative", + args: args{ + ctx: nil, + slice: pattern3[:], + target: "abc/xy", + }, + want: false, + }, + { + name: "Test incorrect pattern negative", + args: args{ + ctx: nil, + slice: pattern4[:], + target: "random/path", + }, + want: false, + }, + { + name: "Test * pattern with bigger slice", + args: args{ + ctx: nil, + slice: pattern4[:], + target: "random", + }, + want: true, + }, + { + name: "Test file path with **", + args: args{ + ctx: nil, + slice: pattern5[:], + target: "path/to/file", + }, + want: true, + }, + { + name: "Test / pattern", + args: args{ + ctx: nil, + slice: pattern6[:], + target: "doc/frotz", + }, + want: true, + }, + { + name: "Test ? pattern", + args: args{ + ctx: nil, + slice: pattern7[:], + target: "filename", + }, + want: true, + }, + { + name: "Test /** pattern", + args: args{ + ctx: nil, + slice: pattern8[:], + target: "foo", + }, + want: true, + }, + { + name: "Test /** pattern with slash", + args: args{ + ctx: nil, + slice: pattern8[:], + target: "foo/bar", + }, + want: false, + }, + { + name: "Test **/ with deep nesting", + args: args{ + ctx: nil, + slice: pattern8[:], + target: "path/to/foo", + }, + want: true, + }, + { + name: "Test **/ pattern", + args: args{ + ctx: nil, + slice: pattern9[:], + target: "foo/bar", + }, + want: true, + }, + { + name: "Test a/**/b pattern", + args: args{ + ctx: nil, + slice: pattern10[:], + target: "a/x/y/b", + }, + want: true, + }, + { + name: "Test /* pattern positive", + args: args{ + ctx: nil, + slice: pattern11[:], + target: "foo/getting-started.md", + }, + want: true, + }, + { + name: "Test /* pattern negative", + args: args{ + ctx: nil, + slice: pattern11[:], + target: "foo/build-app/troubleshooting.md", + }, + want: false, + }, + { + name: "Test * for files", + args: args{ + ctx: nil, + slice: pattern12[:], + target: "foo.txt", + }, + want: true, + }, + { + name: "Test /a/", + args: args{ + ctx: nil, + slice: pattern13[:], + target: "/scripts/filename.txt", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, _ := contains(tt.args.slice, tt.args.target); got != tt.want { + t.Errorf("contains() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/app/services/codeowners/wire.go b/app/services/codeowners/wire.go index e19dcbeb3..c73613fa5 100644 --- a/app/services/codeowners/wire.go +++ b/app/services/codeowners/wire.go @@ -29,11 +29,7 @@ func ProvideCodeOwners( gitRPCClient gitrpc.Interface, repoStore store.RepoStore, config Config, -) (*Service, error) { - service, err := New(repoStore, gitRPCClient, config) - if err != nil { - return nil, err - } - - return service, nil + principalStore store.PrincipalStore, +) *Service { + return New(repoStore, gitRPCClient, config, principalStore) } diff --git a/app/services/protection/interface.go b/app/services/protection/interface.go index 809a97b5a..88720ff00 100644 --- a/app/services/protection/interface.go +++ b/app/services/protection/interface.go @@ -18,6 +18,7 @@ import ( "context" "errors" + "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" ) @@ -47,7 +48,7 @@ type ( Reviewers []*types.PullReqReviewer Method enum.MergeMethod CheckResults []types.CheckResult - // TODO: Add code owners + CodeOwners *codeowners.Evaluation } CanMergeOutput struct { diff --git a/app/services/protection/rule_branch.go b/app/services/protection/rule_branch.go index 3095b03e9..3b4ba107e 100644 --- a/app/services/protection/rule_branch.go +++ b/app/services/protection/rule_branch.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" + "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" @@ -34,6 +35,11 @@ type Branch struct { Lifecycle DefLifecycle `json:"lifecycle"` } +type Review struct { + ReviewSHA string + Decision enum.PullReqReviewDecision +} + var _ Definition = (*Branch)(nil) // ensures that the Branch type implements Definition interface. //nolint:gocognit // well aware of this @@ -69,7 +75,40 @@ func (v *Branch) CanMerge(_ context.Context, in CanMergeInput) (CanMergeOutput, len(approvedBy), v.PullReq.Approvals.RequireMinimumCount) } - // TODO: implement v.PullReq.Approvals.RequireCodeOwners + //nolint:nestif + if v.PullReq.Approvals.RequireCodeOwners { + for _, entry := range in.CodeOwners.EvaluationEntries { + reviewDecision, approvers := getCodeOwnerApprovalStatus(entry.OwnerEvaluations) + + if reviewDecision == enum.PullReqReviewDecisionPending { + violations.Addf("pullreq.approvals.require_code_owners", + "Code owners approval pending for %s", entry.Pattern) + continue + } + + if reviewDecision == enum.PullReqReviewDecisionChangeReq { + violations.Addf("pullreq.approvals.require_code_owners", + "Code owners requested changes for %s", entry.Pattern) + continue + } + // pull req approved. check other settings + if !v.PullReq.Approvals.RequireLatestCommit { + continue + } + // check for latest commit approved or not + latestSHAApproved := false + for _, approver := range approvers { + if approver.ReviewSHA == in.PullReq.SourceSHA { + latestSHAApproved = true + break + } + } + if !latestSHAApproved { + violations.Addf("pullreq.approvals.require_code_owners", + "Code owners approval pending on latest commit for %s", entry.Pattern) + } + } + } // pullreq.comments @@ -273,3 +312,21 @@ func (v DefPullReq) Validate() error { return nil } + +func getCodeOwnerApprovalStatus( + ownerStatus []codeowners.OwnerEvaluation, +) (enum.PullReqReviewDecision, []codeowners.OwnerEvaluation) { + approvers := make([]codeowners.OwnerEvaluation, 0) + for _, o := range ownerStatus { + if o.ReviewDecision == enum.PullReqReviewDecisionChangeReq { + return enum.PullReqReviewDecisionChangeReq, nil + } + if o.ReviewDecision == enum.PullReqReviewDecisionApproved { + approvers = append(approvers, o) + } + } + if len(approvers) > 0 { + return enum.PullReqReviewDecisionApproved, approvers + } + return enum.PullReqReviewDecisionPending, nil +} diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 575f401af..aca1d8e62 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -153,10 +153,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } codeownersConfig := server.ProvideCodeOwnerConfig(config) - codeownersService, err := codeowners.ProvideCodeOwners(gitrpcInterface, repoStore, codeownersConfig) - if err != nil { - return nil, err - } + codeownersService := codeowners.ProvideCodeOwners(gitrpcInterface, repoStore, codeownersConfig, principalStore) repoController := repo.ProvideController(config, transactor, provider, pathUID, authorizer, repoStore, spaceStore, pipelineStore, principalStore, ruleStore, protectionManager, gitrpcInterface, repository, codeownersService) executionStore := database.ProvideExecutionStore(db) checkStore := database.ProvideCheckStore(db, principalInfoCache) @@ -219,7 +216,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro if err != nil { return nil, err } - pullreqController := pullreq2.ProvideController(transactor, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, pullReqFileViewStore, membershipStore, checkStore, gitrpcInterface, reporter, mutexManager, migrator, pullreqService, protectionManager, streamer) + pullreqController := pullreq2.ProvideController(transactor, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, pullReqFileViewStore, membershipStore, checkStore, gitrpcInterface, reporter, mutexManager, migrator, pullreqService, protectionManager, streamer, codeownersService) webhookConfig := server.ProvideWebhookConfig(config) webhookStore := database.ProvideWebhookStore(db) webhookExecutionStore := database.ProvideWebhookExecutionStore(db) diff --git a/gitrpc/diff.go b/gitrpc/diff.go index c34402111..107e36493 100644 --- a/gitrpc/diff.go +++ b/gitrpc/diff.go @@ -127,26 +127,26 @@ func (c *Client) DiffShortStat(ctx context.Context, params *DiffParams) (DiffSho }, nil } -type DiffFileStatOutput struct { +type DiffFileNamesOutput struct { Files []string } -func (c *Client) DiffFileStat(ctx context.Context, params *DiffParams) (DiffFileStatOutput, error) { +func (c *Client) DiffFileNames(ctx context.Context, params *DiffParams) (DiffFileNamesOutput, error) { if err := params.Validate(); err != nil { - return DiffFileStatOutput{}, err + return DiffFileNamesOutput{}, err } - fileStat, err := c.diffService.DiffFileStat(ctx, &rpc.DiffRequest{ + fileNames, err := c.diffService.DiffFileNames(ctx, &rpc.DiffRequest{ Base: mapToRPCReadRequest(params.ReadParams), BaseRef: params.BaseRef, HeadRef: params.HeadRef, MergeBase: params.MergeBase, }) if err != nil { - return DiffFileStatOutput{}, processRPCErrorf(err, "failed to get diff file data between '%s' and '%s'", + return DiffFileNamesOutput{}, processRPCErrorf(err, "failed to get diff file data between '%s' and '%s'", params.BaseRef, params.HeadRef) } - return DiffFileStatOutput{ - Files: fileStat.GetFiles(), + return DiffFileNamesOutput{ + Files: fileNames.GetFiles(), }, nil } diff --git a/gitrpc/interface.go b/gitrpc/interface.go index a1bfb5c7b..d50b3c9f4 100644 --- a/gitrpc/interface.go +++ b/gitrpc/interface.go @@ -65,7 +65,7 @@ type Interface interface { */ RawDiff(ctx context.Context, in *DiffParams, w io.Writer) error Diff(ctx context.Context, in *DiffParams) (<-chan *FileDiff, <-chan error) - DiffFileStat(ctx context.Context, in *DiffParams) (DiffFileStatOutput, error) + DiffFileNames(ctx context.Context, in *DiffParams) (DiffFileNamesOutput, error) CommitDiff(ctx context.Context, params *GetCommitParams, w io.Writer) error DiffShortStat(ctx context.Context, params *DiffParams) (DiffShortStatOutput, error) DiffStats(ctx context.Context, params *DiffParams) (DiffStatsOutput, error) diff --git a/gitrpc/internal/gitea/diff.go b/gitrpc/internal/gitea/diff.go index 64195a356..30c7a3301 100644 --- a/gitrpc/internal/gitea/diff.go +++ b/gitrpc/internal/gitea/diff.go @@ -188,14 +188,19 @@ func (g Adapter) DiffCut( return diffCutHeader, linesHunk, nil } -func (g Adapter) DiffFileStat( - ctx context.Context, +func (g Adapter) DiffFileName(ctx context.Context, repoPath string, baseRef string, headRef string, + mergeBase bool, ) ([]string, error) { - cmd := git.NewCommand(ctx, - "diff", "--name-only", headRef, baseRef) + args := make([]string, 0, 8) + args = append(args, "diff", "--name-only") + if mergeBase { + args = append(args, "--merge-base") + } + args = append(args, baseRef, headRef) + cmd := git.NewCommand(ctx, args...) stdout, _, runErr := cmd.RunStdBytes(&git.RunOpts{Dir: repoPath}) if runErr != nil { return nil, processGiteaErrorf(runErr, "failed to trigger diff command") diff --git a/gitrpc/internal/service/diff.go b/gitrpc/internal/service/diff.go index 0aa00f41c..dc3436814 100644 --- a/gitrpc/internal/service/diff.go +++ b/gitrpc/internal/service/diff.go @@ -151,18 +151,18 @@ func (s DiffService) GetDiffHunkHeaders( }, nil } -func (s DiffService) DiffFileStat( +func (s DiffService) DiffFileNames( ctx context.Context, r *rpc.DiffRequest, -) (*rpc.DiffFileStatResponse, error) { +) (*rpc.DiffFileNameResponse, error) { base := r.GetBase() repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid()) - files, err := s.adapter.DiffFileStat(ctx, repoPath, r.BaseRef, r.HeadRef) + files, err := s.adapter.DiffFileName(ctx, repoPath, r.BaseRef, r.HeadRef, r.MergeBase) if err != nil { return nil, processGitErrorf(err, "failed to get diff file stat") } - return &rpc.DiffFileStatResponse{Files: files}, nil + return &rpc.DiffFileNameResponse{Files: files}, nil } func (s DiffService) DiffCut( diff --git a/gitrpc/internal/service/interface.go b/gitrpc/internal/service/interface.go index 441991c61..886126630 100644 --- a/gitrpc/internal/service/interface.go +++ b/gitrpc/internal/service/interface.go @@ -111,9 +111,9 @@ type GitAdapter interface { regExpDef string, maxSize int) ([]types.FileContent, error) - DiffFileStat( - ctx context.Context, + DiffFileName(ctx context.Context, repoPath string, baseRef string, - headRef string) ([]string, error) + headRef string, + mergeBase bool) ([]string, error) } diff --git a/gitrpc/proto/diff.proto b/gitrpc/proto/diff.proto index 5ea384028..361c34b13 100644 --- a/gitrpc/proto/diff.proto +++ b/gitrpc/proto/diff.proto @@ -14,7 +14,7 @@ service DiffService { rpc DiffShortStat(DiffRequest) returns (DiffShortStatResponse) {} rpc GetDiffHunkHeaders(GetDiffHunkHeadersRequest) returns (GetDiffHunkHeadersResponse) {} rpc DiffCut(DiffCutRequest) returns (DiffCutResponse) {} - rpc DiffFileStat(DiffRequest) returns (DiffFileStatResponse) {} + rpc DiffFileNames(DiffRequest) returns (DiffFileNameResponse) {} } message DiffRequest { @@ -138,6 +138,6 @@ message CommitDiffResponse { bytes data = 1; } -message DiffFileStatResponse { +message DiffFileNameResponse { repeated string files = 1; } \ No newline at end of file diff --git a/gitrpc/rpc/diff.pb.go b/gitrpc/rpc/diff.pb.go index 849960d41..2a20812b9 100644 --- a/gitrpc/rpc/diff.pb.go +++ b/gitrpc/rpc/diff.pb.go @@ -1022,7 +1022,7 @@ func (x *CommitDiffResponse) GetData() []byte { return nil } -type DiffFileStatResponse struct { +type DiffFileNameResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -1030,8 +1030,8 @@ type DiffFileStatResponse struct { Files []string `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"` } -func (x *DiffFileStatResponse) Reset() { - *x = DiffFileStatResponse{} +func (x *DiffFileNameResponse) Reset() { + *x = DiffFileNameResponse{} if protoimpl.UnsafeEnabled { mi := &file_diff_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1039,13 +1039,13 @@ func (x *DiffFileStatResponse) Reset() { } } -func (x *DiffFileStatResponse) String() string { +func (x *DiffFileNameResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DiffFileStatResponse) ProtoMessage() {} +func (*DiffFileNameResponse) ProtoMessage() {} -func (x *DiffFileStatResponse) ProtoReflect() protoreflect.Message { +func (x *DiffFileNameResponse) ProtoReflect() protoreflect.Message { mi := &file_diff_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1057,12 +1057,12 @@ func (x *DiffFileStatResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DiffFileStatResponse.ProtoReflect.Descriptor instead. -func (*DiffFileStatResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use DiffFileNameResponse.ProtoReflect.Descriptor instead. +func (*DiffFileNameResponse) Descriptor() ([]byte, []int) { return file_diff_proto_rawDescGZIP(), []int{13} } -func (x *DiffFileStatResponse) GetFiles() []string { +func (x *DiffFileNameResponse) GetFiles() []string { if x != nil { return x.Files } @@ -1209,9 +1209,9 @@ var file_diff_proto_rawDesc = []byte{ 0x09, 0x52, 0x03, 0x73, 0x68, 0x61, 0x22, 0x28, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x44, 0x69, 0x66, 0x66, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, - 0x22, 0x2c, 0x0a, 0x14, 0x44, 0x69, 0x66, 0x66, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x22, 0x2c, 0x0a, 0x14, 0x44, 0x69, 0x66, 0x66, 0x46, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x32, 0xc7, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x32, 0xc8, 0x03, 0x0a, 0x0b, 0x44, 0x69, 0x66, 0x66, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x07, 0x52, 0x61, 0x77, 0x44, 0x69, 0x66, 0x66, 0x12, 0x10, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x70, @@ -1236,14 +1236,14 @@ var file_diff_proto_rawDesc = []byte{ 0x00, 0x12, 0x36, 0x0a, 0x07, 0x44, 0x69, 0x66, 0x66, 0x43, 0x75, 0x74, 0x12, 0x13, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x43, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x43, 0x75, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0c, 0x44, 0x69, 0x66, - 0x66, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x12, 0x10, 0x2e, 0x72, 0x70, 0x63, 0x2e, - 0x44, 0x69, 0x66, 0x66, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x70, - 0x63, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x72, 0x6e, 0x65, 0x73, 0x73, 0x2f, 0x67, - 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2f, 0x72, 0x70, - 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0d, 0x44, 0x69, 0x66, + 0x66, 0x46, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x10, 0x2e, 0x72, 0x70, 0x63, + 0x2e, 0x44, 0x69, 0x66, 0x66, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, + 0x70, 0x63, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x46, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x72, 0x6e, 0x65, 0x73, 0x73, 0x2f, + 0x67, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x2f, 0x67, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2f, 0x72, + 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1275,7 +1275,7 @@ var file_diff_proto_goTypes = []interface{}{ (*DiffResponse)(nil), // 11: rpc.DiffResponse (*CommitDiffRequest)(nil), // 12: rpc.CommitDiffRequest (*CommitDiffResponse)(nil), // 13: rpc.CommitDiffResponse - (*DiffFileStatResponse)(nil), // 14: rpc.DiffFileStatResponse + (*DiffFileNameResponse)(nil), // 14: rpc.DiffFileNameResponse nil, // 15: rpc.DiffFileHeader.ExtensionsEntry (*ReadRequest)(nil), // 16: rpc.ReadRequest } @@ -1296,14 +1296,14 @@ var file_diff_proto_depIdxs = []int32{ 1, // 13: rpc.DiffService.DiffShortStat:input_type -> rpc.DiffRequest 7, // 14: rpc.DiffService.GetDiffHunkHeaders:input_type -> rpc.GetDiffHunkHeadersRequest 9, // 15: rpc.DiffService.DiffCut:input_type -> rpc.DiffCutRequest - 1, // 16: rpc.DiffService.DiffFileStat:input_type -> rpc.DiffRequest + 1, // 16: rpc.DiffService.DiffFileNames:input_type -> rpc.DiffRequest 2, // 17: rpc.DiffService.RawDiff:output_type -> rpc.RawDiffResponse 11, // 18: rpc.DiffService.Diff:output_type -> rpc.DiffResponse 13, // 19: rpc.DiffService.CommitDiff:output_type -> rpc.CommitDiffResponse 3, // 20: rpc.DiffService.DiffShortStat:output_type -> rpc.DiffShortStatResponse 8, // 21: rpc.DiffService.GetDiffHunkHeaders:output_type -> rpc.GetDiffHunkHeadersResponse 10, // 22: rpc.DiffService.DiffCut:output_type -> rpc.DiffCutResponse - 14, // 23: rpc.DiffService.DiffFileStat:output_type -> rpc.DiffFileStatResponse + 14, // 23: rpc.DiffService.DiffFileNames:output_type -> rpc.DiffFileNameResponse 17, // [17:24] is the sub-list for method output_type 10, // [10:17] is the sub-list for method input_type 10, // [10:10] is the sub-list for extension type_name @@ -1475,7 +1475,7 @@ func file_diff_proto_init() { } } file_diff_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DiffFileStatResponse); i { + switch v := v.(*DiffFileNameResponse); i { case 0: return &v.state case 1: diff --git a/gitrpc/rpc/diff_grpc.pb.go b/gitrpc/rpc/diff_grpc.pb.go index 47bbc0285..27c2c1f98 100644 --- a/gitrpc/rpc/diff_grpc.pb.go +++ b/gitrpc/rpc/diff_grpc.pb.go @@ -29,7 +29,7 @@ type DiffServiceClient interface { DiffShortStat(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (*DiffShortStatResponse, error) GetDiffHunkHeaders(ctx context.Context, in *GetDiffHunkHeadersRequest, opts ...grpc.CallOption) (*GetDiffHunkHeadersResponse, error) DiffCut(ctx context.Context, in *DiffCutRequest, opts ...grpc.CallOption) (*DiffCutResponse, error) - DiffFileStat(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (*DiffFileStatResponse, error) + DiffFileNames(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (*DiffFileNameResponse, error) } type diffServiceClient struct { @@ -163,9 +163,9 @@ func (c *diffServiceClient) DiffCut(ctx context.Context, in *DiffCutRequest, opt return out, nil } -func (c *diffServiceClient) DiffFileStat(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (*DiffFileStatResponse, error) { - out := new(DiffFileStatResponse) - err := c.cc.Invoke(ctx, "/rpc.DiffService/DiffFileStat", in, out, opts...) +func (c *diffServiceClient) DiffFileNames(ctx context.Context, in *DiffRequest, opts ...grpc.CallOption) (*DiffFileNameResponse, error) { + out := new(DiffFileNameResponse) + err := c.cc.Invoke(ctx, "/rpc.DiffService/DiffFileNames", in, out, opts...) if err != nil { return nil, err } @@ -182,7 +182,7 @@ type DiffServiceServer interface { DiffShortStat(context.Context, *DiffRequest) (*DiffShortStatResponse, error) GetDiffHunkHeaders(context.Context, *GetDiffHunkHeadersRequest) (*GetDiffHunkHeadersResponse, error) DiffCut(context.Context, *DiffCutRequest) (*DiffCutResponse, error) - DiffFileStat(context.Context, *DiffRequest) (*DiffFileStatResponse, error) + DiffFileNames(context.Context, *DiffRequest) (*DiffFileNameResponse, error) mustEmbedUnimplementedDiffServiceServer() } @@ -208,8 +208,8 @@ func (UnimplementedDiffServiceServer) GetDiffHunkHeaders(context.Context, *GetDi func (UnimplementedDiffServiceServer) DiffCut(context.Context, *DiffCutRequest) (*DiffCutResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DiffCut not implemented") } -func (UnimplementedDiffServiceServer) DiffFileStat(context.Context, *DiffRequest) (*DiffFileStatResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method DiffFileStat not implemented") +func (UnimplementedDiffServiceServer) DiffFileNames(context.Context, *DiffRequest) (*DiffFileNameResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DiffFileNames not implemented") } func (UnimplementedDiffServiceServer) mustEmbedUnimplementedDiffServiceServer() {} @@ -341,20 +341,20 @@ func _DiffService_DiffCut_Handler(srv interface{}, ctx context.Context, dec func return interceptor(ctx, in, info, handler) } -func _DiffService_DiffFileStat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _DiffService_DiffFileNames_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DiffRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(DiffServiceServer).DiffFileStat(ctx, in) + return srv.(DiffServiceServer).DiffFileNames(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/rpc.DiffService/DiffFileStat", + FullMethod: "/rpc.DiffService/DiffFileNames", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DiffServiceServer).DiffFileStat(ctx, req.(*DiffRequest)) + return srv.(DiffServiceServer).DiffFileNames(ctx, req.(*DiffRequest)) } return interceptor(ctx, in, info, handler) } @@ -379,8 +379,8 @@ var DiffService_ServiceDesc = grpc.ServiceDesc{ Handler: _DiffService_DiffCut_Handler, }, { - MethodName: "DiffFileStat", - Handler: _DiffService_DiffFileStat_Handler, + MethodName: "DiffFileNames", + Handler: _DiffService_DiffFileNames_Handler, }, }, Streams: []grpc.StreamDesc{ diff --git a/gitrpc/rpc/http.pb.go b/gitrpc/rpc/http.pb.go index 171d83ca8..b9ff35cff 100644 --- a/gitrpc/rpc/http.pb.go +++ b/gitrpc/rpc/http.pb.go @@ -152,6 +152,7 @@ type ServicePackRequest struct { // Depending on the service the matching base type has to be passed // // Types that are assignable to Base: + // // *ServicePackRequest_ReadBase // *ServicePackRequest_WriteBase Base isServicePackRequest_Base `protobuf_oneof:"base"` diff --git a/gitrpc/rpc/operations.pb.go b/gitrpc/rpc/operations.pb.go index 21c44d984..89c027cad 100644 --- a/gitrpc/rpc/operations.pb.go +++ b/gitrpc/rpc/operations.pb.go @@ -264,6 +264,7 @@ type CommitFilesAction struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Payload: + // // *CommitFilesAction_Header // *CommitFilesAction_Content Payload isCommitFilesAction_Payload `protobuf_oneof:"payload"` @@ -347,6 +348,7 @@ type CommitFilesRequest struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Payload: + // // *CommitFilesRequest_Header // *CommitFilesRequest_Action Payload isCommitFilesRequest_Payload `protobuf_oneof:"payload"` diff --git a/gitrpc/rpc/repo.pb.go b/gitrpc/rpc/repo.pb.go index 8b376ddf1..422033501 100644 --- a/gitrpc/rpc/repo.pb.go +++ b/gitrpc/rpc/repo.pb.go @@ -217,6 +217,7 @@ type CreateRepositoryRequest struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Data: + // // *CreateRepositoryRequest_Header // *CreateRepositoryRequest_File Data isCreateRepositoryRequest_Data `protobuf_oneof:"data"` @@ -1312,6 +1313,7 @@ type GetBlobResponse struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Data: + // // *GetBlobResponse_Header // *GetBlobResponse_Content Data isGetBlobResponse_Data `protobuf_oneof:"data"` diff --git a/gitrpc/rpc/shared.pb.go b/gitrpc/rpc/shared.pb.go index ba46c3b76..6030b54ed 100644 --- a/gitrpc/rpc/shared.pb.go +++ b/gitrpc/rpc/shared.pb.go @@ -299,6 +299,7 @@ type FileUpload struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Data: + // // *FileUpload_Header // *FileUpload_Chunk Data isFileUpload_Data `protobuf_oneof:"data"` diff --git a/types/codeowners.go b/types/codeowners.go new file mode 100644 index 000000000..c8d4f9a76 --- /dev/null +++ b/types/codeowners.go @@ -0,0 +1,33 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import "github.com/harness/gitness/types/enum" + +type CodeOwnerEvaluation struct { + EvaluationEntries []CodeOwnerEvaluationEntry `json:"evaluation_entries"` + FileSha string `json:"file_sha"` +} + +type CodeOwnerEvaluationEntry struct { + Pattern string `json:"pattern"` + OwnerEvaluations []OwnerEvaluation `json:"owner_evaluations"` +} + +type OwnerEvaluation struct { + Owner PrincipalInfo `json:"owner"` + ReviewDecision enum.PullReqReviewDecision `json:"review_decision,omitempty"` + ReviewSHA string `json:"review_sha,omitempty"` +} diff --git a/types/config.go b/types/config.go index 190226adb..d9f8503a2 100644 --- a/types/config.go +++ b/types/config.go @@ -283,6 +283,6 @@ type Config struct { } CodeOwners struct { - FilePath string `envconfig:"GITNESS_CODEOWNERS_FILEPATH" default:".gitness/CODEOWNERS"` + FilePath string `envconfig:"GITNESS_CODEOWNERS_FILEPATH" default:".harness/CODEOWNERS"` } }