Pull request timeline feature: DB and the list API (#136)

This commit is contained in:
Marko Gaćeša 2022-12-26 12:17:38 +01:00 committed by GitHub
parent 23792ab2f5
commit 7fc77396a9
26 changed files with 1120 additions and 188 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,4 +19,7 @@ const (
date = "date"
defaultString = "default"
undefined = "undefined"
system = "system"
comment = "comment"
code = "code"
)

View File

@ -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"`
}