add user filters to the pull request list API (#2624)

* add user filters to the pull request list API
pull/3558/head
Marko Gaćeša 2024-09-02 09:00:50 +00:00 committed by Harness
parent ccf267f1cc
commit a2d2ba07aa
5 changed files with 156 additions and 18 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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 {

View File

@ -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