diff --git a/app/api/openapi/pullreq.go b/app/api/openapi/pullreq.go index 07a205f9c..03e00f0f0 100644 --- a/app/api/openapi/pullreq.go +++ b/app/api/openapi/pullreq.go @@ -393,6 +393,71 @@ var queryParameterIncludeDescription = openapi3.ParameterOrRef{ }, } +var queryParameterAuthorID = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamAuthorID, + In: openapi3.ParameterInQuery, + Description: ptr.String("Return only pull requests where this user is the author."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeInteger), + }, + }, + }, +} + +var queryParameterCommenterID = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamCommenterID, + In: openapi3.ParameterInQuery, + Description: ptr.String("Return only pull requests where this user has created at least one comment."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeInteger), + }, + }, + }, +} + +var queryParameterReviewerID = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamReviewerID, + In: openapi3.ParameterInQuery, + Description: ptr.String("Return only pull requests where this user has been added as a reviewer."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeInteger), + }, + }, + }, +} + +var queryParameterReviewDecision = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamReviewDecision, + In: openapi3.ParameterInQuery, + Description: ptr.String("Require only this review decision of the reviewer. " + + "Requires " + request.QueryParamReviewerID + " parameter."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeArray), + Items: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeString), + Enum: enum.PullReqReviewDecision("").Enum(), + }, + }, + }, + }, + Style: ptr.String(string(openapi3.EncodingStyleForm)), + Explode: ptr.Bool(true), + }, +} + //nolint:funlen func pullReqOperations(reflector *openapi3.Reflector) { createPullReq := openapi3.Operation{} @@ -417,7 +482,8 @@ func pullReqOperations(reflector *openapi3.Reflector) { queryParameterCreatedLt, queryParameterCreatedGt, queryParameterEditedLt, queryParameterEditedGt, queryParameterIncludeDescription, QueryParameterPage, QueryParameterLimit, - QueryParameterLabelID, QueryParameterValueID) + QueryParameterLabelID, QueryParameterValueID, + queryParameterAuthorID, queryParameterCommenterID, queryParameterReviewerID, queryParameterReviewDecision) _ = reflector.SetRequest(&listPullReq, new(listPullReqRequest), http.MethodGet) _ = reflector.SetJSONResponse(&listPullReq, new([]types.PullReq), http.StatusOK) _ = reflector.SetJSONResponse(&listPullReq, new(usererror.Error), http.StatusBadRequest) diff --git a/app/api/openapi/space.go b/app/api/openapi/space.go index 827690ea4..a22890534 100644 --- a/app/api/openapi/space.go +++ b/app/api/openapi/space.go @@ -611,7 +611,8 @@ func spaceOperations(reflector *openapi3.Reflector) { queryParameterCreatedLt, queryParameterCreatedGt, queryParameterEditedLt, queryParameterIncludeDescription, queryParameterIncludeSubspaces, QueryParameterLimit, - QueryParameterLabelID, QueryParameterValueID) + QueryParameterLabelID, QueryParameterValueID, + queryParameterAuthorID, queryParameterCommenterID, queryParameterReviewerID, queryParameterReviewDecision) _ = reflector.SetRequest(&listPullReq, new(listPullReqRequest), http.MethodGet) _ = reflector.SetJSONResponse(&listPullReq, new([]types.PullReq), http.StatusOK) _ = reflector.SetJSONResponse(&listPullReq, new(usererror.Error), http.StatusBadRequest) diff --git a/app/api/request/pullreq.go b/app/api/request/pullreq.go index a92aa5ff9..76b954e55 100644 --- a/app/api/request/pullreq.go +++ b/app/api/request/pullreq.go @@ -18,6 +18,7 @@ import ( "fmt" "net/http" + "github.com/harness/gitness/errors" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" ) @@ -28,6 +29,10 @@ const ( PathParamReviewerID = "pullreq_reviewer_id" PathParamUserGroupID = "user_group_id" + QueryParamAuthorID = "author_id" + QueryParamCommenterID = "commenter_id" + QueryParamReviewerID = "reviewer_id" + QueryParamReviewDecision = "review_decision" QueryParamIncludeDescription = "include_description" ) @@ -70,6 +75,24 @@ func parsePullReqStates(r *http.Request) []enum.PullReqState { return states } +// parseReviewDecisions extracts the pull request reviewer decisions from the url. +func parseReviewDecisions(r *http.Request) []enum.PullReqReviewDecision { + strReviewDecisions, _ := QueryParamList(r, QueryParamReviewDecision) + m := make(map[enum.PullReqReviewDecision]struct{}) // use map to eliminate duplicates + for _, s := range strReviewDecisions { + if state, ok := enum.PullReqReviewDecision(s).Sanitize(); ok { + m[state] = struct{}{} + } + } + + reviewDecisions := make([]enum.PullReqReviewDecision, 0, len(m)) + for s := range m { + reviewDecisions = append(reviewDecisions, s) + } + + return reviewDecisions +} + // ParsePullReqFilter extracts the pull request query parameter from the url. func ParsePullReqFilter(r *http.Request) (*types.PullReqFilter, error) { createdBy, err := QueryParamListAsPositiveInt64(r, QueryParamCreatedBy) @@ -101,6 +124,26 @@ func ParsePullReqFilter(r *http.Request) (*types.PullReqFilter, error) { return nil, fmt.Errorf("encountered error parsing include description filter: %w", err) } + authorID, err := QueryParamAsPositiveInt64OrDefault(r, QueryParamAuthorID, 0) + if err != nil { + return nil, fmt.Errorf("encountered error parsing author ID filter: %w", err) + } + + commenterID, err := QueryParamAsPositiveInt64OrDefault(r, QueryParamCommenterID, 0) + if err != nil { + return nil, fmt.Errorf("encountered error parsing commenter ID filter: %w", err) + } + + reviewerID, err := QueryParamAsPositiveInt64OrDefault(r, QueryParamReviewerID, 0) + if err != nil { + return nil, fmt.Errorf("encountered error parsing reviewer ID filter: %w", err) + } + + reviewDecisions := parseReviewDecisions(r) + if len(reviewDecisions) > 0 && reviewerID <= 0 { + return nil, errors.InvalidArgument("Can't use review decisions without providing a reviewer ID") + } + return &types.PullReqFilter{ Page: ParsePage(r), Size: ParseLimit(r), @@ -114,6 +157,10 @@ func ParsePullReqFilter(r *http.Request) (*types.PullReqFilter, error) { Order: ParseOrder(r), LabelID: labelID, ValueID: valueID, + AuthorID: authorID, + CommenterID: commenterID, + ReviewerID: reviewerID, + ReviewDecisions: reviewDecisions, IncludeDescription: includeDescription, CreatedFilter: createdAtFilter, EditedFilter: editedAtFilter, diff --git a/app/store/database/pullreq.go b/app/store/database/pullreq.go index 67832e4c1..f64a9375d 100644 --- a/app/store/database/pullreq.go +++ b/app/store/database/pullreq.go @@ -547,7 +547,7 @@ func (s *PullReqStore) listQuery(opts *types.PullReqFilter) squirrel.SelectBuild columns = pullReqColumns } - if len(opts.LabelID) > 0 || len(opts.ValueID) > 0 { + if len(opts.LabelID) > 0 || len(opts.ValueID) > 0 || opts.CommenterID > 0 { stmt = database.Builder.Select("DISTINCT " + columns) } else { stmt = database.Builder.Select(columns) @@ -621,6 +621,26 @@ func (*PullReqStore) applyFilter(stmt *squirrel.SelectBuilder, opts *types.PullR *stmt = stmt.Where(squirrel.NotEq{"pullreq_target_repo_id": opts.RepoIDBlacklist}) } + if opts.AuthorID > 0 { + *stmt = stmt.Where("pullreq_created_by = ?", opts.AuthorID) + } + + if opts.CommenterID > 0 { + *stmt = stmt.InnerJoin("pullreq_activities ON pullreq_activity_pullreq_id = pullreq_id") + *stmt = stmt.Where("pullreq_activity_deleted IS NULL") + *stmt = stmt.Where("(pullreq_activity_kind = 'comment' OR pullreq_activity_kind = 'change-comment')") + *stmt = stmt.Where("pullreq_activity_created_by = ?", opts.CommenterID) + } + + if opts.ReviewerID > 0 { + *stmt = stmt.InnerJoin( + fmt.Sprintf("pullreq_reviewers ON "+ + "pullreq_reviewer_pullreq_id = pullreq_id AND pullreq_reviewer_principal_id = %d", opts.ReviewerID)) + if len(opts.ReviewDecisions) > 0 { + *stmt = stmt.Where(squirrel.Eq{"pullreq_reviewer_review_decision": opts.ReviewDecisions}) + } + } + // labels if len(opts.LabelID) == 0 && len(opts.ValueID) == 0 { diff --git a/types/pullreq.go b/types/pullreq.go index 61ed4438e..715a97419 100644 --- a/types/pullreq.go +++ b/types/pullreq.go @@ -94,21 +94,25 @@ type PullReqStats struct { // PullReqFilter stores pull request query parameters. type PullReqFilter struct { - Page int `json:"page"` - Size int `json:"size"` - Query string `json:"query"` - CreatedBy []int64 `json:"created_by"` - SourceRepoID int64 `json:"-"` // caller should use source_repo_ref - SourceRepoRef string `json:"source_repo_ref"` - SourceBranch string `json:"source_branch"` - TargetRepoID int64 `json:"-"` - TargetBranch string `json:"target_branch"` - States []enum.PullReqState `json:"state"` - Sort enum.PullReqSort `json:"sort"` - Order enum.Order `json:"order"` - LabelID []int64 `json:"label_id"` - ValueID []int64 `json:"value_id"` - IncludeDescription bool `json:"include_description"` + Page int `json:"page"` + Size int `json:"size"` + Query string `json:"query"` + CreatedBy []int64 `json:"created_by"` + SourceRepoID int64 `json:"-"` // caller should use source_repo_ref + SourceRepoRef string `json:"source_repo_ref"` + SourceBranch string `json:"source_branch"` + TargetRepoID int64 `json:"-"` + TargetBranch string `json:"target_branch"` + States []enum.PullReqState `json:"state"` + Sort enum.PullReqSort `json:"sort"` + Order enum.Order `json:"order"` + LabelID []int64 `json:"label_id"` + ValueID []int64 `json:"value_id"` + AuthorID int64 `json:"author_id"` + CommenterID int64 `json:"commenter_id"` + ReviewerID int64 `json:"reviewer_id"` + ReviewDecisions []enum.PullReqReviewDecision `json:"review_decisions"` + IncludeDescription bool `json:"include_description"` CreatedFilter EditedFilter