Detailed changes
@@ -9,9 +9,9 @@ model:
autobind:
- "github.com/git-bug/git-bug/api/graphql/models"
+ - "github.com/git-bug/git-bug/cache"
- "github.com/git-bug/git-bug/repository"
- "github.com/git-bug/git-bug/entity"
- - "github.com/git-bug/git-bug/entity/dag"
- "github.com/git-bug/git-bug/entities/common"
- "github.com/git-bug/git-bug/entities/bug"
- "github.com/git-bug/git-bug/entities/identity"
@@ -1576,7 +1576,7 @@ func (ec *executionContext) fieldContext_BugEdge_node(_ context.Context, field g
// region **************************** object.gotpl ****************************
-var bugImplementors = []string{"Bug", "Authored"}
+var bugImplementors = []string{"Bug", "Authored", "Entity"}
func (ec *executionContext) _Bug(ctx context.Context, sel ast.SelectionSet, obj models.BugWrapper) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, bugImplementors)
@@ -702,7 +702,7 @@ func (ec *executionContext) fieldContext_IdentityEdge_node(_ context.Context, fi
// region **************************** object.gotpl ****************************
-var identityImplementors = []string{"Identity"}
+var identityImplementors = []string{"Identity", "Entity"}
func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj models.IdentityWrapper) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, identityImplementors)
@@ -53,6 +53,7 @@ type ResolverRoot interface {
Mutation() MutationResolver
Query() QueryResolver
Repository() RepositoryResolver
+ Subscription() SubscriptionResolver
}
type DirectiveRoot struct {
@@ -200,6 +201,11 @@ type ComplexityRoot struct {
Operation func(childComplexity int) int
}
+ BugEvent struct {
+ Bug func(childComplexity int) int
+ Type func(childComplexity int) int
+ }
+
BugLabelChangeOperation struct {
Added func(childComplexity int) int
Author func(childComplexity int) int
@@ -282,6 +288,11 @@ type ComplexityRoot struct {
R func(childComplexity int) int
}
+ EntityEvent struct {
+ Entity func(childComplexity int) int
+ Type func(childComplexity int) int
+ }
+
Identity struct {
AvatarUrl func(childComplexity int) int
DisplayName func(childComplexity int) int
@@ -305,6 +316,11 @@ type ComplexityRoot struct {
Node func(childComplexity int) int
}
+ IdentityEvent struct {
+ Identity func(childComplexity int) int
+ Type func(childComplexity int) int
+ }
+
Label struct {
Color func(childComplexity int) int
Name func(childComplexity int) int
@@ -371,6 +387,12 @@ type ComplexityRoot struct {
UserIdentity func(childComplexity int) int
ValidLabels func(childComplexity int, after *string, before *string, first *int, last *int) int
}
+
+ Subscription struct {
+ AllEvents func(childComplexity int, repoRef *string, typename *string) int
+ BugEvents func(childComplexity int, repoRef *string) int
+ IdentityEvents func(childComplexity int, repoRef *string) int
+ }
}
type executableSchema struct {
@@ -1026,6 +1048,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.BugEditCommentPayload.Operation(childComplexity), true
+ case "BugEvent.bug":
+ if e.complexity.BugEvent.Bug == nil {
+ break
+ }
+
+ return e.complexity.BugEvent.Bug(childComplexity), true
+
+ case "BugEvent.type":
+ if e.complexity.BugEvent.Type == nil {
+ break
+ }
+
+ return e.complexity.BugEvent.Type(childComplexity), true
+
case "BugLabelChangeOperation.added":
if e.complexity.BugLabelChangeOperation.Added == nil {
break
@@ -1348,6 +1384,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Color.R(childComplexity), true
+ case "EntityEvent.entity":
+ if e.complexity.EntityEvent.Entity == nil {
+ break
+ }
+
+ return e.complexity.EntityEvent.Entity(childComplexity), true
+
+ case "EntityEvent.type":
+ if e.complexity.EntityEvent.Type == nil {
+ break
+ }
+
+ return e.complexity.EntityEvent.Type(childComplexity), true
+
case "Identity.avatarUrl":
if e.complexity.Identity.AvatarUrl == nil {
break
@@ -1446,6 +1496,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.IdentityEdge.Node(childComplexity), true
+ case "IdentityEvent.identity":
+ if e.complexity.IdentityEvent.Identity == nil {
+ break
+ }
+
+ return e.complexity.IdentityEvent.Identity(childComplexity), true
+
+ case "IdentityEvent.type":
+ if e.complexity.IdentityEvent.Type == nil {
+ break
+ }
+
+ return e.complexity.IdentityEvent.Type(childComplexity), true
+
case "Label.color":
if e.complexity.Label.Color == nil {
break
@@ -1780,6 +1844,42 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Repository.ValidLabels(childComplexity, args["after"].(*string), args["before"].(*string), args["first"].(*int), args["last"].(*int)), true
+ case "Subscription.allEvents":
+ if e.complexity.Subscription.AllEvents == nil {
+ break
+ }
+
+ args, err := ec.field_Subscription_allEvents_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Subscription.AllEvents(childComplexity, args["repoRef"].(*string), args["typename"].(*string)), true
+
+ case "Subscription.bugEvents":
+ if e.complexity.Subscription.BugEvents == nil {
+ break
+ }
+
+ args, err := ec.field_Subscription_bugEvents_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Subscription.BugEvents(childComplexity, args["repoRef"].(*string)), true
+
+ case "Subscription.identityEvents":
+ if e.complexity.Subscription.IdentityEvents == nil {
+ break
+ }
+
+ args, err := ec.field_Subscription_identityEvents_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Subscription.IdentityEvents(childComplexity, args["repoRef"].(*string)), true
+
}
return 0, false
}
@@ -1842,6 +1942,23 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
var buf bytes.Buffer
data.MarshalGQL(&buf)
+ return &graphql.Response{
+ Data: buf.Bytes(),
+ }
+ }
+ case ast.Subscription:
+ next := ec._Subscription(ctx, opCtx.Operation.SelectionSet)
+
+ var buf bytes.Buffer
+ return func(ctx context.Context) *graphql.Response {
+ buf.Reset()
+ data := next(ctx)
+
+ if data == nil {
+ return nil
+ }
+ data.MarshalGQL(&buf)
+
return &graphql.Response{
Data: buf.Bytes(),
}
@@ -1894,7 +2011,7 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
}
var sources = []*ast.Source{
- {Name: "../schema/bug.graphql", Input: `type Bug implements Authored {
+ {Name: "../schema/bug.graphql", Input: `type Bug implements Authored & Entity {
"""The identifier for this bug"""
id: ID!
"""The human version (truncated) identifier for this bug"""
@@ -2419,7 +2536,7 @@ directive @goTag(
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
`, BuiltIn: false},
{Name: "../schema/identity.graphql", Input: `"""Represents an identity"""
-type Identity {
+type Identity implements Entity {
"""The identifier for this identity"""
id: ID!
"""The human version (truncated) identifier for this identity"""
@@ -2487,7 +2604,8 @@ type LabelChangeResult {
}
`, BuiltIn: false},
{Name: "../schema/operation.graphql", Input: `"""An operation applied to an entity."""
-interface Operation {
+interface Operation
+@goModel(model: "github.com/git-bug/git-bug/entity/dag.Operation") {
"""The identifier of the operation"""
id: ID!
"""The operations author."""
@@ -2573,6 +2691,36 @@ type Mutation # See each entity mutations
OPEN
CLOSED
}
+`, BuiltIn: false},
+ {Name: "../schema/subscription.graphql", Input: `type Subscription {
+ """Subscribe to events on all entities. For events on a specific repo you can provide a repo reference. Without it, you get the unique default repo or all repo events."""
+ allEvents(repoRef: String, typename: String): EntityEvent!
+ """Subscribe to identity entity events. For events on a specific repo you can provide a repo reference. Without it, you get the unique default repo or all repo events."""
+ identityEvents(repoRef: String): IdentityEvent!
+ """Subscribe to bug entity events. For events on a specific repo you can provide a repo reference. Without it, you get the unique default repo or all repo events."""
+ bugEvents(repoRef: String): BugEvent!
+}
+
+enum EntityEventType {
+ CREATED
+ UPDATED
+ REMOVED
+}
+
+type EntityEvent {
+ type: EntityEventType!
+ entity: Entity
+}
+
+type IdentityEvent {
+ type: EntityEventType!
+ identity: Identity!
+}
+
+type BugEvent {
+ type: EntityEventType!
+ bug: Bug!
+}
`, BuiltIn: false},
{Name: "../schema/types.graphql", Input: `scalar CombinedId
scalar Time
@@ -2605,6 +2753,15 @@ interface Authored {
"""The author of this object."""
author: Identity!
}
+
+
+"""An entity (identity, bug, ...)."""
+interface Entity {
+ """The identifier for this entity"""
+ id: ID!
+ """The human version (truncated) identifier for this entity"""
+ humanId: String!
+}
`, BuiltIn: false},
}
var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -0,0 +1,899 @@
+// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
+
+package graph
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "strconv"
+ "sync/atomic"
+
+ "github.com/99designs/gqlgen/graphql"
+ "github.com/git-bug/git-bug/api/graphql/models"
+ "github.com/git-bug/git-bug/cache"
+ "github.com/vektah/gqlparser/v2/ast"
+)
+
+// region ************************** generated!.gotpl **************************
+
+type SubscriptionResolver interface {
+ AllEvents(ctx context.Context, repoRef *string, typename *string) (<-chan *models.EntityEvent, error)
+ IdentityEvents(ctx context.Context, repoRef *string) (<-chan *models.IdentityEvent, error)
+ BugEvents(ctx context.Context, repoRef *string) (<-chan *models.BugEvent, error)
+}
+
+// endregion ************************** generated!.gotpl **************************
+
+// region ***************************** args.gotpl *****************************
+
+func (ec *executionContext) field_Subscription_allEvents_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Subscription_allEvents_argsRepoRef(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["repoRef"] = arg0
+ arg1, err := ec.field_Subscription_allEvents_argsTypename(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["typename"] = arg1
+ return args, nil
+}
+func (ec *executionContext) field_Subscription_allEvents_argsRepoRef(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["repoRef"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("repoRef"))
+ if tmp, ok := rawArgs["repoRef"]; ok {
+ return ec.unmarshalOString2ástring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Subscription_allEvents_argsTypename(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["typename"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("typename"))
+ if tmp, ok := rawArgs["typename"]; ok {
+ return ec.unmarshalOString2ástring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Subscription_bugEvents_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Subscription_bugEvents_argsRepoRef(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["repoRef"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Subscription_bugEvents_argsRepoRef(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["repoRef"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("repoRef"))
+ if tmp, ok := rawArgs["repoRef"]; ok {
+ return ec.unmarshalOString2ástring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Subscription_identityEvents_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Subscription_identityEvents_argsRepoRef(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["repoRef"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Subscription_identityEvents_argsRepoRef(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["repoRef"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("repoRef"))
+ if tmp, ok := rawArgs["repoRef"]; ok {
+ return ec.unmarshalOString2ástring(ctx, tmp)
+ }
+
+ var zeroVal *string
+ return zeroVal, nil
+}
+
+// endregion ***************************** args.gotpl *****************************
+
+// region ************************** directives.gotpl **************************
+
+// endregion ************************** directives.gotpl **************************
+
+// region **************************** field.gotpl *****************************
+
+func (ec *executionContext) _BugEvent_type(ctx context.Context, field graphql.CollectedField, obj *models.BugEvent) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BugEvent_type(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Type, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(cache.EntityEventType)
+ fc.Result = res
+ return ec.marshalNEntityEventType2githubácomágitábugágitábugácacheáEntityEventType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_BugEvent_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BugEvent",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type EntityEventType does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _BugEvent_bug(ctx context.Context, field graphql.CollectedField, obj *models.BugEvent) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BugEvent_bug(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Bug, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(models.BugWrapper)
+ fc.Result = res
+ return ec.marshalNBug2githubácomágitábugágitábugáapiágraphqlámodelsáBugWrapper(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_BugEvent_bug(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BugEvent",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_Bug_id(ctx, field)
+ case "humanId":
+ return ec.fieldContext_Bug_humanId(ctx, field)
+ case "status":
+ return ec.fieldContext_Bug_status(ctx, field)
+ case "title":
+ return ec.fieldContext_Bug_title(ctx, field)
+ case "labels":
+ return ec.fieldContext_Bug_labels(ctx, field)
+ case "author":
+ return ec.fieldContext_Bug_author(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_Bug_createdAt(ctx, field)
+ case "lastEdit":
+ return ec.fieldContext_Bug_lastEdit(ctx, field)
+ case "actors":
+ return ec.fieldContext_Bug_actors(ctx, field)
+ case "participants":
+ return ec.fieldContext_Bug_participants(ctx, field)
+ case "comments":
+ return ec.fieldContext_Bug_comments(ctx, field)
+ case "timeline":
+ return ec.fieldContext_Bug_timeline(ctx, field)
+ case "operations":
+ return ec.fieldContext_Bug_operations(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type Bug", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _EntityEvent_type(ctx context.Context, field graphql.CollectedField, obj *models.EntityEvent) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_EntityEvent_type(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Type, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(cache.EntityEventType)
+ fc.Result = res
+ return ec.marshalNEntityEventType2githubácomágitábugágitábugácacheáEntityEventType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_EntityEvent_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "EntityEvent",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type EntityEventType does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _EntityEvent_entity(ctx context.Context, field graphql.CollectedField, obj *models.EntityEvent) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_EntityEvent_entity(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Entity, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(models.Entity)
+ fc.Result = res
+ return ec.marshalOEntity2githubácomágitábugágitábugáapiágraphqlámodelsáEntity(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_EntityEvent_entity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "EntityEvent",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _IdentityEvent_type(ctx context.Context, field graphql.CollectedField, obj *models.IdentityEvent) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_IdentityEvent_type(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Type, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(cache.EntityEventType)
+ fc.Result = res
+ return ec.marshalNEntityEventType2githubácomágitábugágitábugácacheáEntityEventType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_IdentityEvent_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "IdentityEvent",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type EntityEventType does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _IdentityEvent_identity(ctx context.Context, field graphql.CollectedField, obj *models.IdentityEvent) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_IdentityEvent_identity(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Identity, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(models.IdentityWrapper)
+ fc.Result = res
+ return ec.marshalNIdentity2githubácomágitábugágitábugáapiágraphqlámodelsáIdentityWrapper(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_IdentityEvent_identity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "IdentityEvent",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_Identity_id(ctx, field)
+ case "humanId":
+ return ec.fieldContext_Identity_humanId(ctx, field)
+ case "name":
+ return ec.fieldContext_Identity_name(ctx, field)
+ case "email":
+ return ec.fieldContext_Identity_email(ctx, field)
+ case "login":
+ return ec.fieldContext_Identity_login(ctx, field)
+ case "displayName":
+ return ec.fieldContext_Identity_displayName(ctx, field)
+ case "avatarUrl":
+ return ec.fieldContext_Identity_avatarUrl(ctx, field)
+ case "isProtected":
+ return ec.fieldContext_Identity_isProtected(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type Identity", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Subscription_allEvents(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) {
+ fc, err := ec.fieldContext_Subscription_allEvents(ctx, field)
+ if err != nil {
+ return nil
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Subscription().AllEvents(rctx, fc.Args["repoRef"].(*string), fc.Args["typename"].(*string))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return nil
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return nil
+ }
+ return func(ctx context.Context) graphql.Marshaler {
+ select {
+ case res, ok := <-resTmp.(<-chan *models.EntityEvent):
+ if !ok {
+ return nil
+ }
+ return graphql.WriterFunc(func(w io.Writer) {
+ w.Write([]byte{'{'})
+ graphql.MarshalString(field.Alias).MarshalGQL(w)
+ w.Write([]byte{':'})
+ ec.marshalNEntityEvent2ágithubácomágitábugágitábugáapiágraphqlámodelsáEntityEvent(ctx, field.Selections, res).MarshalGQL(w)
+ w.Write([]byte{'}'})
+ })
+ case <-ctx.Done():
+ return nil
+ }
+ }
+}
+
+func (ec *executionContext) fieldContext_Subscription_allEvents(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Subscription",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "type":
+ return ec.fieldContext_EntityEvent_type(ctx, field)
+ case "entity":
+ return ec.fieldContext_EntityEvent_entity(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type EntityEvent", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Subscription_allEvents_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Subscription_identityEvents(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) {
+ fc, err := ec.fieldContext_Subscription_identityEvents(ctx, field)
+ if err != nil {
+ return nil
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Subscription().IdentityEvents(rctx, fc.Args["repoRef"].(*string))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return nil
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return nil
+ }
+ return func(ctx context.Context) graphql.Marshaler {
+ select {
+ case res, ok := <-resTmp.(<-chan *models.IdentityEvent):
+ if !ok {
+ return nil
+ }
+ return graphql.WriterFunc(func(w io.Writer) {
+ w.Write([]byte{'{'})
+ graphql.MarshalString(field.Alias).MarshalGQL(w)
+ w.Write([]byte{':'})
+ ec.marshalNIdentityEvent2ágithubácomágitábugágitábugáapiágraphqlámodelsáIdentityEvent(ctx, field.Selections, res).MarshalGQL(w)
+ w.Write([]byte{'}'})
+ })
+ case <-ctx.Done():
+ return nil
+ }
+ }
+}
+
+func (ec *executionContext) fieldContext_Subscription_identityEvents(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Subscription",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "type":
+ return ec.fieldContext_IdentityEvent_type(ctx, field)
+ case "identity":
+ return ec.fieldContext_IdentityEvent_identity(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type IdentityEvent", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Subscription_identityEvents_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Subscription_bugEvents(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) {
+ fc, err := ec.fieldContext_Subscription_bugEvents(ctx, field)
+ if err != nil {
+ return nil
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Subscription().BugEvents(rctx, fc.Args["repoRef"].(*string))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return nil
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return nil
+ }
+ return func(ctx context.Context) graphql.Marshaler {
+ select {
+ case res, ok := <-resTmp.(<-chan *models.BugEvent):
+ if !ok {
+ return nil
+ }
+ return graphql.WriterFunc(func(w io.Writer) {
+ w.Write([]byte{'{'})
+ graphql.MarshalString(field.Alias).MarshalGQL(w)
+ w.Write([]byte{':'})
+ ec.marshalNBugEvent2ágithubácomágitábugágitábugáapiágraphqlámodelsáBugEvent(ctx, field.Selections, res).MarshalGQL(w)
+ w.Write([]byte{'}'})
+ })
+ case <-ctx.Done():
+ return nil
+ }
+ }
+}
+
+func (ec *executionContext) fieldContext_Subscription_bugEvents(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Subscription",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "type":
+ return ec.fieldContext_BugEvent_type(ctx, field)
+ case "bug":
+ return ec.fieldContext_BugEvent_bug(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type BugEvent", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Subscription_bugEvents_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+// endregion **************************** field.gotpl *****************************
+
+// region **************************** input.gotpl *****************************
+
+// endregion **************************** input.gotpl *****************************
+
+// region ************************** interface.gotpl ***************************
+
+// endregion ************************** interface.gotpl ***************************
+
+// region **************************** object.gotpl ****************************
+
+var bugEventImplementors = []string{"BugEvent"}
+
+func (ec *executionContext) _BugEvent(ctx context.Context, sel ast.SelectionSet, obj *models.BugEvent) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, bugEventImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("BugEvent")
+ case "type":
+ out.Values[i] = ec._BugEvent_type(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "bug":
+ out.Values[i] = ec._BugEvent_bug(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var entityEventImplementors = []string{"EntityEvent"}
+
+func (ec *executionContext) _EntityEvent(ctx context.Context, sel ast.SelectionSet, obj *models.EntityEvent) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, entityEventImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("EntityEvent")
+ case "type":
+ out.Values[i] = ec._EntityEvent_type(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "entity":
+ out.Values[i] = ec._EntityEvent_entity(ctx, field, obj)
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var identityEventImplementors = []string{"IdentityEvent"}
+
+func (ec *executionContext) _IdentityEvent(ctx context.Context, sel ast.SelectionSet, obj *models.IdentityEvent) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, identityEventImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("IdentityEvent")
+ case "type":
+ out.Values[i] = ec._IdentityEvent_type(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "identity":
+ out.Values[i] = ec._IdentityEvent_identity(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
+var subscriptionImplementors = []string{"Subscription"}
+
+func (ec *executionContext) _Subscription(ctx context.Context, sel ast.SelectionSet) func(ctx context.Context) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, subscriptionImplementors)
+ ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{
+ Object: "Subscription",
+ })
+ if len(fields) != 1 {
+ ec.Errorf(ctx, "must subscribe to exactly one stream")
+ return nil
+ }
+
+ switch fields[0].Name {
+ case "allEvents":
+ return ec._Subscription_allEvents(ctx, fields[0])
+ case "identityEvents":
+ return ec._Subscription_identityEvents(ctx, fields[0])
+ case "bugEvents":
+ return ec._Subscription_bugEvents(ctx, fields[0])
+ default:
+ panic("unknown field " + strconv.Quote(fields[0].Name))
+ }
+}
+
+// endregion **************************** object.gotpl ****************************
+
+// region ***************************** type.gotpl *****************************
+
+func (ec *executionContext) marshalNBugEvent2githubácomágitábugágitábugáapiágraphqlámodelsáBugEvent(ctx context.Context, sel ast.SelectionSet, v models.BugEvent) graphql.Marshaler {
+ return ec._BugEvent(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNBugEvent2ágithubácomágitábugágitábugáapiágraphqlámodelsáBugEvent(ctx context.Context, sel ast.SelectionSet, v *models.BugEvent) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._BugEvent(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalNEntityEvent2githubácomágitábugágitábugáapiágraphqlámodelsáEntityEvent(ctx context.Context, sel ast.SelectionSet, v models.EntityEvent) graphql.Marshaler {
+ return ec._EntityEvent(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNEntityEvent2ágithubácomágitábugágitábugáapiágraphqlámodelsáEntityEvent(ctx context.Context, sel ast.SelectionSet, v *models.EntityEvent) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._EntityEvent(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalNEntityEventType2githubácomágitábugágitábugácacheáEntityEventType(ctx context.Context, v any) (cache.EntityEventType, error) {
+ var res cache.EntityEventType
+ err := res.UnmarshalGQL(v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNEntityEventType2githubácomágitábugágitábugácacheáEntityEventType(ctx context.Context, sel ast.SelectionSet, v cache.EntityEventType) graphql.Marshaler {
+ return v
+}
+
+func (ec *executionContext) marshalNIdentityEvent2githubácomágitábugágitábugáapiágraphqlámodelsáIdentityEvent(ctx context.Context, sel ast.SelectionSet, v models.IdentityEvent) graphql.Marshaler {
+ return ec._IdentityEvent(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNIdentityEvent2ágithubácomágitábugágitábugáapiágraphqlámodelsáIdentityEvent(ctx context.Context, sel ast.SelectionSet, v *models.IdentityEvent) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._IdentityEvent(ctx, sel, v)
+}
+
+// endregion ***************************** type.gotpl *****************************
@@ -414,6 +414,11 @@ func (ec *executionContext) _Authored(ctx context.Context, sel ast.SelectionSet,
return graphql.Null
}
return ec._BugAddCommentOperation(ctx, sel, obj)
+ case models.BugWrapper:
+ if obj == nil {
+ return graphql.Null
+ }
+ return ec._Bug(ctx, sel, obj)
case bug.Comment:
return ec._BugComment(ctx, sel, &obj)
case *bug.Comment:
@@ -421,11 +426,25 @@ func (ec *executionContext) _Authored(ctx context.Context, sel ast.SelectionSet,
return graphql.Null
}
return ec._BugComment(ctx, sel, obj)
+ default:
+ panic(fmt.Errorf("unexpected type %T", obj))
+ }
+}
+
+func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet, obj models.Entity) graphql.Marshaler {
+ switch obj := (obj).(type) {
+ case nil:
+ return graphql.Null
case models.BugWrapper:
if obj == nil {
return graphql.Null
}
return ec._Bug(ctx, sel, obj)
+ case models.IdentityWrapper:
+ if obj == nil {
+ return graphql.Null
+ }
+ return ec._Identity(ctx, sel, obj)
default:
panic(fmt.Errorf("unexpected type %T", obj))
}
@@ -747,6 +766,13 @@ func (ec *executionContext) marshalNTime2átimeáTime(ctx context.Context, sel
return res
}
+func (ec *executionContext) marshalOEntity2githubácomágitábugágitábugáapiágraphqlámodelsáEntity(ctx context.Context, sel ast.SelectionSet, v models.Entity) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ return ec._Entity(ctx, sel, v)
+}
+
func (ec *executionContext) unmarshalOHash2ágithubácomágitábugágitábugárepositoryáHashá(ctx context.Context, v any) ([]repository.Hash, error) {
if v == nil {
return nil, nil
@@ -219,3 +219,36 @@ func TestQueries(t *testing.T) {
err := c.Post(query, &resp)
assert.NoError(t, err)
}
+
+func TestBugEventsSubscription(t *testing.T) {
+ repo := repository.CreateGoGitTestRepo(t, false)
+
+ mrc := cache.NewMultiRepoCache()
+ rc, events := mrc.RegisterDefaultRepository(repo)
+ for event := range events {
+ require.NoError(t, event.Err)
+ }
+
+ h := NewHandler(mrc, nil)
+ c := client.New(h)
+
+ sub := c.Websocket(`subscription { bugEvents { type bug { id } } }`)
+ t.Cleanup(func() { _ = sub.Close() })
+
+ rene, err := rc.Identities().New("RenĂŠ Descartes", "rene@descartes.fr")
+ require.NoError(t, err)
+ require.NoError(t, rc.SetUserIdentity(rene))
+
+ b, _, err := rc.Bugs().New("test subscription", "body")
+ require.NoError(t, err)
+
+ var resp struct {
+ BugEvents struct {
+ Type string
+ Bug struct{ Id string }
+ }
+ }
+ require.NoError(t, sub.Next(&resp))
+ assert.Equal(t, "CREATED", resp.BugEvents.Type)
+ assert.Equal(t, b.Id().String(), resp.BugEvents.Bug.Id)
+}
@@ -6,8 +6,14 @@ package graphql
import (
"io"
"net/http"
+ "time"
"github.com/99designs/gqlgen/graphql/handler"
+ "github.com/99designs/gqlgen/graphql/handler/extension"
+ "github.com/99designs/gqlgen/graphql/handler/lru"
+ "github.com/99designs/gqlgen/graphql/handler/transport"
+ "github.com/gorilla/websocket"
+ "github.com/vektah/gqlparser/v2/ast"
"github.com/git-bug/git-bug/api/graphql/graph"
"github.com/git-bug/git-bug/api/graphql/resolvers"
@@ -23,7 +29,26 @@ type Handler struct {
func NewHandler(mrc *cache.MultiRepoCache, errorOut io.Writer) Handler {
rootResolver := resolvers.NewRootResolver(mrc)
config := graph.Config{Resolvers: rootResolver}
- h := handler.NewDefaultServer(graph.NewExecutableSchema(config))
+
+ h := handler.New(graph.NewExecutableSchema(config))
+
+ h.AddTransport(transport.Websocket{
+ KeepAlivePingInterval: 10 * time.Second,
+ Upgrader: websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool { return true },
+ },
+ })
+ h.AddTransport(transport.Options{})
+ h.AddTransport(transport.GET{})
+ h.AddTransport(transport.POST{})
+ h.AddTransport(transport.MultipartForm{})
+
+ h.SetQueryCache(lru.New[*ast.QueryDocument](1000))
+
+ h.Use(extension.Introspection{})
+ h.Use(extension.AutomaticPersistedQuery{
+ Cache: lru.New[string](100),
+ })
if errorOut != nil {
h.Use(&Tracer{Out: errorOut})
@@ -3,6 +3,7 @@
package models
import (
+ "github.com/git-bug/git-bug/cache"
"github.com/git-bug/git-bug/entities/bug"
"github.com/git-bug/git-bug/entities/common"
"github.com/git-bug/git-bug/entity/dag"
@@ -14,6 +15,11 @@ type Authored interface {
IsAuthored()
}
+// An entity (identity, bug, ...).
+type Entity interface {
+ IsEntity()
+}
+
type BugAddCommentAndCloseInput struct {
// A unique identifier for the client performing the mutation.
ClientMutationID *string `json:"clientMutationId,omitempty"`
@@ -183,6 +189,11 @@ type BugEditCommentPayload struct {
Operation *bug.EditCommentOperation `json:"operation"`
}
+type BugEvent struct {
+ Type cache.EntityEventType `json:"type"`
+ Bug BugWrapper `json:"bug"`
+}
+
type BugSetTitleInput struct {
// A unique identifier for the client performing the mutation.
ClientMutationID *string `json:"clientMutationId,omitempty"`
@@ -253,6 +264,11 @@ type BugTimelineItemEdge struct {
Node bug.TimelineItem `json:"node"`
}
+type EntityEvent struct {
+ Type cache.EntityEventType `json:"type"`
+ Entity Entity `json:"entity,omitempty"`
+}
+
type IdentityConnection struct {
Edges []*IdentityEdge `json:"edges"`
Nodes []IdentityWrapper `json:"nodes"`
@@ -265,6 +281,11 @@ type IdentityEdge struct {
Node IdentityWrapper `json:"node"`
}
+type IdentityEvent struct {
+ Type cache.EntityEventType `json:"type"`
+ Identity IdentityWrapper `json:"identity"`
+}
+
type LabelConnection struct {
Edges []*LabelEdge `json:"edges"`
Nodes []common.Label `json:"nodes"`
@@ -308,3 +329,6 @@ type PageInfo struct {
type Query struct {
}
+
+type Subscription struct {
+}
@@ -28,12 +28,15 @@ type BugWrapper interface {
Timeline() ([]bug.TimelineItem, error)
Operations() ([]dag.Operation, error)
+ // IsAuthored is a sign-post method for gqlgen, to mark compliance to an interface.
IsAuthored()
+ // IsEntity is a sign post-method for gqlgen, to mark compliance to an interface.
+ IsEntity()
}
var _ BugWrapper = &lazyBug{}
-// lazyBug is a lazy-loading wrapper that fetch data from the cache (BugExcerpt) in priority,
+// lazyBug is a lazy-loading wrapper that fetches data from the cache (BugExcerpt) in priority,
// and load the complete bug and snapshot only when necessary.
type lazyBug struct {
cache *cache.RepoCache
@@ -75,9 +78,6 @@ func (lb *lazyBug) identity(id entity.Id) (IdentityWrapper, error) {
return &lazyIdentity{cache: lb.cache, excerpt: i}, nil
}
-// Sign post method for gqlgen
-func (lb *lazyBug) IsAuthored() {}
-
func (lb *lazyBug) Id() entity.Id {
return lb.excerpt.Id()
}
@@ -154,6 +154,12 @@ func (lb *lazyBug) Operations() ([]dag.Operation, error) {
return lb.snap.Operations, nil
}
+// IsAuthored is a sign-post method for gqlgen, to mark compliance to an interface.
+func (lb *lazyBug) IsAuthored() {}
+
+// IsEntity is a sign post-method for gqlgen, to mark compliance to an interface.
+func (lb *lazyBug) IsEntity() {}
+
var _ BugWrapper = &loadedBug{}
type loadedBug struct {
@@ -215,3 +221,9 @@ func (l *loadedBug) Timeline() ([]bug.TimelineItem, error) {
func (l *loadedBug) Operations() ([]dag.Operation, error) {
return l.Snapshot.Operations, nil
}
+
+// IsAuthored is a sign-post method for gqlgen, to mark compliance to an interface.
+func (l *loadedBug) IsAuthored() {}
+
+// IsEntity is a sign post-method for gqlgen, to mark compliance to an interface.
+func (l *loadedBug) IsEntity() {}
@@ -21,6 +21,9 @@ type IdentityWrapper interface {
Keys() ([]*identity.Key, error)
DisplayName() string
IsProtected() (bool, error)
+
+ // IsEntity is a sign post-method for gqlgen, to mark compliance to an interface.
+ IsEntity()
}
var _ IdentityWrapper = &lazyIdentity{}
@@ -108,6 +111,9 @@ func (li *lazyIdentity) IsProtected() (bool, error) {
return id.IsProtected(), nil
}
+// IsEntity is a sign post-method for gqlgen, to mark compliance to an interface.
+func (li *lazyIdentity) IsEntity() {}
+
var _ IdentityWrapper = &loadedIdentity{}
type loadedIdentity struct {
@@ -137,3 +143,6 @@ func (l loadedIdentity) Keys() ([]*identity.Key, error) {
func (l loadedIdentity) IsProtected() (bool, error) {
return l.Interface.IsProtected(), nil
}
+
+// IsEntity is a sign post-method for gqlgen, to mark compliance to an interface.
+func (l loadedIdentity) IsEntity() {}
@@ -31,6 +31,12 @@ func (r RootResolver) Mutation() graph.MutationResolver {
}
}
+func (r RootResolver) Subscription() graph.SubscriptionResolver {
+ return &subscriptionResolver{
+ cache: r.MultiRepoCache,
+ }
+}
+
func (RootResolver) Color() graph.ColorResolver {
return &colorResolver{}
}
@@ -0,0 +1,148 @@
+package resolvers
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/git-bug/git-bug/api/graphql/graph"
+ "github.com/git-bug/git-bug/api/graphql/models"
+ "github.com/git-bug/git-bug/cache"
+ "github.com/git-bug/git-bug/entities/bug"
+ "github.com/git-bug/git-bug/entities/identity"
+ "github.com/git-bug/git-bug/entity"
+)
+
+var _ graph.SubscriptionResolver = &subscriptionResolver{}
+
+type subscriptionResolver struct {
+ cache *cache.MultiRepoCache
+}
+
+func (s subscriptionResolver) AllEvents(ctx context.Context, repoRef *string, typename *string) (<-chan *models.EntityEvent, error) {
+ out := make(chan *models.EntityEvent)
+ sub := &subscription[models.EntityEvent]{
+ cache: s.cache,
+ out: out,
+ makeEvent: func(repo *cache.RepoCache, excerpt cache.Excerpt, eventType cache.EntityEventType) *models.EntityEvent {
+ switch excerpt := excerpt.(type) {
+ case *cache.BugExcerpt:
+ return &models.EntityEvent{Type: eventType, Entity: models.NewLazyBug(repo, excerpt)}
+ case *cache.IdentityExcerpt:
+ return &models.EntityEvent{Type: eventType, Entity: models.NewLazyIdentity(repo, excerpt)}
+ default:
+ panic(fmt.Sprintf("unknown excerpt type: %T", excerpt))
+ }
+ },
+ }
+
+ var repoRefStr string
+ if repoRef != nil {
+ repoRefStr = *repoRef
+ }
+
+ var typenameStr string
+ if typename != nil {
+ typenameStr = *typename
+ }
+
+ err := s.cache.RegisterObserver(sub, repoRefStr, typenameStr)
+ if err != nil {
+ return nil, err
+ }
+
+ go func() {
+ <-ctx.Done()
+ s.cache.UnregisterObserver(sub)
+ }()
+
+ return out, nil
+}
+
+func (s subscriptionResolver) BugEvents(ctx context.Context, repoRef *string) (<-chan *models.BugEvent, error) {
+ out := make(chan *models.BugEvent)
+ sub := &subscription[models.BugEvent]{
+ cache: s.cache,
+ out: out,
+ makeEvent: func(repo *cache.RepoCache, excerpt cache.Excerpt, event cache.EntityEventType) *models.BugEvent {
+ return &models.BugEvent{Type: event, Bug: models.NewLazyBug(repo, excerpt.(*cache.BugExcerpt))}
+ },
+ }
+
+ var repoRefStr string
+ if repoRef != nil {
+ repoRefStr = *repoRef
+ }
+
+ err := s.cache.RegisterObserver(sub, repoRefStr, bug.Typename)
+ if err != nil {
+ return nil, err
+ }
+
+ go func() {
+ <-ctx.Done()
+ s.cache.UnregisterObserver(sub)
+ }()
+
+ return out, nil
+}
+
+func (s subscriptionResolver) IdentityEvents(ctx context.Context, repoRef *string) (<-chan *models.IdentityEvent, error) {
+ out := make(chan *models.IdentityEvent)
+ sub := &subscription[models.IdentityEvent]{
+ cache: s.cache,
+ out: out,
+ makeEvent: func(repo *cache.RepoCache, excerpt cache.Excerpt, event cache.EntityEventType) *models.IdentityEvent {
+ return &models.IdentityEvent{Type: event, Identity: models.NewLazyIdentity(repo, excerpt.(*cache.IdentityExcerpt))}
+ },
+ }
+
+ var repoRefStr string
+ if repoRef != nil {
+ repoRefStr = *repoRef
+ }
+
+ err := s.cache.RegisterObserver(sub, repoRefStr, identity.Typename)
+ if err != nil {
+ return nil, err
+ }
+
+ go func() {
+ <-ctx.Done()
+ s.cache.UnregisterObserver(sub)
+ }()
+
+ return out, nil
+}
+
+var _ cache.Observer = &subscription[any]{}
+
+type subscription[eventT any] struct {
+ cache *cache.MultiRepoCache
+ out chan *eventT
+ filter func(cache.Excerpt) bool
+ makeEvent func(repo *cache.RepoCache, excerpt cache.Excerpt, event cache.EntityEventType) *eventT
+}
+
+func (s subscription[eventT]) EntityEvent(event cache.EntityEventType, repoName string, typename string, id entity.Id) {
+ repo, err := s.cache.ResolveRepo(repoName)
+ if err != nil {
+ // something terrible happened
+ return
+ }
+ var excerpt cache.Excerpt
+ switch typename {
+ case bug.Typename:
+ excerpt, err = repo.Bugs().ResolveExcerpt(id)
+ case identity.Typename:
+ excerpt, err = repo.Identities().ResolveExcerpt(id)
+ default:
+ panic(fmt.Sprintf("unknown typename: %s", typename))
+ }
+ if err != nil {
+ return
+ }
+ if s.filter != nil && !s.filter(excerpt) {
+ return
+ }
+ s.out <- s.makeEvent(repo, excerpt, event)
+}
@@ -1,4 +1,4 @@
-type Bug implements Authored {
+type Bug implements Authored & Entity {
"""The identifier for this bug"""
id: ID!
"""The human version (truncated) identifier for this bug"""
@@ -1,5 +1,5 @@
"""Represents an identity"""
-type Identity {
+type Identity implements Entity {
"""The identifier for this identity"""
id: ID!
"""The human version (truncated) identifier for this identity"""
@@ -1,5 +1,6 @@
"""An operation applied to an entity."""
-interface Operation {
+interface Operation
+@goModel(model: "github.com/git-bug/git-bug/entity/dag.Operation") {
"""The identifier of the operation"""
id: ID!
"""The operations author."""
@@ -0,0 +1,29 @@
+type Subscription {
+ """Subscribe to events on all entities. For events on a specific repo you can provide a repo reference. Without it, you get the unique default repo or all repo events."""
+ allEvents(repoRef: String, typename: String): EntityEvent!
+ """Subscribe to identity entity events. For events on a specific repo you can provide a repo reference. Without it, you get the unique default repo or all repo events."""
+ identityEvents(repoRef: String): IdentityEvent!
+ """Subscribe to bug entity events. For events on a specific repo you can provide a repo reference. Without it, you get the unique default repo or all repo events."""
+ bugEvents(repoRef: String): BugEvent!
+}
+
+enum EntityEventType {
+ CREATED
+ UPDATED
+ REMOVED
+}
+
+type EntityEvent {
+ type: EntityEventType!
+ entity: Entity
+}
+
+type IdentityEvent {
+ type: EntityEventType!
+ identity: Identity!
+}
+
+type BugEvent {
+ type: EntityEventType!
+ bug: Bug!
+}
@@ -29,3 +29,12 @@ interface Authored {
"""The author of this object."""
author: Identity!
}
+
+
+"""An entity (identity, bug, ...)."""
+interface Entity {
+ """The identifier for this entity"""
+ id: ID!
+ """The human version (truncated) identifier for this entity"""
+ humanId: String!
+}
@@ -0,0 +1,84 @@
+package cache
+
+import (
+ "fmt"
+ "io"
+ "strconv"
+
+ "github.com/git-bug/git-bug/entity"
+)
+
+type BuildEventType int
+
+const (
+ _ BuildEventType = iota
+ // BuildEventCacheIsBuilt signal that the cache is being built (aka, not skipped)
+ BuildEventCacheIsBuilt
+ // BuildEventRemoveLock signal that an old repo lock has been cleaned
+ BuildEventRemoveLock
+ // BuildEventStarted signal the beginning of a cache build for an entity
+ BuildEventStarted
+ // BuildEventProgress signal progress in the cache building for an entity
+ BuildEventProgress
+ // BuildEventFinished signal the end of a cache build for an entity
+ BuildEventFinished
+)
+
+// BuildEvent carry an event happening during the cache build process.
+type BuildEvent struct {
+ // Err carry an error if the build process failed. If set, no other field matters.
+ Err error
+ // Typename is the name of the entity of which the event relate to. Can be empty if no particular entity is involved.
+ Typename string
+ // Event is the type of the event.
+ Event BuildEventType
+ // Total is the total number of elements being built. Set if Event is BuildEventStarted.
+ Total int64
+ // Progress is the current count of processed elements. Set if Event is BuildEventProgress.
+ Progress int64
+}
+
+type EntityEventType int
+
+const (
+ _ EntityEventType = iota
+ EntityEventCreated
+ EntityEventUpdated
+ EntityEventRemoved
+)
+
+// Observer gets notified of changes in entities in the cache
+type Observer interface {
+ EntityEvent(event EntityEventType, repoName string, typename string, id entity.Id)
+}
+
+func (e EntityEventType) MarshalGQL(w io.Writer) {
+ switch e {
+ case EntityEventCreated:
+ _, _ = w.Write([]byte(strconv.Quote("CREATED")))
+ case EntityEventUpdated:
+ _, _ = w.Write([]byte(strconv.Quote("UPDATED")))
+ case EntityEventRemoved:
+ _, _ = w.Write([]byte(strconv.Quote("REMOVED")))
+ default:
+ panic("missing case")
+ }
+}
+
+func (e *EntityEventType) UnmarshalGQL(v interface{}) error {
+ str, ok := v.(string)
+ if !ok {
+ return fmt.Errorf("enums must be strings")
+ }
+ switch str {
+ case "CREATED":
+ *e = EntityEventCreated
+ case "UPDATED":
+ *e = EntityEventUpdated
+ case "REMOVED":
+ *e = EntityEventRemoved
+ default:
+ return fmt.Errorf("%s is not a valid EntityEventType", str)
+ }
+ return nil
+}
@@ -8,17 +8,17 @@ import (
"github.com/git-bug/git-bug/query"
)
-// Filter is a predicate that match a subset of bugs
+// Filter is a predicate that matches a subset of bugs
type Filter func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool
-// StatusFilter return a Filter that match a bug status
+// StatusFilter return a Filter that matches a bug status
func StatusFilter(status common.Status) Filter {
return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
return excerpt.Status == status
}
}
-// AuthorFilter return a Filter that match a bug author
+// AuthorFilter return a Filter that matches a bug author
func AuthorFilter(query string) Filter {
return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
query = strings.ToLower(query)
@@ -32,7 +32,7 @@ func AuthorFilter(query string) Filter {
}
}
-// MetadataFilter return a Filter that match a bug metadata at creation time
+// MetadataFilter return a Filter that matches a bug metadata at creation time
func MetadataFilter(pair query.StringPair) Filter {
return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
if value, ok := excerpt.CreateMetadata[pair.Key]; ok {
@@ -42,7 +42,7 @@ func MetadataFilter(pair query.StringPair) Filter {
}
}
-// LabelFilter return a Filter that match a label
+// LabelFilter return a Filter that matches a label
func LabelFilter(label string) Filter {
return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
for _, l := range excerpt.Labels {
@@ -54,7 +54,7 @@ func LabelFilter(label string) Filter {
}
}
-// ActorFilter return a Filter that match a bug actor
+// ActorFilter return a Filter that matches a bug actor
func ActorFilter(query string) Filter {
return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
query = strings.ToLower(query)
@@ -73,7 +73,7 @@ func ActorFilter(query string) Filter {
}
}
-// ParticipantFilter return a Filter that match a bug participant
+// ParticipantFilter return a Filter that matches a bug participant
func ParticipantFilter(query string) Filter {
return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
query = strings.ToLower(query)
@@ -92,7 +92,7 @@ func ParticipantFilter(query string) Filter {
}
}
-// TitleFilter return a Filter that match if the title contains the given query
+// TitleFilter return a Filter that matches if the title contains the given query
func TitleFilter(query string) Filter {
return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
return strings.Contains(
@@ -102,7 +102,7 @@ func TitleFilter(query string) Filter {
}
}
-// NoLabelFilter return a Filter that match the absence of labels
+// NoLabelFilter return a Filter that matches the absence of labels
func NoLabelFilter() Filter {
return func(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
return len(excerpt.Labels) == 0
@@ -154,7 +154,7 @@ func compileMatcher(filters query.Filters) *Matcher {
return result
}
-// Match check if a bug match the set of filters
+// Match check if a bug matches the set of filters
func (f *Matcher) Match(excerpt *BugExcerpt, resolvers entity.Resolvers) bool {
if match := f.orMatch(f.Status, excerpt, resolvers); !match {
return false
@@ -1,8 +1,6 @@
package cache
import (
- "fmt"
-
"github.com/git-bug/git-bug/entities/identity"
"github.com/git-bug/git-bug/entity"
"github.com/git-bug/git-bug/repository"
@@ -106,17 +104,7 @@ func (c *RepoCacheIdentity) finishIdentity(i *identity.Identity, metadata map[st
return nil, err
}
- c.mu.Lock()
- if _, has := c.cached[i.Id()]; has {
- return nil, fmt.Errorf("identity %s already exist in the cache", i.Id())
- }
-
- cached := NewIdentityCache(i, c.repo, c.entityUpdated)
- c.cached[i.Id()] = cached
- c.mu.Unlock()
-
- // force the write of the excerpt
- err = c.entityUpdated(i.Id())
+ cached, err := c.add(i)
if err != nil {
return nil, err
}
@@ -20,11 +20,11 @@ func NewMultiRepoCache() *MultiRepoCache {
}
}
-// RegisterRepository register a named repository. Use this for multi-repo setup
+// RegisterRepository registers a named repository. Use this for multi-repo setup
func (c *MultiRepoCache) RegisterRepository(repo repository.ClockedRepo, name string) (*RepoCache, chan BuildEvent) {
r, events := NewNamedRepoCache(repo, name)
- // intercept events to make sure the cache building process succeed properly
+ // intercept events to make sure the cache building process succeeds properly
out := make(chan BuildEvent)
go func() {
defer close(out)
@@ -42,12 +42,12 @@ func (c *MultiRepoCache) RegisterRepository(repo repository.ClockedRepo, name st
return r, out
}
-// RegisterDefaultRepository register an unnamed repository. Use this for single-repo setup
+// RegisterDefaultRepository registers an unnamed repository. Use this for single-repo setup
func (c *MultiRepoCache) RegisterDefaultRepository(repo repository.ClockedRepo) (*RepoCache, chan BuildEvent) {
return c.RegisterRepository(repo, defaultRepoName)
}
-// DefaultRepo retrieve the default repository
+// DefaultRepo retrieves the default repository
func (c *MultiRepoCache) DefaultRepo() (*RepoCache, error) {
if len(c.repos) != 1 {
return nil, fmt.Errorf("repository is not unique")
@@ -60,7 +60,7 @@ func (c *MultiRepoCache) DefaultRepo() (*RepoCache, error) {
panic("unreachable")
}
-// ResolveRepo retrieve a repository by name
+// ResolveRepo retrieves a repository by name
func (c *MultiRepoCache) ResolveRepo(name string) (*RepoCache, error) {
r, ok := c.repos[name]
if !ok {
@@ -69,6 +69,46 @@ func (c *MultiRepoCache) ResolveRepo(name string) (*RepoCache, error) {
return r, nil
}
+// RegisterObserver registers an Observer on repo and entity, according to nameFilter and typename.
+// - if nameFilter is empty, the observer is registered on all available repo
+// - if nameFilter is not empty, the observer is registered on the repo with the matching name
+// - if typename is empty, the observer is registered on all available entities
+// - if typename is not empty, the observer is registered on the matching entity type only
+func (c *MultiRepoCache) RegisterObserver(observer Observer, nameFilter string, typename string) error {
+ if nameFilter == "" {
+ for repoName, repo := range c.repos {
+ if typename == "" {
+ repo.registerAllObservers(repoName, observer)
+ } else {
+ if err := repo.registerObserver(repoName, typename, observer); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ }
+
+ r, err := c.ResolveRepo(nameFilter)
+ if err != nil {
+ return err
+ }
+ if typename == "" {
+ r.registerAllObservers(r.Name(), observer)
+ } else {
+ if err := r.registerObserver(r.Name(), typename, observer); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// UnregisterObserver deregisters the observer from all repos and all entity types.
+func (c *MultiRepoCache) UnregisterObserver(observer Observer) {
+ for _, repo := range c.repos {
+ repo.unregisterAllObservers(observer)
+ }
+}
+
// Close will do anything that is needed to close the cache properly
func (c *MultiRepoCache) Close() error {
for _, cachedRepo := range c.repos {
@@ -5,8 +5,11 @@ import (
"io"
"os"
"strconv"
+ "strings"
"sync"
+ "github.com/git-bug/git-bug/entities/bug"
+ "github.com/git-bug/git-bug/entities/identity"
"github.com/git-bug/git-bug/entity"
"github.com/git-bug/git-bug/repository"
"github.com/git-bug/git-bug/util/multierr"
@@ -35,6 +38,8 @@ type cacheMgmt interface {
RemoveAll() error
MergeAll(remote string) <-chan entity.MergeResult
GetNamespace() string
+ RegisterObserver(repoName string, observer Observer)
+ UnregisterObserver(observer Observer)
Close() error
}
@@ -42,14 +47,14 @@ type cacheMgmt interface {
//
// 1. After being loaded, a Bug is kept in memory in the cache, allowing for fast
// access later.
-// 2. The cache maintain in memory and on disk a pre-digested excerpt for each bug,
+// 2. The cache maintains in memory and on disk a pre-digested excerpt for each bug,
// allowing for fast querying the whole set of bugs without having to load
// them individually.
-// 3. The cache guarantee that a single instance of a Bug is loaded at once, avoiding
+// 3. The cache guarantees that a single instance of a Bug is loaded at once, avoiding
// loss of data that we could have with multiple copies in the same process.
-// 4. The same way, the cache maintain in memory a single copy of the loaded identities.
+// 4. The same way, the cache maintains in memory a single copy of the loaded identities.
//
-// The cache also protect the on-disk data by locking the git repository for its
+// The cache also protects the on-disk data by locking the git repository for its
// own usage, by writing a lock file. Of course, normal git operations are not
// affected, only git-bug related one.
type RepoCache struct {
@@ -101,7 +106,7 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan
&BugExcerpt{}: entity.ResolverFunc[*BugExcerpt](c.bugs.ResolveExcerpt),
}
- // small buffer so that below functions can emit an event without blocking
+ // small buffer so that the functions below can emit an event without blocking
events := make(chan BuildEvent)
go func() {
@@ -206,36 +211,6 @@ func (c *RepoCache) Close() error {
return c.repo.LocalStorage().Remove(lockfile)
}
-type BuildEventType int
-
-const (
- _ BuildEventType = iota
- // BuildEventCacheIsBuilt signal that the cache is being built (aka, not skipped)
- BuildEventCacheIsBuilt
- // BuildEventRemoveLock signal that an old repo lock has been cleaned
- BuildEventRemoveLock
- // BuildEventStarted signal the beginning of a cache build for an entity
- BuildEventStarted
- // BuildEventProgress signal progress in the cache building for an entity
- BuildEventProgress
- // BuildEventFinished signal the end of a cache build for an entity
- BuildEventFinished
-)
-
-// BuildEvent carry an event happening during the cache build process.
-type BuildEvent struct {
- // Err carry an error if the build process failed. If set, no other field matter.
- Err error
- // Typename is the name of the entity of which the event relate to. Can be empty if not particular entity is involved.
- Typename string
- // Event is the type of the event.
- Event BuildEventType
- // Total is the total number of element being built. Set if Event is BuildEventStarted.
- Total int64
- // Progress is the current count of processed element. Set if Event is BuildEventProgress.
- Progress int64
-}
-
func (c *RepoCache) buildCache(events chan BuildEvent) {
events <- BuildEvent{Event: BuildEventCacheIsBuilt}
@@ -257,6 +232,34 @@ func (c *RepoCache) buildCache(events chan BuildEvent) {
wg.Wait()
}
+func (c *RepoCache) registerObserver(repoName string, typename string, observer Observer) error {
+ switch typename {
+ case bug.Typename:
+ c.bugs.RegisterObserver(repoName, observer)
+ case identity.Typename:
+ c.identities.RegisterObserver(repoName, observer)
+ default:
+ var allTypenames []string
+ for _, subcache := range c.subcaches {
+ allTypenames = append(allTypenames, subcache.Typename())
+ }
+ return fmt.Errorf("unknown typename `%s`, available types are [%s]", typename, strings.Join(allTypenames, ", "))
+ }
+ return nil
+}
+
+func (c *RepoCache) registerAllObservers(repoName string, observer Observer) {
+ for _, subcache := range c.subcaches {
+ subcache.RegisterObserver(repoName, observer)
+ }
+}
+
+func (c *RepoCache) unregisterAllObservers(observer Observer) {
+ for _, subcache := range c.subcaches {
+ subcache.UnregisterObserver(observer)
+ }
+}
+
// repoIsAvailable check is the given repository is locked by a Cache.
// Note: this is a smart function that will clean the lock file if the
// corresponding process is not there anymore.
@@ -16,6 +16,30 @@ import (
"github.com/git-bug/git-bug/repository"
)
+type observerEvent struct {
+ typename string
+ id entity.Id
+}
+
+var _ Observer = &observer{}
+
+type observer struct {
+ created []observerEvent
+ updated []observerEvent
+ removed []observerEvent
+}
+
+func (o *observer) EntityEvent(event EntityEventType, _ string, typename string, id entity.Id) {
+ switch event {
+ case EntityEventCreated:
+ o.created = append(o.created, observerEvent{typename, id})
+ case EntityEventUpdated:
+ o.updated = append(o.updated, observerEvent{typename, id})
+ case EntityEventRemoved:
+ o.removed = append(o.removed, observerEvent{typename, id})
+ }
+}
+
func TestCache(t *testing.T) {
f := test.NewFlaky(t, &test.FlakyOptions{
MaxAttempts: 5,
@@ -23,23 +47,34 @@ func TestCache(t *testing.T) {
f.Run(func(t testing.TB) {
repo := repository.CreateGoGitTestRepo(t, false)
+
indexCount := func(t testing.TB, name string) uint64 {
t.Helper()
-
idx, err := repo.GetIndex(name)
require.NoError(t, err)
count, err := idx.DocCount()
require.NoError(t, err)
-
return count
}
+ assertOberserverEvent := func(obs observer, created, updated, removed int) {
+ t.Helper()
+ require.Len(t, obs.created, created)
+ require.Len(t, obs.updated, updated)
+ require.Len(t, obs.removed, removed)
+ }
cache, err := NewRepoCacheNoEvents(repo)
require.NoError(t, err)
+ var obsIdentity, obsBug observer
+ require.NoError(t, cache.registerObserver("repotest", identity.Typename, &obsIdentity))
+ require.NoError(t, cache.registerObserver("repotest", bug.Typename, &obsBug))
+
// Create, set and get user identity
iden1, err := cache.Identities().New("RenĂŠ Descartes", "rene@descartes.fr")
require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 1, 0, 0)
+ assertOberserverEvent(obsBug, 0, 0, 0)
err = cache.SetUserIdentity(iden1)
require.NoError(t, err)
userIden, err := cache.GetUserIdentity()
@@ -49,11 +84,13 @@ func TestCache(t *testing.T) {
// it's possible to create two identical identities
iden2, err := cache.Identities().New("RenĂŠ Descartes", "rene@descartes.fr")
require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 2, 0, 0)
+ assertOberserverEvent(obsBug, 0, 0, 0)
// Two identical identities yield a different id
require.NotEqual(t, iden1.Id(), iden2.Id())
- // There is now two identities in the cache
+ // There are now two identities in the cache
require.Len(t, cache.Identities().AllIds(), 2)
require.Len(t, cache.identities.excerpts, 2)
require.Len(t, cache.identities.cached, 2)
@@ -63,10 +100,14 @@ func TestCache(t *testing.T) {
// Create a bug
bug1, _, err := cache.Bugs().New("title", "message")
require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 2, 0, 0)
+ assertOberserverEvent(obsBug, 1, 0, 0)
// It's possible to create two identical bugs
bug2, _, err := cache.Bugs().New("title", "marker")
require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 2, 0, 0)
+ assertOberserverEvent(obsBug, 2, 0, 0)
// two identical bugs yield a different id
require.NotEqual(t, bug1.Id(), bug2.Id())
@@ -106,6 +147,12 @@ func TestCache(t *testing.T) {
require.NoError(t, err)
require.Len(t, res, 1)
+ // Updating
+ _, _, err = bug1.AddComment("new comment")
+ require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 2, 0, 0)
+ assertOberserverEvent(obsBug, 2, 1, 0)
+
// Close
require.NoError(t, cache.Close())
require.Empty(t, cache.bugs.cached)
@@ -117,6 +164,8 @@ func TestCache(t *testing.T) {
// to check the signatures, we also load the identity used above
cache, err = NewRepoCacheNoEvents(repo)
require.NoError(t, err)
+ require.NoError(t, cache.registerObserver("repotest", identity.Typename, &obsIdentity))
+ require.NoError(t, cache.registerObserver("repotest", bug.Typename, &obsBug))
require.Len(t, cache.bugs.cached, 0)
require.Len(t, cache.bugs.excerpts, 2)
@@ -150,8 +199,12 @@ func TestCache(t *testing.T) {
// Remove + RemoveAll
err = cache.Identities().Remove(iden1.Id().String()[:10])
require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 2, 0, 1)
+ assertOberserverEvent(obsBug, 2, 1, 0)
err = cache.Bugs().Remove(bug1.Id().String()[:10])
require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 2, 0, 1)
+ assertOberserverEvent(obsBug, 2, 1, 1)
require.Len(t, cache.bugs.cached, 0)
require.Len(t, cache.bugs.excerpts, 1)
require.Len(t, cache.identities.cached, 0)
@@ -161,11 +214,17 @@ func TestCache(t *testing.T) {
_, err = cache.Identities().New("RenĂŠ Descartes", "rene@descartes.fr")
require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 3, 0, 1)
+ assertOberserverEvent(obsBug, 2, 1, 1)
_, _, err = cache.Bugs().NewRaw(iden2, time.Now().Unix(), "title", "message", nil, nil)
require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 3, 0, 1)
+ assertOberserverEvent(obsBug, 3, 1, 1)
err = cache.RemoveAll()
require.NoError(t, err)
+ assertOberserverEvent(obsIdentity, 3, 0, 3)
+ assertOberserverEvent(obsBug, 3, 1, 3)
require.Len(t, cache.bugs.cached, 0)
require.Len(t, cache.bugs.excerpts, 0)
require.Len(t, cache.identities.cached, 0)
@@ -59,6 +59,9 @@ type SubCache[EntityT entity.Interface, ExcerptT Excerpt, CacheT CacheEntity] st
excerpts map[entity.Id]ExcerptT
cached map[entity.Id]CacheT
lru lruIdCache
+
+ muObservers sync.RWMutex
+ observers map[Observer]string // observer --> repo name
}
func NewSubCache[EntityT entity.Interface, ExcerptT Excerpt, CacheT CacheEntity](
@@ -332,6 +335,21 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Close() error {
return nil
}
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) RegisterObserver(repoName string, observer Observer) {
+ sc.muObservers.Lock()
+ defer sc.muObservers.Unlock()
+ if sc.observers == nil {
+ sc.observers = make(map[Observer]string)
+ }
+ sc.observers[observer] = repoName
+}
+
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) UnregisterObserver(observer Observer) {
+ sc.muObservers.Lock()
+ defer sc.muObservers.Unlock()
+ delete(sc.observers, observer)
+}
+
// AllIds return all known bug ids
func (sc *SubCache[EntityT, ExcerptT, CacheT]) AllIds() []entity.Id {
sc.mu.RLock()
@@ -392,7 +410,7 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveMatcher(f func(ExcerptT) b
return sc.Resolve(id)
}
-// ResolveExcerpt retrieve an Excerpt matching the exact given id
+// ResolveExcerpt retrieves an Excerpt matching the exact given id
func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerpt(id entity.Id) (ExcerptT, error) {
sc.mu.RLock()
defer sc.mu.RUnlock()
@@ -405,7 +423,7 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerpt(id entity.Id) (Exc
return excerpt, nil
}
-// ResolveExcerptPrefix retrieve an Excerpt matching an id prefix. It fails if multiple
+// ResolveExcerptPrefix retrieves an Excerpt matching an id prefix. It fails if multiple
// entities match.
func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerptPrefix(prefix string) (ExcerptT, error) {
return sc.ResolveExcerptMatcher(func(excerpt ExcerptT) bool {
@@ -413,6 +431,7 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerptPrefix(prefix strin
})
}
+// ResolveExcerptMatcher retrieves an Excerpt selected by the given matcher function.
func (sc *SubCache[EntityT, ExcerptT, CacheT]) ResolveExcerptMatcher(f func(ExcerptT) bool) (ExcerptT, error) {
id, err := sc.resolveMatcher(f)
if err != nil {
@@ -460,11 +479,14 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) add(e EntityT) (CacheT, error) {
sc.evictIfNeeded()
// force the write of the excerpt
- err := sc.entityUpdated(e.Id())
+ err := sc.updateExcerptAndIndex(e.Id())
if err != nil {
return *new(CacheT), err
}
+ // defer to notify after the release of the mutex
+ defer sc.notifyObservers(EntityEventCreated, e.Id())
+
return cached, nil
}
@@ -498,6 +520,9 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Remove(prefix string) error {
return err
}
+ // defer to notify after the release of the mutex
+ defer sc.notifyObservers(EntityEventRemoved, e.Id())
+
return sc.write()
}
@@ -510,12 +535,16 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) RemoveAll() error {
return err
}
+ ids := make(map[entity.Id]struct{})
+
for id, _ := range sc.cached {
delete(sc.cached, id)
sc.lru.Remove(id)
+ ids[id] = struct{}{}
}
for id, _ := range sc.excerpts {
delete(sc.excerpts, id)
+ ids[id] = struct{}{}
}
index, err := sc.repo.GetIndex(sc.namespace)
@@ -530,6 +559,13 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) RemoveAll() error {
return err
}
+ // defer to notify after the release of the mutex
+ defer func() {
+ for id := range ids {
+ sc.notifyObservers(EntityEventRemoved, id)
+ }
+ }()
+
return sc.write()
}
@@ -555,7 +591,19 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) MergeAll(remote string) <-chan en
}
switch result.Status {
- case entity.MergeStatusNew, entity.MergeStatusUpdated:
+ case entity.MergeStatusNew:
+ e := result.Entity.(EntityT)
+ cached := sc.makeCached(e, sc.entityUpdated)
+
+ sc.mu.Lock()
+ sc.excerpts[result.Id] = sc.makeExcerpt(cached)
+ // might as well keep them in memory
+ sc.cached[result.Id] = cached
+ sc.mu.Unlock()
+ sc.notifyObservers(EntityEventCreated, result.Id)
+
+ case entity.MergeStatusUpdated:
+ // TODO: can that result in multiple copy of the same entity?
e := result.Entity.(EntityT)
cached := sc.makeCached(e, sc.entityUpdated)
@@ -564,6 +612,7 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) MergeAll(remote string) <-chan en
// might as well keep them in memory
sc.cached[result.Id] = cached
sc.mu.Unlock()
+ sc.notifyObservers(EntityEventUpdated, result.Id)
}
}
@@ -578,12 +627,27 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) MergeAll(remote string) <-chan en
}
+// GetNamespace expose the namespace in git where entities are located.
func (sc *SubCache[EntityT, ExcerptT, CacheT]) GetNamespace() string {
return sc.namespace
}
// entityUpdated is a callback to trigger when the excerpt of an entity changed
func (sc *SubCache[EntityT, ExcerptT, CacheT]) entityUpdated(id entity.Id) error {
+ sc.notifyObservers(EntityEventUpdated, id)
+ return sc.updateExcerptAndIndex(id)
+}
+
+// notifyObservers notifies all the observers when something happening for an entity
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) notifyObservers(event EntityEventType, id entity.Id) {
+ sc.muObservers.RLock()
+ for observer, repoName := range sc.observers {
+ observer.EntityEvent(event, repoName, sc.typename, id)
+ }
+ sc.muObservers.RUnlock()
+}
+
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) updateExcerptAndIndex(id entity.Id) error {
sc.mu.Lock()
e, ok := sc.cached[id]
if !ok {
@@ -597,7 +661,6 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) entityUpdated(id entity.Id) error
return errors.New("entity missing from cache")
}
sc.lru.Get(id)
- // sc.excerpts[id] = bug2.NewBugExcerpt(b.bug, b.Snapshot())
sc.excerpts[id] = sc.makeExcerpt(e)
sc.mu.Unlock()
@@ -38,7 +38,7 @@ type Interface interface {
// Bug holds the data of a bug thread, organized in a way close to
// how it will be persisted inside Git. This is the data structure
-// used to merge two different version of the same Bug.
+// used to merge two different versions of the same Bug.
type Bug struct {
*dag.Entity
}
@@ -48,5 +48,5 @@ func (c Comment) FormatTime() string {
return c.unixTime.Time().Format("Mon Jan 2 15:04:05 2006 -0700")
}
-// IsAuthored is a sign post method for gqlgen
+// IsAuthored is a sign-post method for gqlgen, to mark compliance to an interface.
func (c Comment) IsAuthored() {}
@@ -79,7 +79,7 @@ type AddCommentTimelineItem struct {
CommentTimelineItem
}
-// IsAuthored is a sign post method for gqlgen
+// IsAuthored is a sign post-method for gqlgen, to mark compliance to an interface.
func (a *AddCommentTimelineItem) IsAuthored() {}
// AddComment is a convenience function to add a comment to a bug
@@ -97,7 +97,7 @@ type CreateTimelineItem struct {
CommentTimelineItem
}
-// IsAuthored is a sign post method for gqlgen
+// IsAuthored is a sign post-method for gqlgen, to mark compliance to an interface.
func (c *CreateTimelineItem) IsAuthored() {}
// Create is a convenience function to create a bug
@@ -117,7 +117,7 @@ func (l LabelChangeTimelineItem) CombinedId() entity.CombinedId {
return l.combinedId
}
-// IsAuthored is a sign post method for gqlgen
+// IsAuthored is a sign post-method for gqlgen, to mark compliance to an interface.
func (l *LabelChangeTimelineItem) IsAuthored() {}
// ChangeLabels is a convenience function to change labels on a bug
@@ -68,7 +68,7 @@ func (s SetStatusTimelineItem) CombinedId() entity.CombinedId {
return s.combinedId
}
-// IsAuthored is a sign post method for gqlgen
+// IsAuthored is a sign post-method for gqlgen, to mark compliance to an interface.
func (s *SetStatusTimelineItem) IsAuthored() {}
// Open is a convenience function to change a bugs state to Open
@@ -80,7 +80,7 @@ func (s SetTitleTimelineItem) CombinedId() entity.CombinedId {
return s.combinedId
}
-// IsAuthored is a sign post method for gqlgen
+// IsAuthored is a sign post-method for gqlgen, to mark compliance to an interface.
func (s *SetTitleTimelineItem) IsAuthored() {}
// SetTitle is a convenience function to change a bugs title
@@ -155,6 +155,3 @@ func (snap *Snapshot) HasAnyActor(ids ...entity.Id) bool {
}
return false
}
-
-// IsAuthored is a sign post method for gqlgen
-func (snap *Snapshot) IsAuthored() {}