diff --git a/app/api/controller/pullreq/delete_branch.go b/app/api/controller/pullreq/delete_branch.go index c38bf9ba1..100260c84 100644 --- a/app/api/controller/pullreq/delete_branch.go +++ b/app/api/controller/pullreq/delete_branch.go @@ -107,14 +107,14 @@ func (c *Controller) DeleteBranch(ctx context.Context, if err != nil { return types.DeleteBranchOutput{}, nil, err } - if pr.SourceSHA != branch.SHA { + if pr.SourceSHA != branch.SHA.String() { return types.DeleteBranchOutput{}, nil, errors.Conflict("source branch SHA does not match pull request source SHA") } err = c.git.DeleteBranch(ctx, &git.DeleteBranchParams{ WriteParams: writeParams, BranchName: branchName, - SHA: branch.SHA, + SHA: branch.SHA.String(), }) if err != nil { return types.DeleteBranchOutput{}, nil, err @@ -126,7 +126,7 @@ func (c *Controller) DeleteBranch(ctx context.Context, } _, err := c.activityStore.CreateWithPayload(ctx, pr, session.Principal.ID, - &types.PullRequestActivityPayloadBranchDelete{SHA: branch.SHA}, nil) + &types.PullRequestActivityPayloadBranchDelete{SHA: branch.SHA.String()}, nil) return err }() if err != nil { diff --git a/app/api/controller/repo/controller.go b/app/api/controller/repo/controller.go index 584ddaa90..a7de5e99a 100644 --- a/app/api/controller/repo/controller.go +++ b/app/api/controller/repo/controller.go @@ -81,6 +81,8 @@ type Controller struct { executionStore store.ExecutionStore principalStore store.PrincipalStore ruleStore store.RuleStore + checkStore store.CheckStore + pullReqStore store.PullReqStore settings *settings.Service principalInfoCache store.PrincipalInfoCache userGroupStore store.UserGroupStore @@ -113,6 +115,8 @@ func NewController( executionStore store.ExecutionStore, principalStore store.PrincipalStore, ruleStore store.RuleStore, + checkStore store.CheckStore, + pullReqStore store.PullReqStore, settings *settings.Service, principalInfoCache store.PrincipalInfoCache, protectionManager *protection.Manager, @@ -144,6 +148,8 @@ func NewController( executionStore: executionStore, principalStore: principalStore, ruleStore: ruleStore, + checkStore: checkStore, + pullReqStore: pullReqStore, settings: settings, principalInfoCache: principalInfoCache, protectionManager: protectionManager, diff --git a/app/api/controller/repo/get_branch.go b/app/api/controller/repo/get_branch.go index 05c1f0952..7d5d55c19 100644 --- a/app/api/controller/repo/get_branch.go +++ b/app/api/controller/repo/get_branch.go @@ -30,7 +30,8 @@ func (c *Controller) GetBranch(ctx context.Context, session *auth.Session, repoRef string, branchName string, -) (*types.Branch, error) { + options types.BranchMetadataOptions, +) (*types.BranchExtended, error) { repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) if err != nil { return nil, err @@ -44,10 +45,22 @@ func (c *Controller) GetBranch(ctx context.Context, return nil, fmt.Errorf("failed to get branch: %w", err) } + metadata, err := c.collectBranchMetadata(ctx, repo, []git.Branch{rpcOut.Branch}, options) + if err != nil { + return nil, fmt.Errorf("fail to collect branch metadata: %w", err) + } + branch, err := controller.MapBranch(rpcOut.Branch) if err != nil { return nil, fmt.Errorf("failed to map branch: %w", err) } - return &branch, nil + branchExtended := &types.BranchExtended{ + Branch: branch, + IsDefault: branchName == repo.DefaultBranch, + } + + metadata.apply(0, branchExtended) + + return branchExtended, nil } diff --git a/app/api/controller/repo/get_commit_divergences.go b/app/api/controller/repo/get_commit_divergences.go index dad31b230..065328089 100644 --- a/app/api/controller/repo/get_commit_divergences.go +++ b/app/api/controller/repo/get_commit_divergences.go @@ -21,6 +21,7 @@ import ( "github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/auth" "github.com/harness/gitness/git" + "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" ) @@ -40,20 +41,12 @@ type CommitDivergenceRequest struct { To string `json:"to"` } -// CommitDivergence contains the information of the count of converging commits between two refs. -type CommitDivergence struct { - // Ahead is the count of commits the 'From' ref is ahead of the 'To' ref. - Ahead int32 `json:"ahead"` - // Behind is the count of commits the 'From' ref is behind the 'To' ref. - Behind int32 `json:"behind"` -} - // GetCommitDivergences returns the commit divergences between reference pairs. func (c *Controller) GetCommitDivergences(ctx context.Context, session *auth.Session, repoRef string, in *GetCommitDivergencesInput, -) ([]CommitDivergence, error) { +) ([]types.CommitDivergence, error) { repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) if err != nil { return nil, err @@ -61,7 +54,7 @@ func (c *Controller) GetCommitDivergences(ctx context.Context, // if no requests were provided return an empty list if in == nil || len(in.Requests) == 0 { - return []CommitDivergence{}, nil + return []types.CommitDivergence{}, nil } // if num of requests > page max return error @@ -91,10 +84,9 @@ func (c *Controller) GetCommitDivergences(ctx context.Context, } // map to output type - divergences := make([]CommitDivergence, len(rpcOutput.Divergences)) + divergences := make([]types.CommitDivergence, len(rpcOutput.Divergences)) for i := range rpcOutput.Divergences { - divergences[i].Ahead = rpcOutput.Divergences[i].Ahead - divergences[i].Behind = rpcOutput.Divergences[i].Behind + divergences[i] = types.CommitDivergence(rpcOutput.Divergences[i]) } return divergences, nil diff --git a/app/api/controller/repo/list_branches.go b/app/api/controller/repo/list_branches.go index 74f3697ae..c2d4072bf 100644 --- a/app/api/controller/repo/list_branches.go +++ b/app/api/controller/repo/list_branches.go @@ -20,18 +20,21 @@ import ( "github.com/harness/gitness/app/api/controller" "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/git" + "github.com/harness/gitness/git/sha" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + + "github.com/gotidy/ptr" ) // ListBranches lists the branches of a repo. func (c *Controller) ListBranches(ctx context.Context, session *auth.Session, repoRef string, - includeCommit bool, filter *types.BranchFilter, -) ([]types.Branch, error) { +) ([]types.BranchExtended, error) { repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) if err != nil { return nil, err @@ -39,7 +42,7 @@ func (c *Controller) ListBranches(ctx context.Context, rpcOut, err := c.git.ListBranches(ctx, &git.ListBranchesParams{ ReadParams: git.CreateReadParams(repo), - IncludeCommit: includeCommit, + IncludeCommit: filter.IncludeCommit, Query: filter.Query, Sort: mapToRPCBranchSortOption(filter.Sort), Order: mapToRPCSortOrder(filter.Order), @@ -47,18 +50,149 @@ func (c *Controller) ListBranches(ctx context.Context, PageSize: int32(filter.Size), }) if err != nil { - return nil, err + return nil, fmt.Errorf("fail to get the list of branches from git: %w", err) } - branches := make([]types.Branch, len(rpcOut.Branches)) - for i := range rpcOut.Branches { - branches[i], err = controller.MapBranch(rpcOut.Branches[i]) + branches := rpcOut.Branches + + metadata, err := c.collectBranchMetadata(ctx, repo, branches, filter.BranchMetadataOptions) + if err != nil { + return nil, fmt.Errorf("fail to collect branch metadata: %w", err) + } + + response := make([]types.BranchExtended, len(branches)) + for i := range branches { + response[i].Branch, err = controller.MapBranch(branches[i]) if err != nil { return nil, fmt.Errorf("failed to map branch: %w", err) } + + response[i].IsDefault = repo.DefaultBranch == branches[i].Name + + metadata.apply(i, &response[i]) } - return branches, nil + return response, nil +} + +// collectBranchMetadata collects the metadata for the provided list of branches. +// The metadata includes check, rules, pull requests, and branch divergences. +// Each of these would be returned only if the corresponding option is true. +func (c *Controller) collectBranchMetadata( + ctx context.Context, + repo *types.Repository, + branches []git.Branch, + options types.BranchMetadataOptions, +) (branchMetadataOutput, error) { + var ( + checkSummary map[sha.SHA]types.CheckCountSummary + branchRuleMap map[string][]types.RuleInfo + pullReqMap map[string][]*types.PullReq + divergences *git.GetCommitDivergencesOutput + err error + ) + + if options.IncludeChecks { + commitSHAs := make([]string, len(branches)) + for i := range branches { + commitSHAs[i] = branches[i].SHA.String() + } + + checkSummary, err = c.checkStore.ResultSummary(ctx, repo.ID, commitSHAs) + if err != nil { + return branchMetadataOutput{}, fmt.Errorf("fail to fetch check summary for commits: %w", err) + } + } + + if options.IncludeRules { + rules, err := c.protectionManager.ForRepository(ctx, repo.ID) + if err != nil { + return branchMetadataOutput{}, fmt.Errorf("failed to fetch protection rules for the repository: %w", err) + } + + branchRuleMap = make(map[string][]types.RuleInfo) + for i := range branches { + branchName := branches[i].Name + + branchRuleInfos, err := protection.GetRuleInfos( + rules, + repo.DefaultBranch, + branchName, + protection.RuleInfoFilterStatusActive, + protection.RuleInfoFilterTypeBranch) + if err != nil { + return branchMetadataOutput{}, fmt.Errorf("failed get branch rule infos: %w", err) + } + + branchRuleMap[branchName] = branchRuleInfos + } + } + + if options.IncludePullReqs { + branchNames := make([]string, len(branches)) + for i := range branches { + branchNames[i] = branches[i].Name + } + + pullReqMap, err = c.pullReqStore.ListOpenByBranchName(ctx, repo.ID, branchNames) + if err != nil { + return branchMetadataOutput{}, fmt.Errorf("fail to fetch pull requests per branch: %w", err) + } + } + + if options.MaxDivergence > 0 { + readParams := git.CreateReadParams(repo) + + divergenceRequests := make([]git.CommitDivergenceRequest, len(branches)) + for i := range branches { + divergenceRequests[i].From = branches[i].Name + divergenceRequests[i].To = repo.DefaultBranch + } + + divergences, err = c.git.GetCommitDivergences(ctx, &git.GetCommitDivergencesParams{ + ReadParams: readParams, + MaxCount: int32(options.MaxDivergence), + Requests: divergenceRequests, + }) + if err != nil { + return branchMetadataOutput{}, fmt.Errorf("fail to fetch commit divergences: %w", err) + } + } + + return branchMetadataOutput{ + checkSummary: checkSummary, + branchRuleMap: branchRuleMap, + pullReqMap: pullReqMap, + divergences: divergences, + }, nil +} + +type branchMetadataOutput struct { + checkSummary map[sha.SHA]types.CheckCountSummary + branchRuleMap map[string][]types.RuleInfo + pullReqMap map[string][]*types.PullReq + divergences *git.GetCommitDivergencesOutput +} + +func (metadata branchMetadataOutput) apply( + idx int, + branch *types.BranchExtended, +) { + if metadata.checkSummary != nil { + branch.CheckSummary = ptr.Of(metadata.checkSummary[branch.SHA]) + } + + if metadata.branchRuleMap != nil { + branch.Rules = metadata.branchRuleMap[branch.Name] + } + + if metadata.pullReqMap != nil { + branch.PullRequests = metadata.pullReqMap[branch.Name] + } + + if metadata.divergences != nil { + branch.CommitDivergence = ptr.Of(types.CommitDivergence(metadata.divergences.Divergences[idx])) + } } func mapToRPCBranchSortOption(o enum.BranchSortOption) git.BranchSortOption { diff --git a/app/api/controller/repo/wire.go b/app/api/controller/repo/wire.go index 5a7e2dced..aabcc6411 100644 --- a/app/api/controller/repo/wire.go +++ b/app/api/controller/repo/wire.go @@ -56,6 +56,8 @@ func ProvideController( principalStore store.PrincipalStore, executionStore store.ExecutionStore, ruleStore store.RuleStore, + checkStore store.CheckStore, + pullReqStore store.PullReqStore, settings *settings.Service, principalInfoCache store.PrincipalInfoCache, protectionManager *protection.Manager, @@ -79,7 +81,8 @@ func ProvideController( return NewController(config, tx, urlProvider, authorizer, repoStore, spaceStore, pipelineStore, executionStore, - principalStore, ruleStore, settings, principalInfoCache, protectionManager, rpcClient, importer, + principalStore, ruleStore, checkStore, pullReqStore, settings, + principalInfoCache, protectionManager, rpcClient, importer, codeOwners, reporeporter, indexer, limiter, locker, auditService, mtxManager, identifierCheck, repoChecks, publicAccess, labelSvc, instrumentation, userGroupStore, userGroupService) } diff --git a/app/api/controller/util.go b/app/api/controller/util.go index d6bc328da..40f360e8e 100644 --- a/app/api/controller/util.go +++ b/app/api/controller/util.go @@ -91,7 +91,7 @@ func MapBranch(b git.Branch) (types.Branch, error) { } return types.Branch{ Name: b.Name, - SHA: b.SHA.String(), + SHA: b.SHA, Commit: commit, }, nil } diff --git a/app/api/handler/repo/get_branch.go b/app/api/handler/repo/get_branch.go index cab63f5ef..c32ba835d 100644 --- a/app/api/handler/repo/get_branch.go +++ b/app/api/handler/repo/get_branch.go @@ -22,9 +22,7 @@ import ( "github.com/harness/gitness/app/api/request" ) -/* - * Gets a given branch. - */ +// HandleGetBranch returns a given branch. func HandleGetBranch(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -34,13 +32,20 @@ func HandleGetBranch(repoCtrl *repo.Controller) http.HandlerFunc { render.TranslatedUserError(ctx, w, err) return } + branchName, err := request.GetRemainderFromPath(r) if err != nil { render.TranslatedUserError(ctx, w, err) return } - branch, err := repoCtrl.GetBranch(ctx, session, repoRef, branchName) + options, err := request.ParseBranchMetadataOptions(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + branch, err := repoCtrl.GetBranch(ctx, session, repoRef, branchName, options) if err != nil { render.TranslatedUserError(ctx, w, err) return diff --git a/app/api/handler/repo/list_branches.go b/app/api/handler/repo/list_branches.go index 6b91c70bc..d8392fcc0 100644 --- a/app/api/handler/repo/list_branches.go +++ b/app/api/handler/repo/list_branches.go @@ -22,9 +22,7 @@ import ( "github.com/harness/gitness/app/api/request" ) -/* - * Writes json-encoded branch information to the http response body. - */ +// HandleListBranches writes json-encoded branch list to the http response body. func HandleListBranches(repoCtrl *repo.Controller) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -35,15 +33,13 @@ func HandleListBranches(repoCtrl *repo.Controller) http.HandlerFunc { return } - includeCommit, err := request.GetIncludeCommitFromQueryOrDefault(r, false) + filter, err := request.ParseBranchFilter(r) if err != nil { render.TranslatedUserError(ctx, w, err) return } - filter := request.ParseBranchFilter(r) - - branches, err := repoCtrl.ListBranches(ctx, session, repoRef, includeCommit, filter) + branches, err := repoCtrl.ListBranches(ctx, session, repoRef, filter) if err != nil { render.TranslatedUserError(ctx, w, err) return diff --git a/app/api/openapi/repo.go b/app/api/openapi/repo.go index b4534ac69..0a69582a3 100644 --- a/app/api/openapi/repo.go +++ b/app/api/openapi/repo.go @@ -312,6 +312,71 @@ var queryParameterIncludeCommit = openapi3.ParameterOrRef{ }, } +var queryParameterIncludeChecks = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamIncludeChecks, + In: openapi3.ParameterInQuery, + Description: ptr.String( + "If true, the summary of check for the branch commit SHA would be included in the response."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeBoolean), + Default: ptrptr(false), + }, + }, + }, +} + +var queryParameterIncludeRules = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamIncludeRules, + In: openapi3.ParameterInQuery, + Description: ptr.String( + "If true, a list of rules that apply to this branch would be included in the response."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeBoolean), + Default: ptrptr(false), + }, + }, + }, +} + +var queryParameterIncludePullReqs = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamIncludePullReqs, + In: openapi3.ParameterInQuery, + Description: ptr.String( + "If true, a list of pull requests from the branch would be included in the response."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeBoolean), + Default: ptrptr(false), + }, + }, + }, +} + +var queryParameterMaxDivergence = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamMaxDivergence, + In: openapi3.ParameterInQuery, + Description: ptr.String( + "If greater than zero, branch divergence from the default branch will be included in the response. " + + "The divergence would be calculated up the this many commits."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeInteger), + Default: ptrptr(0), + }, + }, + }, +} + var queryParameterIncludeDirectories = openapi3.ParameterOrRef{ Parameter: &openapi3.Parameter{ Name: request.QueryParamIncludeDirectories, @@ -888,7 +953,7 @@ func repoOperations(reflector *openapi3.Reflector) { opCalulateCommitDivergence.WithTags("repository") opCalulateCommitDivergence.WithMapOfAnything(map[string]interface{}{"operationId": "calculateCommitDivergence"}) _ = reflector.SetRequest(&opCalulateCommitDivergence, new(calculateCommitDivergenceRequest), http.MethodPost) - _ = reflector.SetJSONResponse(&opCalulateCommitDivergence, []repo.CommitDivergence{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opCalulateCommitDivergence, []types.CommitDivergence{}, http.StatusOK) _ = reflector.SetJSONResponse(&opCalulateCommitDivergence, new(usererror.Error), http.StatusInternalServerError) _ = reflector.SetJSONResponse(&opCalulateCommitDivergence, new(usererror.Error), http.StatusUnauthorized) _ = reflector.SetJSONResponse(&opCalulateCommitDivergence, new(usererror.Error), http.StatusForbidden) @@ -911,8 +976,12 @@ func repoOperations(reflector *openapi3.Reflector) { opGetBranch := openapi3.Operation{} opGetBranch.WithTags("repository") opGetBranch.WithMapOfAnything(map[string]interface{}{"operationId": "getBranch"}) + opGetBranch.WithParameters( + queryParameterIncludeChecks, queryParameterIncludeRules, queryParameterIncludePullReqs, + queryParameterMaxDivergence, + ) _ = reflector.SetRequest(&opGetBranch, new(getBranchRequest), http.MethodGet) - _ = reflector.SetJSONResponse(&opGetBranch, new(types.Branch), http.StatusOK) + _ = reflector.SetJSONResponse(&opGetBranch, new(types.BranchExtended), http.StatusOK) _ = reflector.SetJSONResponse(&opGetBranch, new(usererror.Error), http.StatusInternalServerError) _ = reflector.SetJSONResponse(&opGetBranch, new(usererror.Error), http.StatusUnauthorized) _ = reflector.SetJSONResponse(&opGetBranch, new(usererror.Error), http.StatusForbidden) @@ -935,11 +1004,15 @@ func repoOperations(reflector *openapi3.Reflector) { opListBranches := openapi3.Operation{} opListBranches.WithTags("repository") opListBranches.WithMapOfAnything(map[string]interface{}{"operationId": "listBranches"}) - opListBranches.WithParameters(queryParameterIncludeCommit, + opListBranches.WithParameters( queryParameterQueryBranches, queryParameterOrder, queryParameterSortBranch, - QueryParameterPage, QueryParameterLimit) + QueryParameterPage, QueryParameterLimit, + queryParameterIncludeCommit, + queryParameterIncludeChecks, queryParameterIncludeRules, queryParameterIncludePullReqs, + queryParameterMaxDivergence, + ) _ = reflector.SetRequest(&opListBranches, new(listBranchesRequest), http.MethodGet) - _ = reflector.SetJSONResponse(&opListBranches, []types.Branch{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opListBranches, []types.BranchExtended{}, http.StatusOK) _ = reflector.SetJSONResponse(&opListBranches, new(usererror.Error), http.StatusInternalServerError) _ = reflector.SetJSONResponse(&opListBranches, new(usererror.Error), http.StatusUnauthorized) _ = reflector.SetJSONResponse(&opListBranches, new(usererror.Error), http.StatusForbidden) diff --git a/app/api/request/git.go b/app/api/request/git.go index 25039d2b3..2d48fa834 100644 --- a/app/api/request/git.go +++ b/app/api/request/git.go @@ -44,6 +44,11 @@ const ( QueryParamInternal = "internal" QueryParamService = "service" QueryParamCommitSHA = "commit_sha" + + QueryParamIncludeChecks = "include_checks" + QueryParamIncludeRules = "include_rules" + QueryParamIncludePullReqs = "include_pullreqs" + QueryParamMaxDivergence = "max_divergence" ) func GetGitRefFromQueryOrDefault(r *http.Request, deflt string) string { @@ -54,6 +59,22 @@ func GetIncludeCommitFromQueryOrDefault(r *http.Request, deflt bool) (bool, erro return QueryParamAsBoolOrDefault(r, QueryParamIncludeCommit, deflt) } +func GetIncludeChecksFromQueryOrDefault(r *http.Request, deflt bool) (bool, error) { + return QueryParamAsBoolOrDefault(r, QueryParamIncludeChecks, deflt) +} + +func GetIncludeRulesFromQueryOrDefault(r *http.Request, deflt bool) (bool, error) { + return QueryParamAsBoolOrDefault(r, QueryParamIncludeRules, deflt) +} + +func GetIncludePullReqsFromQueryOrDefault(r *http.Request, deflt bool) (bool, error) { + return QueryParamAsBoolOrDefault(r, QueryParamIncludePullReqs, deflt) +} + +func GetMaxDivergenceFromQueryOrDefault(r *http.Request, deflt int64) (int64, error) { + return QueryParamAsPositiveInt64OrDefault(r, QueryParamMaxDivergence, deflt) +} + func GetIncludeDirectoriesFromQueryOrDefault(r *http.Request, deflt bool) (bool, error) { return QueryParamAsBoolOrDefault(r, QueryParamIncludeDirectories, deflt) } @@ -69,15 +90,56 @@ func ParseSortBranch(r *http.Request) enum.BranchSortOption { ) } -// ParseBranchFilter extracts the branch filter from the url. -func ParseBranchFilter(r *http.Request) *types.BranchFilter { - return &types.BranchFilter{ - Query: ParseQuery(r), - Sort: ParseSortBranch(r), - Order: ParseOrder(r), - Page: ParsePage(r), - Size: ParseLimit(r), +func ParseBranchMetadataOptions(r *http.Request) (types.BranchMetadataOptions, error) { + includeChecks, err := GetIncludeChecksFromQueryOrDefault(r, false) + if err != nil { + return types.BranchMetadataOptions{}, err } + + includeRules, err := GetIncludeRulesFromQueryOrDefault(r, false) + if err != nil { + return types.BranchMetadataOptions{}, err + } + + includePullReqs, err := GetIncludePullReqsFromQueryOrDefault(r, false) + if err != nil { + return types.BranchMetadataOptions{}, err + } + + maxDivergence, err := GetMaxDivergenceFromQueryOrDefault(r, 0) + if err != nil { + return types.BranchMetadataOptions{}, err + } + + return types.BranchMetadataOptions{ + IncludeChecks: includeChecks, + IncludeRules: includeRules, + IncludePullReqs: includePullReqs, + MaxDivergence: int(maxDivergence), + }, nil +} + +// ParseBranchFilter extracts the branch filter from the url. +func ParseBranchFilter(r *http.Request) (*types.BranchFilter, error) { + includeCommit, err := GetIncludeCommitFromQueryOrDefault(r, false) + if err != nil { + return nil, err + } + + metadataOptions, err := ParseBranchMetadataOptions(r) + if err != nil { + return nil, err + } + + return &types.BranchFilter{ + Query: ParseQuery(r), + Sort: ParseSortBranch(r), + Order: ParseOrder(r), + Page: ParsePage(r), + Size: ParseLimit(r), + IncludeCommit: includeCommit, + BranchMetadataOptions: metadataOptions, + }, nil } // ParseSortTag extracts the tag sort parameter from the url. diff --git a/app/services/protection/get_rule_infos.go b/app/services/protection/get_rule_infos.go new file mode 100644 index 000000000..a5f211d44 --- /dev/null +++ b/app/services/protection/get_rule_infos.go @@ -0,0 +1,68 @@ +// 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 protection + +import ( + "fmt" + + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +var RuleInfoFilterTypeBranch = func(r *types.RuleInfoInternal) (bool, error) { + return r.Type == TypeBranch, nil +} + +var RuleInfoFilterStatusActive = func(r *types.RuleInfoInternal) (bool, error) { + return r.State == enum.RuleStateActive, nil +} + +func GetRuleInfos( + protection Protection, + defaultBranch string, + branchName string, + filterFns ...func(*types.RuleInfoInternal) (bool, error), +) (ruleInfos []types.RuleInfo, err error) { + v, ok := protection.(ruleSet) + if !ok { + return ruleInfos, nil + } + + err = v.forEachRuleMatchBranch( + defaultBranch, + branchName, + func(r *types.RuleInfoInternal, _ Protection) error { + for _, filterFn := range filterFns { + allow, err := filterFn(r) + if err != nil { + return fmt.Errorf("rule info filter function error: %w", err) + } + + if !allow { + return nil + } + } + + ruleInfos = append(ruleInfos, r.RuleInfo) + + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to process each rule in ruleSet: %w", err) + } + + return ruleInfos, nil +} diff --git a/app/services/protection/set.go b/app/services/protection/set.go index 068ba7ea8..4179fb7ec 100644 --- a/app/services/protection/set.go +++ b/app/services/protection/set.go @@ -62,7 +62,7 @@ func (s ruleSet) MergeVerify( return nil }) if err != nil { - return out, nil, fmt.Errorf("failed to merge verify: %w", err) + return out, nil, fmt.Errorf("failed to process each rule in ruleSet: %w", err) } return out, violations, nil @@ -95,7 +95,7 @@ func (s ruleSet) RequiredChecks( return nil }) if err != nil { - return RequiredChecksOutput{}, err + return RequiredChecksOutput{}, fmt.Errorf("failed to process each rule in ruleSet: %w", err) } return RequiredChecksOutput{ @@ -122,7 +122,7 @@ func (s ruleSet) RefChangeVerify(ctx context.Context, in RefChangeVerifyInput) ( return nil }) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to process each rule in ruleSet: %w", err) } return violations, nil @@ -143,7 +143,7 @@ func (s ruleSet) UserIDs() ([]int64, error) { return nil }) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to process each rule in ruleSet: %w", err) } result := make([]int64, 0, len(mapIDs)) @@ -169,7 +169,7 @@ func (s ruleSet) UserGroupIDs() ([]int64, error) { return nil }) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to process each rule in ruleSet: %w", err) } result := make([]int64, 0, len(mapIDs)) diff --git a/app/store/database.go b/app/store/database.go index 588967452..a4106b719 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -20,6 +20,7 @@ import ( "encoding/json" "time" + "github.com/harness/gitness/git/sha" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" ) @@ -405,6 +406,9 @@ type ( // Stream returns streams pull requests from repositories. Stream(ctx context.Context, opts *types.PullReqFilter) (<-chan *types.PullReq, <-chan error) + + // ListOpenByBranchName returns open pull requests for each branch. + ListOpenByBranchName(ctx context.Context, repoID int64, branchNames []string) (map[string][]*types.PullReq, error) } PullReqActivityStore interface { @@ -633,6 +637,13 @@ type ( // ListResults returns a list of status check results for a specific commit in a repo. ListResults(ctx context.Context, repoID int64, commitSHA string) ([]types.CheckResult, error) + + // ResultSummary returns a list of status check result summaries for the provided list of commits in a repo. + ResultSummary( + ctx context.Context, + repoID int64, + commitSHAs []string, + ) (map[sha.SHA]types.CheckCountSummary, error) } GitspaceConfigStore interface { diff --git a/app/store/database/check.go b/app/store/database/check.go index b98fee9f9..2647f5924 100644 --- a/app/store/database/check.go +++ b/app/store/database/check.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/harness/gitness/app/store" + "github.com/harness/gitness/git/sha" "github.com/harness/gitness/store/database" "github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/types" @@ -28,7 +29,6 @@ import ( "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" - "github.com/pkg/errors" ) var _ store.CheckStore = (*CheckStore)(nil) @@ -194,7 +194,7 @@ func (s *CheckStore) Count(ctx context.Context, sql, args, err := stmt.ToSql() if err != nil { - return 0, errors.Wrap(err, "Failed to convert query to sql") + return 0, fmt.Errorf("failed to convert query to sql: %w", err) } db := dbtx.GetAccessor(ctx, s.db) @@ -229,7 +229,7 @@ func (s *CheckStore) List(ctx context.Context, sql, args, err := stmt.ToSql() if err != nil { - return nil, errors.Wrap(err, "Failed to convert query to sql") + return nil, fmt.Errorf("failed to convert query to sql: %w", err) } dst := make([]*check, 0) @@ -265,7 +265,7 @@ func (s *CheckStore) ListRecent(ctx context.Context, sql, args, err := stmt.ToSql() if err != nil { - return nil, errors.Wrap(err, "Failed to convert list recent status checks query to sql") + return nil, fmt.Errorf("failed to convert list recent status checks query to sql: %w", err) } dst := make([]string, 0) @@ -293,7 +293,7 @@ func (s *CheckStore) ListResults(ctx context.Context, sql, args, err := stmt.ToSql() if err != nil { - return nil, errors.Wrap(err, "Failed to convert query to sql") + return nil, fmt.Errorf("failed to convert query to sql: %w", err) } result := make([]types.CheckResult, 0) @@ -307,6 +307,77 @@ func (s *CheckStore) ListResults(ctx context.Context, return result, nil } +// ResultSummary returns a list of status check result summaries for the provided list of commits in a repo. +func (s *CheckStore) ResultSummary(ctx context.Context, + repoID int64, + commitSHAs []string, +) (map[sha.SHA]types.CheckCountSummary, error) { + const selectColumns = ` + check_commit_sha, + COUNT(check_status = 'pending') as "count_pending", + COUNT(check_status = 'running') as "count_running", + COUNT(check_status = 'success') as "count_success", + COUNT(check_status = 'failure') as "count_failure", + COUNT(check_status = 'error') as "count_error"` + + stmt := database.Builder. + Select(selectColumns). + From("checks"). + Where("check_repo_id = ?", repoID). + Where(squirrel.Eq{"check_commit_sha": commitSHAs}). + GroupBy("check_commit_sha") + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, fmt.Errorf("failed to convert query to sql: %w", err) + } + + db := dbtx.GetAccessor(ctx, s.db) + + rows, err := db.QueryxContext(ctx, sql, args...) + if err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to execute status check summary query") + } + + defer func() { + _ = rows.Close() + }() + + result := make(map[sha.SHA]types.CheckCountSummary) + + for rows.Next() { + var commitSHAStr string + var countPending int + var countRunning int + var countSuccess int + var countFailure int + var countError int + err := rows.Scan(&commitSHAStr, &countPending, &countRunning, &countSuccess, &countFailure, &countError) + if err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to scan values of status check summary query") + } + + commitSHA, err := sha.New(commitSHAStr) + if err != nil { + return nil, fmt.Errorf("invalid commit SHA read from DB: %s", commitSHAStr) + } + + result[commitSHA] = types.CheckCountSummary{ + Pending: countPending, + Running: countRunning, + Success: countSuccess, + Failure: countFailure, + Error: countError, + } + } + + if err := rows.Err(); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to read status chek summary") + } + + return result, nil +} + func (*CheckStore) applyOpts(stmt squirrel.SelectBuilder, query string) squirrel.SelectBuilder { if query != "" { stmt = stmt.Where("LOWER(check_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(query))) diff --git a/app/store/database/pullreq.go b/app/store/database/pullreq.go index 6f097e80f..51dc9f368 100644 --- a/app/store/database/pullreq.go +++ b/app/store/database/pullreq.go @@ -21,6 +21,7 @@ import ( "time" "github.com/harness/gitness/app/store" + "github.com/harness/gitness/errors" gitness_store "github.com/harness/gitness/store" "github.com/harness/gitness/store/database" "github.com/harness/gitness/store/database/dbtx" @@ -30,7 +31,6 @@ import ( "github.com/Masterminds/squirrel" "github.com/guregu/null" "github.com/jmoiron/sqlx" - "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -457,7 +457,7 @@ func (s *PullReqStore) Count(ctx context.Context, opts *types.PullReqFilter) (in sql, args, err := stmt.ToSql() if err != nil { - return 0, errors.Wrap(err, "Failed to convert query to sql") + return 0, fmt.Errorf("failed to convert query to sql: %w", err) } db := dbtx.GetAccessor(ctx, s.db) @@ -486,7 +486,7 @@ func (s *PullReqStore) List(ctx context.Context, opts *types.PullReqFilter) ([]* sql, args, err := stmt.ToSql() if err != nil { - return nil, errors.Wrap(err, "Failed to convert query to sql") + return nil, fmt.Errorf("failed to convert query to sql: %w", err) } dst := make([]*pullReq, 0) @@ -520,7 +520,7 @@ func (s *PullReqStore) Stream(ctx context.Context, opts *types.PullReqFilter) (< sql, args, err := stmt.ToSql() if err != nil { - chErr <- errors.Wrap(err, "Failed to convert query to sql") + chErr <- fmt.Errorf("failed to convert query to sql: %w", err) return } @@ -553,6 +553,42 @@ func (s *PullReqStore) Stream(ctx context.Context, opts *types.PullReqFilter) (< return chPRs, chErr } +func (s *PullReqStore) ListOpenByBranchName( + ctx context.Context, + repoID int64, + branchNames []string, +) (map[string][]*types.PullReq, error) { + columns := pullReqColumnsNoDescription + stmt := database.Builder.Select(columns) + stmt = stmt.From("pullreqs") + stmt = stmt.Where("pullreq_source_repo_id = ?", repoID) + stmt = stmt.Where("pullreq_state = ?", enum.PullReqStateOpen) + stmt = stmt.Where(squirrel.Eq{"pullreq_source_branch": branchNames}) + stmt = stmt.OrderBy("pullreq_updated desc") + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, fmt.Errorf("failed to convert query to sql: %w", err) + } + + db := dbtx.GetAccessor(ctx, s.db) + + dst := make([]*pullReq, 0) + + err = db.SelectContext(ctx, &dst, sql, args...) + if err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to fetch list of PRs by branch") + } + + prMap := make(map[string][]*types.PullReq) + for _, prDB := range dst { + pr := s.mapPullReq(ctx, prDB) + prMap[prDB.SourceBranch] = append(prMap[prDB.SourceBranch], pr) + } + + return prMap, nil +} + func (s *PullReqStore) listQuery(opts *types.PullReqFilter) squirrel.SelectBuilder { var stmt squirrel.SelectBuilder diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index f0f14529b..4a2d4d2ab 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -174,6 +174,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro pipelineStore := database.ProvidePipelineStore(db) executionStore := database.ProvideExecutionStore(db) ruleStore := database.ProvideRuleStore(db, principalInfoCache) + checkStore := database.ProvideCheckStore(db, principalInfoCache) + pullReqStore := database.ProvidePullReqStore(db, principalInfoCache) settingsStore := database.ProvideSettingsStore(db) settingsService := settings.ProvideService(settingsStore) protectionManager, err := protection.ProvideManager(ruleStore) @@ -249,9 +251,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro instrumentService := instrument.ProvideService() userGroupStore := database.ProvideUserGroupStore(db) searchService := usergroup.ProvideSearchService() - repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, executionStore, ruleStore, settingsService, principalInfoCache, protectionManager, gitInterface, repository, codeownersService, reporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService, labelService, instrumentService, userGroupStore, searchService) + repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, executionStore, ruleStore, checkStore, pullReqStore, settingsService, principalInfoCache, protectionManager, gitInterface, repository, codeownersService, reporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService, labelService, instrumentService, userGroupStore, searchService) reposettingsController := reposettings.ProvideController(authorizer, repoStore, settingsService, auditService) - checkStore := database.ProvideCheckStore(db, principalInfoCache) stageStore := database.ProvideStageStore(db) schedulerScheduler, err := scheduler.ProvideScheduler(stageStore, mutexManager) if err != nil { @@ -274,7 +275,6 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro connectorStore := database.ProvideConnectorStore(db, secretStore) repoGitInfoView := database.ProvideRepoGitInfoView(db) repoGitInfoCache := cache.ProvideRepoGitInfoCache(repoGitInfoView) - pullReqStore := database.ProvidePullReqStore(db, principalInfoCache) listService := pullreq.ProvideListService(transactor, gitInterface, authorizer, spaceStore, repoStore, repoGitInfoCache, pullReqStore, labelService) exporterRepository, err := exporter.ProvideSpaceExporter(provider, gitInterface, repoStore, jobScheduler, executor, encrypter, streamer) if err != nil { diff --git a/types/branch.go b/types/branch.go index db09a63bf..3d5efa378 100644 --- a/types/branch.go +++ b/types/branch.go @@ -14,12 +14,23 @@ package types +import "github.com/harness/gitness/git/sha" + type Branch struct { Name string `json:"name"` - SHA string `json:"sha"` + SHA sha.SHA `json:"sha"` Commit *Commit `json:"commit,omitempty"` } +type BranchExtended struct { + Branch + IsDefault bool `json:"is_default"` + CheckSummary *CheckCountSummary `json:"check_summary,omitempty"` + Rules []RuleInfo `json:"rules,omitempty"` + PullRequests []*PullReq `json:"pull_requests,omitempty"` + CommitDivergence *CommitDivergence `json:"commit_divergence,omitempty"` +} + type CreateBranchOutput struct { Branch DryRunRulesOutput @@ -28,3 +39,11 @@ type CreateBranchOutput struct { type DeleteBranchOutput struct { DryRunRulesOutput } + +// CommitDivergence contains the information of the count of converging commits between two refs. +type CommitDivergence struct { + // Ahead is the count of commits the 'From' ref is ahead of the 'To' ref. + Ahead int32 `json:"ahead"` + // Behind is the count of commits the 'From' ref is behind the 'To' ref. + Behind int32 `json:"behind"` +} diff --git a/types/check.go b/types/check.go index 190e555e1..2d5ffbb3b 100644 --- a/types/check.go +++ b/types/check.go @@ -109,3 +109,11 @@ type PullReqCheck struct { Bypassable bool `json:"bypassable"` Check Check `json:"check"` } + +type CheckCountSummary struct { + Pending int `json:"pending"` + Running int `json:"running"` + Success int `json:"success"` + Failure int `json:"failure"` + Error int `json:"error"` +} diff --git a/types/git.go b/types/git.go index 604732fe1..b2391fe0e 100644 --- a/types/git.go +++ b/types/git.go @@ -40,13 +40,22 @@ type CommitFilter struct { IncludeStats bool `json:"include_stats"` } +type BranchMetadataOptions struct { + IncludeChecks bool `json:"include_checks"` + IncludeRules bool `json:"include_rules"` + IncludePullReqs bool `json:"include_pullreqs"` + MaxDivergence int `json:"max_divergence"` +} + // BranchFilter stores branch query parameters. type BranchFilter struct { - Query string `json:"query"` - Sort enum.BranchSortOption `json:"sort"` - Order enum.Order `json:"order"` - Page int `json:"page"` - Size int `json:"size"` + Query string `json:"query"` + Sort enum.BranchSortOption `json:"sort"` + Order enum.Order `json:"order"` + Page int `json:"page"` + Size int `json:"size"` + IncludeCommit bool `json:"include_commit"` + BranchMetadataOptions } // TagFilter stores commit tag query parameters.