diff --git a/cli/server/harness.wire_gen.go b/cli/server/harness.wire_gen.go index 1dd975624..25ae0a1a5 100644 --- a/cli/server/harness.wire_gen.go +++ b/cli/server/harness.wire_gen.go @@ -98,7 +98,8 @@ func initSystem(ctx context.Context, config *types.Config) (*system, error) { } repoController := repo.ProvideController(config, checkRepo, authorizer, spaceStore, repoStore, serviceAccountStore, gitrpcInterface) pullReqStore := database.ProvidePullReqStore(db) - pullreqController := pullreq.ProvideController(db, authorizer, pullReqStore, repoStore, serviceAccountStore, gitrpcInterface) + pullReqActivityStore := database.ProvidePullReqActivityStore(db) + pullreqController := pullreq.ProvideController(db, authorizer, pullReqStore, pullReqActivityStore, repoStore, serviceAccountStore, gitrpcInterface) apiHandler := router.ProvideAPIHandler(config, authenticator, accountClient, spaceController, repoController, pullreqController) gitHandler := router.ProvideGitHandler(config, repoStore, authenticator, authorizer, gitrpcInterface) webHandler := router2.ProvideWebHandler(config) diff --git a/cli/server/standalone.wire_gen.go b/cli/server/standalone.wire_gen.go index e4ad5122e..a6d92471c 100644 --- a/cli/server/standalone.wire_gen.go +++ b/cli/server/standalone.wire_gen.go @@ -58,7 +58,8 @@ func initSystem(ctx context.Context, config *types.Config) (*system, error) { checkSpace := check.ProvideSpaceCheck() spaceController := space.ProvideController(config, checkSpace, authorizer, spaceStore, repoStore, serviceAccountStore) pullReqStore := database.ProvidePullReqStore(db) - pullreqController := pullreq.ProvideController(db, authorizer, pullReqStore, repoStore, serviceAccountStore, gitrpcInterface) + pullReqActivityStore := database.ProvidePullReqActivityStore(db) + pullreqController := pullreq.ProvideController(db, authorizer, pullReqStore, pullReqActivityStore, repoStore, serviceAccountStore, gitrpcInterface) serviceAccount := check.ProvideServiceAccountCheck() serviceaccountController := serviceaccount.NewController(serviceAccount, authorizer, serviceAccountStore, spaceStore, repoStore, tokenStore) apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, spaceController, pullreqController, serviceaccountController, controller) diff --git a/internal/api/controller/pullreq/controller.go b/internal/api/controller/pullreq/controller.go index 7b28dc18d..860f4c698 100644 --- a/internal/api/controller/pullreq/controller.go +++ b/internal/api/controller/pullreq/controller.go @@ -19,32 +19,36 @@ import ( "github.com/harness/gitness/types/enum" "github.com/jmoiron/sqlx" + "github.com/rs/zerolog/log" ) type Controller struct { - db *sqlx.DB - authorizer authz.Authorizer - pullreqStore store.PullReqStore - repoStore store.RepoStore - saStore store.ServiceAccountStore - gitRPCClient gitrpc.Interface + db *sqlx.DB + authorizer authz.Authorizer + pullreqStore store.PullReqStore + pullreqActivityStore store.PullReqActivityStore + repoStore store.RepoStore + saStore store.ServiceAccountStore + gitRPCClient gitrpc.Interface } func NewController( db *sqlx.DB, authorizer authz.Authorizer, pullreqStore store.PullReqStore, + pullreqActivityStore store.PullReqActivityStore, repoStore store.RepoStore, saStore store.ServiceAccountStore, gitRPCClient gitrpc.Interface, ) *Controller { return &Controller{ - db: db, - authorizer: authorizer, - pullreqStore: pullreqStore, - repoStore: repoStore, - saStore: saStore, - gitRPCClient: gitRPCClient, + db: db, + authorizer: authorizer, + pullreqStore: pullreqStore, + pullreqActivityStore: pullreqActivityStore, + repoStore: repoStore, + saStore: saStore, + gitRPCClient: gitRPCClient, } } @@ -85,3 +89,24 @@ func (c *Controller) getRepoCheckAccess(ctx context.Context, return repo, nil } + +func (c *Controller) writeActivity(ctx context.Context, + pr *types.PullReq, act *types.PullReqActivity) (*types.PullReq, *types.PullReqActivity) { + prUpd, err := c.pullreqStore.UpdateActivitySeq(ctx, pr) + if err != nil { + // non-critical error + log.Err(err).Msg("failed to get pull request activity number") + return pr, nil + } + + act.Order = prUpd.ActivitySeq + + err = c.pullreqActivityStore.Create(ctx, act) + if err != nil { + // non-critical error + log.Err(err).Msg("failed to create pull request activity") + return prUpd, nil + } + + return prUpd, act +} diff --git a/internal/api/controller/pullreq/create.go b/internal/api/controller/pullreq/create.go index 54d317f86..279d7ae58 100644 --- a/internal/api/controller/pullreq/create.go +++ b/internal/api/controller/pullreq/create.go @@ -103,24 +103,24 @@ func newPullReq(session *auth.Session, number int64, sourceRepo, targetRepo *types.Repository, in *CreateInput) *types.PullReq { now := time.Now().UnixMilli() return &types.PullReq{ - ID: 0, // the ID will be populated in the data layer - Version: 0, - Number: number, - CreatedBy: session.Principal.ID, - Created: now, - Updated: now, - Edited: now, - State: enum.PullReqStateOpen, - Title: in.Title, - Description: in.Description, - SourceRepoID: sourceRepo.ID, - SourceBranch: in.SourceBranch, - TargetRepoID: targetRepo.ID, - TargetBranch: in.TargetBranch, - PullReqActivitySeq: 0, - MergedBy: nil, - Merged: nil, - MergeStrategy: nil, + ID: 0, // the ID will be populated in the data layer + Version: 0, + Number: number, + CreatedBy: session.Principal.ID, + Created: now, + Updated: now, + Edited: now, + State: enum.PullReqStateOpen, + Title: in.Title, + Description: in.Description, + SourceRepoID: sourceRepo.ID, + SourceBranch: in.SourceBranch, + TargetRepoID: targetRepo.ID, + TargetBranch: in.TargetBranch, + ActivitySeq: 0, + MergedBy: nil, + Merged: nil, + MergeStrategy: nil, Author: types.PrincipalInfo{ ID: session.Principal.ID, UID: session.Principal.UID, diff --git a/internal/api/controller/pullreq/list_activities.go b/internal/api/controller/pullreq/list_activities.go new file mode 100644 index 000000000..cde823d47 --- /dev/null +++ b/internal/api/controller/pullreq/list_activities.go @@ -0,0 +1,50 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pullreq + +import ( + "context" + "fmt" + + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// ListActivities returns a list of pull request activities +// from the provided repository and pull request number. +func (c *Controller) ListActivities( + ctx context.Context, + session *auth.Session, + repoRef string, + prNum int64, + filter *types.PullReqActivityFilter, +) ([]*types.PullReqActivity, int64, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) + if err != nil { + return nil, 0, fmt.Errorf("failed to acquire access to repo: %w", err) + } + + pr, err := c.pullreqStore.FindByNumber(ctx, repo.ID, prNum) + if err != nil { + return nil, 0, fmt.Errorf("failed to find pull request by number: %w", err) + } + + list, err := c.pullreqActivityStore.List(ctx, pr.ID, filter) + if err != nil { + return nil, 0, fmt.Errorf("failed to list pull requests activities: %w", err) + } + + if filter.Limit == 0 { + return list, int64(len(list)), nil + } + + count, err := c.pullreqActivityStore.Count(ctx, pr.ID, filter) + if err != nil { + return nil, 0, fmt.Errorf("failed to count pull request activities: %w", err) + } + + return list, count, nil +} diff --git a/internal/api/controller/pullreq/update.go b/internal/api/controller/pullreq/update.go index b4d5fec0a..1576ded13 100644 --- a/internal/api/controller/pullreq/update.go +++ b/internal/api/controller/pullreq/update.go @@ -24,6 +24,8 @@ type UpdateInput struct { } // Update updates an pull request. +// +//nolint:gocognit func (c *Controller) Update(ctx context.Context, session *auth.Session, repoRef string, pullreqNum int64, in *UpdateInput) (*types.PullReq, error) { var pr *types.PullReq @@ -38,6 +40,8 @@ func (c *Controller) Update(ctx context.Context, return nil, fmt.Errorf("failed to acquire access to target repo: %w", err) } + var activity *types.PullReqActivity + err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) error { pr, err = c.pullreqStore.FindByNumber(ctx, targetRepo.ID, pullreqNum) if err != nil { @@ -62,6 +66,8 @@ func (c *Controller) Update(ctx context.Context, return nil } + activity = getUpdateActivity(session, pr, in) + pr.Title = in.Title pr.Description = in.Description pr.Edited = time.Now().UnixMilli() @@ -77,7 +83,45 @@ func (c *Controller) Update(ctx context.Context, return nil, err } - // TODO: Write a row to the pull request activity + // Write a row to the pull request activity + if activity != nil { + pr, activity = c.writeActivity(ctx, pr, activity) + } return pr, nil } + +func getUpdateActivity(session *auth.Session, pr *types.PullReq, in *UpdateInput) *types.PullReqActivity { + if pr.Title == in.Title { + return nil + } + + now := time.Now().UnixMilli() + + act := &types.PullReqActivity{ + ID: 0, // Will be populated in the data layer + Version: 0, + CreatedBy: session.Principal.ID, + Created: now, + Updated: now, + Edited: now, + Deleted: nil, + RepoID: pr.TargetRepoID, + PullReqID: pr.ID, + Order: 0, // Will be filled in writeActivity + SubOrder: 0, + ReplySeq: 0, + Type: enum.PullReqActivityTypeTitleChange, + Kind: enum.PullReqActivityKindSystem, + Text: "", + Payload: map[string]interface{}{ + "old": pr.Title, + "new": in.Title, + }, + Metadata: nil, + ResolvedBy: nil, + Resolved: nil, + } + + return act +} diff --git a/internal/api/controller/pullreq/wire.go b/internal/api/controller/pullreq/wire.go index 26b5e6d7e..97af42019 100644 --- a/internal/api/controller/pullreq/wire.go +++ b/internal/api/controller/pullreq/wire.go @@ -19,9 +19,10 @@ var WireSet = wire.NewSet( ) func ProvideController(db *sqlx.DB, authorizer authz.Authorizer, - pullreqStore store.PullReqStore, repoStore store.RepoStore, saStore store.ServiceAccountStore, + pullReqStore store.PullReqStore, pullReqActivityStore store.PullReqActivityStore, + repoStore store.RepoStore, saStore store.ServiceAccountStore, rpcClient gitrpc.Interface) *Controller { return NewController(db, authorizer, - pullreqStore, repoStore, saStore, + pullReqStore, pullReqActivityStore, repoStore, saStore, rpcClient) } diff --git a/internal/api/handler/pullreq/list_activities.go b/internal/api/handler/pullreq/list_activities.go new file mode 100644 index 000000000..7f5260a9b --- /dev/null +++ b/internal/api/handler/pullreq/list_activities.go @@ -0,0 +1,48 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pullreq + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/pullreq" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +// HandleListActivities returns a http.HandlerFunc that lists pull request activities for a pull request. +func HandleListActivities(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 + } + + filter, err := request.ParsePullReqActivityFilter(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + list, total, err := pullreqCtrl.ListActivities(ctx, session, repoRef, pullreqNumber, filter) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.PaginationLimit(r, w, int(total)) + render.JSON(w, http.StatusOK, list) + } +} diff --git a/internal/api/openapi/pullreq.go b/internal/api/openapi/pullreq.go index 60a80c8af..3b8270690 100644 --- a/internal/api/openapi/pullreq.go +++ b/internal/api/openapi/pullreq.go @@ -40,6 +40,10 @@ type updatePullReqRequest struct { pullreq.UpdateInput } +type listPullReqActivitiesRequest struct { + pullReqRequest +} + var queryParameterQueryPullRequest = openapi3.ParameterOrRef{ Parameter: &openapi3.Parameter{ Name: request.QueryParamQuery, @@ -156,6 +160,55 @@ var queryParameterSortPullRequest = openapi3.ParameterOrRef{ }, } +var queryParameterKindPullRequestActivity = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamKind, + In: openapi3.ParameterInQuery, + Description: ptr.String("The kind of the pull request activity to include in the result."), + 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: []interface{}{ + ptr.String(string(enum.PullReqActivityKindSystem)), + ptr.String(string(enum.PullReqActivityKindComment)), + ptr.String(string(enum.PullReqActivityKindCodeComment)), + }, + }, + }, + }, + }, + }, +} + +var queryParameterTypePullRequestActivity = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamType, + In: openapi3.ParameterInQuery, + Description: ptr.String("The type of the pull request activity to include in the result."), + 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: []interface{}{ + ptr.String(string(enum.PullReqActivityTypeComment)), + ptr.String(string(enum.PullReqActivityTypeCodeComment)), + ptr.String(string(enum.PullReqActivityTypeTitleChange)), + }, + }, + }, + }, + }, + }, +} + +//nolint:funlen func pullReqOperations(reflector *openapi3.Reflector) { createPullReq := openapi3.Operation{} createPullReq.WithTags("pullreq") @@ -206,4 +259,19 @@ func pullReqOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&putPullReq, new(usererror.Error), http.StatusUnauthorized) _ = reflector.SetJSONResponse(&putPullReq, new(usererror.Error), http.StatusForbidden) _ = reflector.Spec.AddOperation(http.MethodPut, "/repos/{repoRef}/pullreq/{pullreq_number}", putPullReq) + + listPullReqActivities := openapi3.Operation{} + listPullReqActivities.WithTags("pullreq") + listPullReqActivities.WithMapOfAnything(map[string]interface{}{"operationId": "listPullReqActivities"}) + listPullReqActivities.WithParameters( + queryParameterKindPullRequestActivity, queryParameterTypePullRequestActivity, + queryParameterSince, queryParameterUntil, queryParameterLimit) + _ = reflector.SetRequest(&listPullReqActivities, new(listPullReqActivitiesRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&listPullReqActivities, new([]types.PullReqActivity), http.StatusOK) + _ = reflector.SetJSONResponse(&listPullReqActivities, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&listPullReqActivities, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&listPullReqActivities, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&listPullReqActivities, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodGet, + "/repos/{repoRef}/pullreq/{pullreq_number}/activities", listPullReqActivities) } diff --git a/internal/api/openapi/shared.go b/internal/api/openapi/shared.go index eb7e05b23..94a710a5e 100644 --- a/internal/api/openapi/shared.go +++ b/internal/api/openapi/shared.go @@ -71,3 +71,48 @@ var queryParameterDirection = openapi3.ParameterOrRef{ }, }, } + +var queryParameterLimit = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamLimit, + In: openapi3.ParameterInQuery, + Description: ptr.String("The maximum number of results to return."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeInteger), + Minimum: ptr.Float64(1), + }, + }, + }, +} + +var queryParameterSince = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamSince, + In: openapi3.ParameterInQuery, + Description: ptr.String("The result should contain only entries created at and after this timestamp (unix millis)."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeInteger), + Minimum: ptr.Float64(0), + }, + }, + }, +} + +var queryParameterUntil = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamUntil, + In: openapi3.ParameterInQuery, + Description: ptr.String("The result should contain only entries created before this timestamp (unix millis)."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeInteger), + Minimum: ptr.Float64(0), + }, + }, + }, +} diff --git a/internal/api/render/header.go b/internal/api/render/header.go index 6402c1d64..b9ecd5479 100644 --- a/internal/api/render/header.go +++ b/internal/api/render/header.go @@ -73,6 +73,11 @@ func PaginationNoTotal(r *http.Request, w http.ResponseWriter, page int, size in } } +// PaginationLimit writes the x-total header. +func PaginationLimit(r *http.Request, w http.ResponseWriter, total int) { + w.Header().Set("x-total", strconv.Itoa(total)) +} + func getPaginationBaseURL(r *http.Request, page int, size int) url.URL { uri := *r.URL diff --git a/internal/api/request/pullreq.go b/internal/api/request/pullreq.go index f787b21c5..2bee09884 100644 --- a/internal/api/request/pullreq.go +++ b/internal/api/request/pullreq.go @@ -26,8 +26,8 @@ func ParseSortPullReq(r *http.Request) enum.PullReqSort { ) } -// ParsePullReqStates extracts the pull request states the url. -func ParsePullReqStates(r *http.Request) []enum.PullReqState { +// parsePullReqStates extracts the pull request states from the url. +func parsePullReqStates(r *http.Request) []enum.PullReqState { strStates := r.Form[QueryParamState] m := make(map[enum.PullReqState]struct{}) // use map to eliminate duplicates for _, s := range strStates { @@ -65,8 +65,79 @@ func ParsePullReqFilter(r *http.Request) (*types.PullReqFilter, error) { SourceRepoRef: r.FormValue("source_repo_ref"), SourceBranch: r.FormValue("source_branch"), TargetBranch: r.FormValue("target_branch"), - States: ParsePullReqStates(r), + States: parsePullReqStates(r), Sort: ParseSortPullReq(r), Order: ParseOrder(r), }, nil } + +// ParsePullReqActivityFilter extracts the pull request activity query parameter from the url. +func ParsePullReqActivityFilter(r *http.Request) (*types.PullReqActivityFilter, error) { + since, err := QueryParamAsPositiveInt64(r, QueryParamSince) + if err != nil { + return nil, err + } + until, err := QueryParamAsPositiveInt64(r, QueryParamUntil) + if err != nil { + return nil, err + } + limit, err := QueryParamAsPositiveInt64(r, QueryParamLimit) + if err != nil { + return nil, err + } + return &types.PullReqActivityFilter{ + Since: since, + Until: until, + Limit: int(limit), + Types: parsePullReqActivityTypes(r), + Kinds: parsePullReqActivityKinds(r), + }, nil +} + +// parsePullReqActivityKinds extracts the pull request activity kinds from the url. +func parsePullReqActivityKinds(r *http.Request) []enum.PullReqActivityKind { + strKinds := r.Form[QueryParamKind] + m := make(map[enum.PullReqActivityKind]struct{}) // use map to eliminate duplicates + for _, s := range strKinds { + kind, ok := enum.ParsePullReqActivityKind(s) + if !ok { + continue + } + m[kind] = struct{}{} + } + + if len(m) == 0 { + return nil + } + + kinds := make([]enum.PullReqActivityKind, 0, len(m)) + for k := range m { + kinds = append(kinds, k) + } + + return kinds +} + +// parsePullReqActivityTypes extracts the pull request activity types from the url. +func parsePullReqActivityTypes(r *http.Request) []enum.PullReqActivityType { + strType := r.Form[QueryParamType] + m := make(map[enum.PullReqActivityType]struct{}) // use map to eliminate duplicates + for _, s := range strType { + t, ok := enum.ParsePullReqActivityType(s) + if !ok { + continue + } + m[t] = struct{}{} + } + + if len(m) == 0 { + return nil + } + + activityTypes := make([]enum.PullReqActivityType, 0, len(m)) + for t := range m { + activityTypes = append(activityTypes, t) + } + + return activityTypes +} diff --git a/internal/api/request/util.go b/internal/api/request/util.go index b6a33c1f6..a83a25b28 100644 --- a/internal/api/request/util.go +++ b/internal/api/request/util.go @@ -26,7 +26,13 @@ const ( QueryParamCreatedBy = "created_by" QueryParamState = "state" + QueryParamKind = "kind" + QueryParamType = "type" + QueryParamSince = "since" + QueryParamUntil = "until" + + QueryParamLimit = "limit" QueryParamPage = "page" QueryParamPerPage = "per_page" PerPageDefault = 50 @@ -83,6 +89,26 @@ func QueryParamOrError(r *http.Request, paramName string) (string, error) { // QueryParamAsID extracts an ID parameter from the url. func QueryParamAsID(r *http.Request, paramName string) (int64, error) { + return QueryParamAsPositiveInt64(r, paramName) +} + +// QueryParamAsInt64 extracts an integer parameter from the url. +func QueryParamAsInt64(r *http.Request, paramName string) (int64, error) { + value, ok := QueryParam(r, paramName) + if !ok { + return 0, nil + } + + valueInt, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return 0, usererror.BadRequest(fmt.Sprintf("Parameter '%s' must be an integer.", paramName)) + } + + return valueInt, nil +} + +// QueryParamAsPositiveInt64 extracts an integer parameter from the url. +func QueryParamAsPositiveInt64(r *http.Request, paramName string) (int64, error) { value, ok := QueryParam(r, paramName) if !ok { return 0, nil diff --git a/internal/router/api.go b/internal/router/api.go index 31a926ee6..4115bf59c 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -208,6 +208,7 @@ func setupPullReq(r chi.Router, pullreqCtrl *pullreq.Controller) { r.Route(fmt.Sprintf("/{%s}", request.PathParamPullReqNumber), func(r chi.Router) { r.Get("/", handlerpullreq.HandleFind(pullreqCtrl)) r.Put("/", handlerpullreq.HandleUpdate(pullreqCtrl)) + r.Get("/activities", handlerpullreq.HandleListActivities(pullreqCtrl)) }) }) } diff --git a/internal/store/database/migrate/postgres/0005_create_table_pullreq_activities.up.sql b/internal/store/database/migrate/postgres/0005_create_table_pullreq_activities.up.sql new file mode 100644 index 000000000..4e3d02124 --- /dev/null +++ b/internal/store/database/migrate/postgres/0005_create_table_pullreq_activities.up.sql @@ -0,0 +1,37 @@ +CREATE TABLE pullreq_activities ( + pullreq_activity_id SERIAL PRIMARY KEY +,pullreq_activity_version BIGINT NOT NULL +,pullreq_activity_created_by INTEGER +,pullreq_activity_created BIGINT NOT NULL +,pullreq_activity_updated BIGINT NOT NULL +,pullreq_activity_edited BIGINT NOT NULL +,pullreq_activity_deleted BIGINT +,pullreq_activity_repo_id INTEGER NOT NULL +,pullreq_activity_pullreq_id INTEGER NOT NULL +,pullreq_activity_order INTEGER NOT NULL +,pullreq_activity_sub_order INTEGER NOT NULL +,pullreq_activity_reply_seq INTEGER NOT NULL +,pullreq_activity_type TEXT NOT NULL +,pullreq_activity_kind TEXT NOT NULL +,pullreq_activity_text TEXT NOT NULL +,pullreq_activity_payload JSONB NOT NULL DEFAULT '{}' +,pullreq_activity_metadata JSONB NOT NULL DEFAULT '{}' +,pullreq_activity_resolved_by INTEGER DEFAULT 0 +,pullreq_activity_resolved BIGINT NULL +,CONSTRAINT fk_pullreq_activities_created_by FOREIGN KEY (pullreq_activity_created_by) + REFERENCES principals (principal_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +,CONSTRAINT fk_pullreq_activities_repo_id FOREIGN KEY (pullreq_activity_repo_id) + REFERENCES repositories (repo_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +,CONSTRAINT fk_pullreq_activities_pullreq_id FOREIGN KEY (pullreq_activity_pullreq_id) + REFERENCES pullreq (pullreq_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +,CONSTRAINT fk_pullreq_activities_resolved_by FOREIGN KEY (pullreq_activity_resolved_by) + REFERENCES principals (principal_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +); diff --git a/internal/store/database/migrate/postgres/0006_create_index_pullreq_activities_pullreq_id_order.up.sql b/internal/store/database/migrate/postgres/0006_create_index_pullreq_activities_pullreq_id_order.up.sql new file mode 100644 index 000000000..b73cb340d --- /dev/null +++ b/internal/store/database/migrate/postgres/0006_create_index_pullreq_activities_pullreq_id_order.up.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX index_pullreq_activities_pullreq_id_order_sub_order +ON pullreq_activities(pullreq_activity_pullreq_id, pullreq_activity_order, pullreq_activity_sub_order); diff --git a/internal/store/database/migrate/sqlite/0005_create_table_pullreq_activities.up.sql b/internal/store/database/migrate/sqlite/0005_create_table_pullreq_activities.up.sql new file mode 100644 index 000000000..e2848f563 --- /dev/null +++ b/internal/store/database/migrate/sqlite/0005_create_table_pullreq_activities.up.sql @@ -0,0 +1,37 @@ +CREATE TABLE pullreq_activities ( + pullreq_activity_id INTEGER PRIMARY KEY AUTOINCREMENT +,pullreq_activity_version BIGINT NOT NULL +,pullreq_activity_created_by INTEGER +,pullreq_activity_created BIGINT NOT NULL +,pullreq_activity_updated BIGINT NOT NULL +,pullreq_activity_edited BIGINT NOT NULL +,pullreq_activity_deleted BIGINT +,pullreq_activity_repo_id INTEGER NOT NULL +,pullreq_activity_pullreq_id INTEGER NOT NULL +,pullreq_activity_order INTEGER NOT NULL +,pullreq_activity_sub_order INTEGER NOT NULL +,pullreq_activity_reply_seq INTEGER NOT NULL +,pullreq_activity_type TEXT NOT NULL +,pullreq_activity_kind TEXT NOT NULL +,pullreq_activity_text TEXT NOT NULL +,pullreq_activity_payload TEXT NOT NULL DEFAULT '{}' +,pullreq_activity_metadata TEXT NOT NULL DEFAULT '{}' +,pullreq_activity_resolved_by INTEGER DEFAULT 0 +,pullreq_activity_resolved BIGINT NULL +,CONSTRAINT fk_pullreq_activities_created_by FOREIGN KEY (pullreq_activity_created_by) + REFERENCES principals (principal_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +,CONSTRAINT fk_pullreq_activities_repo_id FOREIGN KEY (pullreq_activity_repo_id) + REFERENCES repositories (repo_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +,CONSTRAINT fk_pullreq_activities_pullreq_id FOREIGN KEY (pullreq_activity_pullreq_id) + REFERENCES pullreq (pullreq_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +,CONSTRAINT fk_pullreq_activities_resolved_by FOREIGN KEY (pullreq_activity_resolved_by) + REFERENCES principals (principal_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION +); diff --git a/internal/store/database/migrate/sqlite/0006_create_index_pullreq_activities_pullreq_id_order.up.sql b/internal/store/database/migrate/sqlite/0006_create_index_pullreq_activities_pullreq_id_order.up.sql new file mode 100644 index 000000000..b73cb340d --- /dev/null +++ b/internal/store/database/migrate/sqlite/0006_create_index_pullreq_activities_pullreq_id_order.up.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX index_pullreq_activities_pullreq_id_order_sub_order +ON pullreq_activities(pullreq_activity_pullreq_id, pullreq_activity_order, pullreq_activity_sub_order); diff --git a/internal/store/database/pullreq.go b/internal/store/database/pullreq.go index 2f5c9cf03..3a798e74b 100644 --- a/internal/store/database/pullreq.go +++ b/internal/store/database/pullreq.go @@ -55,7 +55,7 @@ type pullReq struct { TargetRepoID int64 `db:"pullreq_target_repo_id"` TargetBranch string `db:"pullreq_target_branch"` - PullReqActivitySeq int64 `db:"pullreq_activity_seq"` + ActivitySeq int64 `db:"pullreq_activity_seq"` MergedBy null.Int `db:"pullreq_merged_by"` Merged null.Int `db:"pullreq_merged"` @@ -238,6 +238,27 @@ func (s *PullReqStore) Update(ctx context.Context, pr *types.PullReq) error { return nil } +// UpdateActivitySeq updates the pull request's activity sequence. +func (s *PullReqStore) UpdateActivitySeq(ctx context.Context, pr *types.PullReq) (*types.PullReq, error) { + for { + dup := *pr + + dup.ActivitySeq++ + err := s.Update(ctx, &dup) + if err == nil { + return &dup, nil + } + if !errors.Is(err, store.ErrConflict) { + return nil, err + } + + pr, err = s.Find(ctx, pr.ID) + if err != nil { + return nil, err + } + } +} + // Delete the pull request. func (s *PullReqStore) Delete(ctx context.Context, id int64) error { const pullReqDelete = `DELETE FROM pullreqs WHERE pullreq_id = $1` @@ -383,26 +404,26 @@ func (s *PullReqStore) List(ctx context.Context, repoID int64, opts *types.PullR func mapPullReq(pr *pullReq) *types.PullReq { m := &types.PullReq{ - ID: pr.ID, - Version: pr.Version, - Number: pr.Number, - CreatedBy: pr.CreatedBy, - Created: pr.Created, - Updated: pr.Updated, - Edited: pr.Edited, - State: pr.State, - Title: pr.Title, - Description: pr.Description, - SourceRepoID: pr.SourceRepoID, - SourceBranch: pr.SourceBranch, - TargetRepoID: pr.TargetRepoID, - TargetBranch: pr.TargetBranch, - PullReqActivitySeq: pr.PullReqActivitySeq, - MergedBy: pr.MergedBy.Ptr(), - Merged: pr.Merged.Ptr(), - MergeStrategy: pr.MergeStrategy.Ptr(), - Author: types.PrincipalInfo{}, - Merger: nil, + ID: pr.ID, + Version: pr.Version, + Number: pr.Number, + CreatedBy: pr.CreatedBy, + Created: pr.Created, + Updated: pr.Updated, + Edited: pr.Edited, + State: pr.State, + Title: pr.Title, + Description: pr.Description, + SourceRepoID: pr.SourceRepoID, + SourceBranch: pr.SourceBranch, + TargetRepoID: pr.TargetRepoID, + TargetBranch: pr.TargetBranch, + ActivitySeq: pr.ActivitySeq, + MergedBy: pr.MergedBy.Ptr(), + Merged: pr.Merged.Ptr(), + MergeStrategy: pr.MergeStrategy.Ptr(), + Author: types.PrincipalInfo{}, + Merger: nil, } m.Author = types.PrincipalInfo{ ID: pr.CreatedBy, @@ -424,24 +445,24 @@ func mapPullReq(pr *pullReq) *types.PullReq { func mapInternalPullReq(pr *types.PullReq) *pullReq { m := &pullReq{ - ID: pr.ID, - Version: pr.Version, - Number: pr.Number, - CreatedBy: pr.CreatedBy, - Created: pr.Created, - Updated: pr.Updated, - Edited: pr.Edited, - State: pr.State, - Title: pr.Title, - Description: pr.Description, - SourceRepoID: pr.SourceRepoID, - SourceBranch: pr.SourceBranch, - TargetRepoID: pr.TargetRepoID, - TargetBranch: pr.TargetBranch, - PullReqActivitySeq: pr.PullReqActivitySeq, - MergedBy: null.IntFromPtr(pr.MergedBy), - Merged: null.IntFromPtr(pr.Merged), - MergeStrategy: null.StringFromPtr(pr.MergeStrategy), + ID: pr.ID, + Version: pr.Version, + Number: pr.Number, + CreatedBy: pr.CreatedBy, + Created: pr.Created, + Updated: pr.Updated, + Edited: pr.Edited, + State: pr.State, + Title: pr.Title, + Description: pr.Description, + SourceRepoID: pr.SourceRepoID, + SourceBranch: pr.SourceBranch, + TargetRepoID: pr.TargetRepoID, + TargetBranch: pr.TargetBranch, + ActivitySeq: pr.ActivitySeq, + MergedBy: null.IntFromPtr(pr.MergedBy), + Merged: null.IntFromPtr(pr.Merged), + MergeStrategy: null.StringFromPtr(pr.MergeStrategy), } return m diff --git a/internal/store/database/pullreq_activity.go b/internal/store/database/pullreq_activity.go new file mode 100644 index 000000000..bd899c0cf --- /dev/null +++ b/internal/store/database/pullreq_activity.go @@ -0,0 +1,429 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + "encoding/json" + "time" + + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/internal/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + + "github.com/Masterminds/squirrel" + "github.com/guregu/null" + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +var _ store.PullReqActivityStore = (*PullReqActivityStore)(nil) + +// NewPullReqActivityStore returns a new PullReqJournalStore. +func NewPullReqActivityStore(db *sqlx.DB) *PullReqActivityStore { + return &PullReqActivityStore{ + db: db, + } +} + +// PullReqActivityStore implements store.PullReqActivityStore backed by a relational database. +type PullReqActivityStore struct { + db *sqlx.DB +} + +// journal is used to fetch pull request data from the database. +// The object should be later re-packed into a different struct to return it as an API response. +type pullReqActivity struct { + ID int64 `db:"pullreq_activity_id"` + Version int64 `db:"pullreq_activity_version"` + + CreatedBy int64 `db:"pullreq_activity_created_by"` + Created int64 `db:"pullreq_activity_created"` + Updated int64 `db:"pullreq_activity_updated"` + Edited int64 `db:"pullreq_activity_edited"` + Deleted null.Int `db:"pullreq_activity_deleted"` + + RepoID int64 `db:"pullreq_activity_repo_id"` + PullReqID int64 `db:"pullreq_activity_pullreq_id"` + + Order int64 `db:"pullreq_activity_order"` + SubOrder int64 `db:"pullreq_activity_sub_order"` + ReplySeq int64 `db:"pullreq_activity_reply_seq"` + + Type enum.PullReqActivityType `db:"pullreq_activity_type"` + Kind enum.PullReqActivityKind `db:"pullreq_activity_kind"` + + Text string `db:"pullreq_activity_text"` + Payload json.RawMessage `db:"pullreq_activity_payload"` + Metadata json.RawMessage `db:"pullreq_activity_metadata"` + + ResolvedBy null.Int `db:"pullreq_activity_resolved_by"` + Resolved null.Int `db:"pullreq_activity_resolved"` + + AuthorUID string `db:"author_uid"` + AuthorName string `db:"author_name"` + AuthorEmail string `db:"author_email"` + ResolverUID null.String `db:"resolver_uid"` + ResolverName null.String `db:"resolver_name"` + ResolverEmail null.String `db:"resolver_email"` +} + +const ( + pullreqActivityColumns = ` + pullreq_activity_id + ,pullreq_activity_version + ,pullreq_activity_created_by + ,pullreq_activity_created + ,pullreq_activity_updated + ,pullreq_activity_edited + ,pullreq_activity_deleted + ,pullreq_activity_repo_id + ,pullreq_activity_pullreq_id + ,pullreq_activity_order + ,pullreq_activity_sub_order + ,pullreq_activity_reply_seq + ,pullreq_activity_type + ,pullreq_activity_kind + ,pullreq_activity_text + ,pullreq_activity_payload + ,pullreq_activity_metadata + ,pullreq_activity_resolved_by + ,pullreq_activity_resolved + ,author.principal_uid as "author_uid" + ,author.principal_displayName as "author_name" + ,author.principal_email as "author_email" + ,resolver.principal_uid as "resolver_uid" + ,resolver.principal_displayName as "resolver_name" + ,resolver.principal_email as "resolver_email"` + + pullreqActivitySelectBase = ` + SELECT` + pullreqActivityColumns + ` + FROM pullreq_activities + INNER JOIN principals author on author.principal_id = pullreq_created_by + LEFT JOIN principals resolver on resolver.principal_id = journal_pullreq_merged_by` +) + +// Find finds the pull request activity by id. +func (s *PullReqActivityStore) Find(ctx context.Context, id int64) (*types.PullReqActivity, error) { + const sqlQuery = pullreqActivitySelectBase + ` + WHERE pullreq_activity_id = $1` + + db := dbtx.GetAccessor(ctx, s.db) + + dst := &pullReqActivity{} + if err := db.GetContext(ctx, dst, sqlQuery, id); err != nil { + return nil, processSQLErrorf(err, "Select query failed") + } + + return mapPullReqActivity(dst), nil +} + +// Create creates a new pull request. +func (s *PullReqActivityStore) Create(ctx context.Context, act *types.PullReqActivity) error { + const sqlQuery = ` + INSERT INTO pullreq_activities ( + pullreq_activity_version + ,pullreq_activity_created_by + ,pullreq_activity_created + ,pullreq_activity_updated + ,pullreq_activity_edited + ,pullreq_activity_deleted + ,pullreq_activity_repo_id + ,pullreq_activity_pullreq_id + ,pullreq_activity_order + ,pullreq_activity_sub_order + ,pullreq_activity_reply_seq + ,pullreq_activity_type + ,pullreq_activity_kind + ,pullreq_activity_text + ,pullreq_activity_payload + ,pullreq_activity_metadata + ,pullreq_activity_resolved_by + ,pullreq_activity_resolved + ) values ( + :pullreq_activity_version + ,:pullreq_activity_created_by + ,:pullreq_activity_created + ,:pullreq_activity_updated + ,:pullreq_activity_edited + ,:pullreq_activity_deleted + ,:pullreq_activity_repo_id + ,:pullreq_activity_pullreq_id + ,:pullreq_activity_order + ,:pullreq_activity_sub_order + ,:pullreq_activity_reply_seq + ,:pullreq_activity_type + ,:pullreq_activity_kind + ,:pullreq_activity_text + ,:pullreq_activity_payload + ,:pullreq_activity_metadata + ,:pullreq_activity_resolved_by + ,:pullreq_activity_resolved + ) RETURNING pullreq_activity_id` + + db := dbtx.GetAccessor(ctx, s.db) + + query, arg, err := db.BindNamed(sqlQuery, mapInternalPullReqActivity(act)) + if err != nil { + return processSQLErrorf(err, "Failed to bind pull request activity object") + } + + if err = db.QueryRowContext(ctx, query, arg...).Scan(&act.ID); err != nil { + return processSQLErrorf(err, "Failed to insert pull request activity") + } + + return nil +} + +// Update updates the pull request. +func (s *PullReqActivityStore) Update(ctx context.Context, act *types.PullReqActivity) error { + const sqlQuery = ` + UPDATE pullreq_activities + SET + pullreq_activity_version = :pullreq_activity_version + ,pullreq_activity_updated = :pullreq_activity_updated + ,pullreq_activity_edited = :pullreq_activity_edited + ,pullreq_activity_reply_seq = :pullreq_activity_reply_seq + ,pullreq_activity_text = :pullreq_activity_text + ,pullreq_activity_payload = :pullreq_activity_payload + ,pullreq_activity_metadata = :pullreq_activity_metadata + ,pullreq_activity_resolved_by = :pullreq_activity_resolved_by + ,pullreq_activity_resolved = :pullreq_activity_resolved + WHERE pullreq_activity_id = :pullreq_activity_id AND pullreq_activity_version = :pullreq_activity_version - 1` + + db := dbtx.GetAccessor(ctx, s.db) + + updatedAt := time.Now() + + dbAct := mapInternalPullReqActivity(act) + dbAct.Version++ + dbAct.Updated = updatedAt.UnixMilli() + + query, arg, err := db.BindNamed(sqlQuery, dbAct) + if err != nil { + return processSQLErrorf(err, "Failed to bind pull request activity object") + } + + result, err := db.ExecContext(ctx, query, arg...) + if err != nil { + return processSQLErrorf(err, "Failed to update pull request activity") + } + + count, err := result.RowsAffected() + if err != nil { + return processSQLErrorf(err, "Failed to get number of updated rows") + } + + if count == 0 { + return store.ErrConflict + } + + act.Version = dbAct.Version + act.Updated = dbAct.Updated + + return nil +} + +// UpdateReplySeq updates the pull request activity's reply sequence. +func (s *PullReqActivityStore) UpdateReplySeq(ctx context.Context, + act *types.PullReqActivity) (*types.PullReqActivity, error) { + for { + dup := *act + + dup.ReplySeq++ + err := s.Update(ctx, &dup) + if err == nil { + return &dup, nil + } + if !errors.Is(err, store.ErrConflict) { + return nil, err + } + + act, err = s.Find(ctx, act.ID) + if err != nil { + return nil, err + } + } +} + +// Count of pull requests for a repo. +func (s *PullReqActivityStore) Count(ctx context.Context, prID int64, + opts *types.PullReqActivityFilter) (int64, error) { + stmt := builder. + Select("count(*)"). + From("pullreq_activities"). + Where("pullreq_activity_pullreq_id = ?", prID) + + if len(opts.Types) == 1 { + stmt = stmt.Where("pullreq_activity_type = ?", opts.Types[0]) + } else if len(opts.Types) > 1 { + stmt = stmt.Where(squirrel.Eq{"pullreq_activity_type": opts.Types}) + } + + if len(opts.Kinds) == 1 { + stmt = stmt.Where("pullreq_activity_kind = ?", opts.Kinds[0]) + } else if len(opts.Kinds) > 1 { + stmt = stmt.Where(squirrel.Eq{"pullreq_activity_kind": opts.Kinds}) + } + + if opts.Since != 0 { + stmt = stmt.Where("pullreq_created >= ?", opts.Since) + } + + if opts.Until != 0 { + stmt = stmt.Where("pullreq_created < ?", opts.Until) + } + + sql, args, err := stmt.ToSql() + if err != nil { + return 0, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + var count int64 + err = db.QueryRowContext(ctx, sql, args...).Scan(&count) + if err != nil { + return 0, processSQLErrorf(err, "Failed executing count query") + } + + return count, nil +} + +// List returns a list of pull requests for a repo. +func (s *PullReqActivityStore) List(ctx context.Context, prID int64, + opts *types.PullReqActivityFilter) ([]*types.PullReqActivity, error) { + stmt := builder. + Select(pullreqActivityColumns). + From("pullreq_activities"). + InnerJoin("principals author on author.principal_id = pullreq_activity_created_by"). + LeftJoin("principals resolver on resolver.principal_id = pullreq_activity_resolved_by"). + Where("pullreq_activity_pullreq_id = ?", prID) + + if len(opts.Types) == 1 { + stmt = stmt.Where("pullreq_activity_type = ?", opts.Types[0]) + } else if len(opts.Types) > 1 { + stmt = stmt.Where(squirrel.Eq{"pullreq_activity_type": opts.Types}) + } + + if len(opts.Kinds) == 1 { + stmt = stmt.Where("pullreq_activity_kind = ?", opts.Kinds[0]) + } else if len(opts.Kinds) > 1 { + stmt = stmt.Where(squirrel.Eq{"pullreq_activity_kind": opts.Kinds}) + } + + if opts.Since != 0 { + stmt = stmt.Where("pullreq_created >= ?", opts.Since) + } + + if opts.Until != 0 { + stmt = stmt.Where("pullreq_created < ?", opts.Until) + } + + if opts.Limit > 0 { + stmt = stmt.Limit(uint64(limit(opts.Limit))) + } + + stmt = stmt.OrderBy("pullreq_activity_order asc", "pullreq_activity_sub_order asc") + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert pull request activity query to sql") + } + + dst := make([]*pullReqActivity, 0) + + db := dbtx.GetAccessor(ctx, s.db) + + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, processSQLErrorf(err, "Failed executing pull request activity list query") + } + + return mapSlicePullReqActivity(dst), nil +} + +func mapPullReqActivity(act *pullReqActivity) *types.PullReqActivity { + m := &types.PullReqActivity{ + ID: act.ID, + Version: act.Version, + CreatedBy: act.CreatedBy, + Created: act.Created, + Updated: act.Updated, + Edited: act.Edited, + Deleted: act.Deleted.Ptr(), + RepoID: act.RepoID, + PullReqID: act.PullReqID, + Order: act.Order, + SubOrder: act.SubOrder, + ReplySeq: act.ReplySeq, + Type: act.Type, + Kind: act.Kind, + Text: act.Text, + Payload: make(map[string]interface{}), + Metadata: make(map[string]interface{}), + ResolvedBy: act.ResolvedBy.Ptr(), + Resolved: act.Resolved.Ptr(), + Author: types.PrincipalInfo{}, + Resolver: nil, + } + m.Author = types.PrincipalInfo{ + ID: act.CreatedBy, + UID: act.AuthorUID, + Name: act.AuthorName, + Email: act.AuthorEmail, + } + + _ = json.Unmarshal(act.Payload, &m.Payload) + _ = json.Unmarshal(act.Metadata, &m.Metadata) + + if act.ResolvedBy.Valid { + m.Resolver = &types.PrincipalInfo{ + ID: act.ResolvedBy.Int64, + UID: act.ResolverUID.String, + Name: act.ResolverName.String, + Email: act.ResolverEmail.String, + } + } + + return m +} + +func mapInternalPullReqActivity(act *types.PullReqActivity) *pullReqActivity { + m := &pullReqActivity{ + ID: act.ID, + Version: act.Version, + CreatedBy: act.CreatedBy, + Created: act.Created, + Updated: act.Updated, + Edited: act.Edited, + Deleted: null.IntFromPtr(act.Deleted), + RepoID: act.RepoID, + PullReqID: act.PullReqID, + Order: act.Order, + SubOrder: act.SubOrder, + ReplySeq: act.ReplySeq, + Type: act.Type, + Kind: act.Kind, + Text: act.Text, + Payload: nil, + Metadata: nil, + ResolvedBy: null.IntFromPtr(act.ResolvedBy), + Resolved: null.IntFromPtr(act.Resolved), + } + + m.Payload, _ = json.Marshal(act.Payload) + m.Metadata, _ = json.Marshal(act.Metadata) + + return m +} + +func mapSlicePullReqActivity(a []*pullReqActivity) []*types.PullReqActivity { + m := make([]*types.PullReqActivity, len(a)) + for i, act := range a { + m[i] = mapPullReqActivity(act) + } + return m +} diff --git a/internal/store/database/pullreq_sync.go b/internal/store/database/pullreq_sync.go deleted file mode 100644 index 307643693..000000000 --- a/internal/store/database/pullreq_sync.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2022 Harness Inc. All rights reserved. -// Use of this source code is governed by the Polyform Free Trial License -// that can be found in the LICENSE.md file for this repository. - -package database - -import ( - "context" - - "github.com/harness/gitness/internal/store" - "github.com/harness/gitness/internal/store/database/mutex" - "github.com/harness/gitness/types" -) - -var _ store.PullReqStore = (*PullReqStoreSync)(nil) - -// NewPullReqStoreSync returns a new PullReqStoreSync. -func NewPullReqStoreSync(base *PullReqStore) *PullReqStoreSync { - return &PullReqStoreSync{ - base: base, - } -} - -// PullReqStoreSync synchronizes read and write access to the -// pull request store. This prevents race conditions when the database -// type is sqlite3. -type PullReqStoreSync struct { - base *PullReqStore -} - -// Find finds the pull request by id. -func (s *PullReqStoreSync) Find(ctx context.Context, id int64) (*types.PullReq, error) { - mutex.RLock() - defer mutex.RUnlock() - return s.base.Find(ctx, id) -} - -// FindByNumber finds the pull request by repo ID and pull request number. -func (s *PullReqStoreSync) FindByNumber(ctx context.Context, repoID, number int64) (*types.PullReq, error) { - mutex.RLock() - defer mutex.RUnlock() - return s.base.FindByNumber(ctx, repoID, number) -} - -// Create creates a new pull request. -func (s *PullReqStoreSync) Create(ctx context.Context, pullReq *types.PullReq) error { - mutex.Lock() - defer mutex.Unlock() - return s.base.Create(ctx, pullReq) -} - -// Update updates the pull request. -func (s *PullReqStoreSync) Update(ctx context.Context, pullReq *types.PullReq) error { - mutex.Lock() - defer mutex.Unlock() - return s.base.Update(ctx, pullReq) -} - -// Delete the pull request. -func (s *PullReqStoreSync) Delete(ctx context.Context, id int64) error { - mutex.Lock() - defer mutex.Unlock() - return s.base.Delete(ctx, id) -} - -// LastNumber return the number of the most recent pull request. -func (s *PullReqStoreSync) LastNumber(ctx context.Context, repoID int64) (int64, error) { - mutex.RLock() - defer mutex.RUnlock() - return s.base.LastNumber(ctx, repoID) -} - -// Count of pull requests for a repo. -func (s *PullReqStoreSync) Count(ctx context.Context, repoID int64, opts *types.PullReqFilter) (int64, error) { - mutex.RLock() - defer mutex.RUnlock() - return s.base.Count(ctx, repoID, opts) -} - -// List returns a list of pull requests for a repo. -func (s *PullReqStoreSync) List( - ctx context.Context, - repoID int64, - opts *types.PullReqFilter, -) ([]*types.PullReq, error) { - mutex.RLock() - defer mutex.RUnlock() - return s.base.List(ctx, repoID, opts) -} diff --git a/internal/store/database/wire.go b/internal/store/database/wire.go index 6e6c89c86..66f2dd7a2 100644 --- a/internal/store/database/wire.go +++ b/internal/store/database/wire.go @@ -26,8 +26,9 @@ var WireSet = wire.NewSet( ProvideServiceStore, ProvideSpaceStore, ProvideRepoStore, - ProvidePullReqStore, ProvideTokenStore, + ProvidePullReqStore, + ProvidePullReqActivityStore, ) // ProvideDatabase provides a database connection. @@ -114,12 +115,10 @@ func ProvideTokenStore(db *sqlx.DB) store.TokenStore { // ProvidePullReqStore provides a pull request store. func ProvidePullReqStore(db *sqlx.DB) store.PullReqStore { - switch db.DriverName() { - case postgres: - return NewPullReqStore(db) - default: - return NewPullReqStoreSync( - NewPullReqStore(db), - ) - } + return NewPullReqStore(db) +} + +// ProvidePullReqActivityStore provides a pull request activity store. +func ProvidePullReqActivityStore(db *sqlx.DB) store.PullReqActivityStore { + return NewPullReqActivityStore(db) } diff --git a/internal/store/store.go b/internal/store/store.go index cacba82a0..ed1094093 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -212,6 +212,10 @@ type ( // Update the pull request. It will set new values to the Version and Updated fields. Update(ctx context.Context, repo *types.PullReq) error + // UpdateActivitySeq the pull request's activity sequence number. + // It will set new values to the ActivitySeq, Version and Updated fields. + UpdateActivitySeq(ctx context.Context, pr *types.PullReq) (*types.PullReq, error) + // Delete the pull request. Delete(ctx context.Context, id int64) error @@ -225,6 +229,28 @@ type ( List(ctx context.Context, repoID int64, opts *types.PullReqFilter) ([]*types.PullReq, error) } + PullReqActivityStore interface { + // Find the pull request activity by id. + Find(ctx context.Context, id int64) (*types.PullReqActivity, error) + + // Create a new pull request activity. Value of the Order field should be fetched with UpdateActivitySeq. + // Value of the SubOrder field (for replies) should be fetched with UpdateReplySeq (non-replies have 0). + Create(ctx context.Context, act *types.PullReqActivity) error + + // Update the pull request activity. It will set new values to the Version and Updated fields. + Update(ctx context.Context, act *types.PullReqActivity) error + + // UpdateReplySeq the pull request activity's reply sequence number. + // It will set new values to the ReplySeq, Version and Updated fields. + UpdateReplySeq(ctx context.Context, act *types.PullReqActivity) (*types.PullReqActivity, error) + + // Count returns number of pull request activities in a pull request. + Count(ctx context.Context, prID int64, opts *types.PullReqActivityFilter) (int64, error) + + // List returns a list of pull request activities in a pull request (a timeline). + List(ctx context.Context, prID int64, opts *types.PullReqActivityFilter) ([]*types.PullReqActivity, error) + } + // SystemStore defines internal system metadata storage. SystemStore interface { // Config returns the system configuration. diff --git a/types/enum/pullreq.go b/types/enum/pullreq.go index 486a4df5f..3f4e0aec8 100644 --- a/types/enum/pullreq.go +++ b/types/enum/pullreq.go @@ -4,7 +4,10 @@ package enum -import "strings" +import ( + "sort" + "strings" +) // PullReqState defines pull request state. type PullReqState string @@ -58,3 +61,67 @@ func (a PullReqSort) String() string { return undefined } } + +// PullReqActivityType defines pull request activity message type. +// Essentially, the Type determines the structure of the pull request activity's Payload structure. +type PullReqActivityType string + +// PullReqActivityType enumeration. +const ( + PullReqActivityTypeComment PullReqActivityType = "comment" + PullReqActivityTypeCodeComment PullReqActivityType = "code-comment" + PullReqActivityTypeTitleChange PullReqActivityType = "title-change" +) + +var pullReqActivityTypes = []string{ + string(PullReqActivityTypeComment), + string(PullReqActivityTypeCodeComment), + string(PullReqActivityTypeTitleChange), +} + +func init() { + sort.Strings(pullReqActivityTypes) +} + +// ParsePullReqActivityType parses the pull request activity type. +func ParsePullReqActivityType(s string) (PullReqActivityType, bool) { + if existsInSortedSlice(pullReqActivityTypes, s) { + return PullReqActivityType(s), true + } + return "", false +} + +// PullReqActivityKind defines kind of pull request activity system message. +// Kind defines the source of the pull request activity entry: +// Whether it's generated by the system, it's a user comment or a part of code review. +type PullReqActivityKind string + +// PullReqActivityKind enumeration. +const ( + PullReqActivityKindSystem PullReqActivityKind = "system" + PullReqActivityKindComment PullReqActivityKind = "comment" + PullReqActivityKindCodeComment PullReqActivityKind = "code" +) + +var pullReqActivityKinds = []string{ + string(PullReqActivityKindSystem), + string(PullReqActivityKindComment), + string(PullReqActivityTypeCodeComment), +} + +func init() { + sort.Strings(pullReqActivityKinds) +} + +// ParsePullReqActivityKind parses the pull request activity type. +func ParsePullReqActivityKind(s string) (PullReqActivityKind, bool) { + if existsInSortedSlice(pullReqActivityKinds, s) { + return PullReqActivityKind(s), true + } + return "", false +} + +func existsInSortedSlice(strs []string, s string) bool { + idx := sort.SearchStrings(strs, s) + return idx >= 0 && idx < len(strs) && strs[idx] == s +} diff --git a/types/enum/shared.go b/types/enum/shared.go index 4eef9e6c4..2400d242e 100644 --- a/types/enum/shared.go +++ b/types/enum/shared.go @@ -19,4 +19,7 @@ const ( date = "date" defaultString = "default" undefined = "undefined" + system = "system" + comment = "comment" + code = "code" ) diff --git a/types/pullreq.go b/types/pullreq.go index 3d01b1b95..1f5096c24 100644 --- a/types/pullreq.go +++ b/types/pullreq.go @@ -29,7 +29,7 @@ type PullReq struct { TargetRepoID int64 `json:"target_repo_id"` TargetBranch string `json:"target_branch"` - PullReqActivitySeq int64 `json:"-"` // not returned, because it's a server internal field + ActivitySeq int64 `json:"-"` // not returned, because it's a server's internal field MergedBy *int64 `json:"-"` // not returned, because the merger info is in the Merger field Merged *int64 `json:"merged"` @@ -59,23 +59,25 @@ type PullReqActivity struct { ID int64 `json:"id"` Version int64 `json:"version"` - CreatedBy int64 `json:"-"` // not returned, because the author info is in the Author field - Created int64 `json:"created"` - Updated int64 `json:"updated"` - Edited int64 `json:"edited"` - Deleted int64 `json:"deleted"` + CreatedBy int64 `json:"-"` // not returned, because the author info is in the Author field + Created int64 `json:"created"` + Updated int64 `json:"updated"` + Edited int64 `json:"edited"` + Deleted *int64 `json:"deleted"` RepoID int64 `json:"repo_id"` PullReqID int64 `json:"pullreq_id"` - Seq int64 `json:"seq"` - SubSeq int64 `json:"subseq"` + Order int64 `json:"order"` + SubOrder int64 `json:"sub_order"` + ReplySeq int64 `json:"-"` // not returned, because it's a server's internal field - Type int64 `json:"type"` - Kind int64 `json:"kind"` + Type enum.PullReqActivityType `json:"type"` + Kind enum.PullReqActivityKind `json:"kind"` - Text string `json:"title"` - Payload map[string]interface{} `json:"payload"` + Text string `json:"title"` + Payload map[string]interface{} `json:"payload"` + Metadata map[string]interface{} `json:"metadata"` ResolvedBy *int64 `json:"-"` // not returned, because the resolver info is in the Resolver field Resolved *int64 `json:"resolved"` @@ -83,3 +85,13 @@ type PullReqActivity struct { Author PrincipalInfo `json:"author"` Resolver *PrincipalInfo `json:"resolver"` } + +// PullReqActivityFilter stores pull request activity query parameters. +type PullReqActivityFilter struct { + Since int64 `json:"since"` + Until int64 `json:"until"` + Limit int `json:"limit"` + + Types []enum.PullReqActivityType `json:"type"` + Kinds []enum.PullReqActivityKind `json:"kind"` +}