Detailed changes
@@ -53,6 +53,7 @@ type ResolverRoot interface {
Mutation() MutationResolver
Query() QueryResolver
Repository() RepositoryResolver
+ Subscription() SubscriptionResolver
}
type DirectiveRoot struct {
@@ -115,6 +116,11 @@ type ComplexityRoot struct {
MessageIsEmpty func(childComplexity int) int
}
+ BugChange struct {
+ Bug func(childComplexity int) int
+ Type func(childComplexity int) int
+ }
+
BugChangeLabelPayload struct {
Bug func(childComplexity int) int
ClientMutationID func(childComplexity int) int
@@ -371,6 +377,10 @@ type ComplexityRoot struct {
UserIdentity func(childComplexity int) int
ValidLabels func(childComplexity int, after *string, before *string, first *int, last *int) int
}
+
+ Subscription struct {
+ BugChanged func(childComplexity int, repoRef *string, query *string) int
+ }
}
type executableSchema struct {
@@ -683,6 +693,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.BugAddCommentTimelineItem.MessageIsEmpty(childComplexity), true
+ case "BugChange.bug":
+ if e.complexity.BugChange.Bug == nil {
+ break
+ }
+
+ return e.complexity.BugChange.Bug(childComplexity), true
+
+ case "BugChange.type":
+ if e.complexity.BugChange.Type == nil {
+ break
+ }
+
+ return e.complexity.BugChange.Type(childComplexity), true
+
case "BugChangeLabelPayload.bug":
if e.complexity.BugChangeLabelPayload.Bug == nil {
break
@@ -1780,6 +1804,18 @@ 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.bugChanged":
+ if e.complexity.Subscription.BugChanged == nil {
+ break
+ }
+
+ args, err := ec.field_Subscription_bugChanged_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Subscription.BugChanged(childComplexity, args["repoRef"].(*string), args["query"].(*string)), true
+
}
return 0, false
}
@@ -1842,6 +1878,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(),
}
@@ -2573,6 +2626,20 @@ type Mutation # See each entity mutations
OPEN
CLOSED
}
+`, BuiltIn: false},
+ {Name: "../schema/subscription.graphql", Input: `type Subscription {
+ bugChanged(repoRef: String, query: String): BugChange!
+}
+
+enum ChangeType {
+ CREATED
+ UPDATED
+}
+
+type BugChange {
+ type: ChangeType!
+ bug: Bug!
+}
`, BuiltIn: false},
{Name: "../schema/types.graphql", Input: `scalar CombinedId
scalar Time
@@ -0,0 +1,382 @@
+// 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/vektah/gqlparser/v2/ast"
+)
+
+// region ************************** generated!.gotpl **************************
+
+type SubscriptionResolver interface {
+ BugChanged(ctx context.Context, repoRef *string, query *string) (<-chan *models.BugChange, error)
+}
+
+// endregion ************************** generated!.gotpl **************************
+
+// region ***************************** args.gotpl *****************************
+
+func (ec *executionContext) field_Subscription_bugChanged_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Subscription_bugChanged_argsRepoRef(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["repoRef"] = arg0
+ arg1, err := ec.field_Subscription_bugChanged_argsQuery(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["query"] = arg1
+ return args, nil
+}
+func (ec *executionContext) field_Subscription_bugChanged_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_bugChanged_argsQuery(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (*string, error) {
+ if _, ok := rawArgs["query"]; !ok {
+ var zeroVal *string
+ return zeroVal, nil
+ }
+
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("query"))
+ if tmp, ok := rawArgs["query"]; 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) _BugChange_type(ctx context.Context, field graphql.CollectedField, obj *models.BugChange) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BugChange_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.(models.ChangeType)
+ fc.Result = res
+ return ec.marshalNChangeType2githubácomágitábugágitábugáapiágraphqlámodelsáChangeType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_BugChange_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BugChange",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type ChangeType does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _BugChange_bug(ctx context.Context, field graphql.CollectedField, obj *models.BugChange) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_BugChange_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_BugChange_bug(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "BugChange",
+ 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) _Subscription_bugChanged(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) {
+ fc, err := ec.fieldContext_Subscription_bugChanged(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().BugChanged(rctx, fc.Args["repoRef"].(*string), fc.Args["query"].(*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.BugChange):
+ if !ok {
+ return nil
+ }
+ return graphql.WriterFunc(func(w io.Writer) {
+ w.Write([]byte{'{'})
+ graphql.MarshalString(field.Alias).MarshalGQL(w)
+ w.Write([]byte{':'})
+ ec.marshalNBugChange2ágithubácomágitábugágitábugáapiágraphqlámodelsáBugChange(ctx, field.Selections, res).MarshalGQL(w)
+ w.Write([]byte{'}'})
+ })
+ case <-ctx.Done():
+ return nil
+ }
+ }
+}
+
+func (ec *executionContext) fieldContext_Subscription_bugChanged(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_BugChange_type(ctx, field)
+ case "bug":
+ return ec.fieldContext_BugChange_bug(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type BugChange", 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_bugChanged_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 bugChangeImplementors = []string{"BugChange"}
+
+func (ec *executionContext) _BugChange(ctx context.Context, sel ast.SelectionSet, obj *models.BugChange) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, bugChangeImplementors)
+
+ 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("BugChange")
+ case "type":
+ out.Values[i] = ec._BugChange_type(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "bug":
+ out.Values[i] = ec._BugChange_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 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 "bugChanged":
+ return ec._Subscription_bugChanged(ctx, fields[0])
+ default:
+ panic("unknown field " + strconv.Quote(fields[0].Name))
+ }
+}
+
+// endregion **************************** object.gotpl ****************************
+
+// region ***************************** type.gotpl *****************************
+
+func (ec *executionContext) marshalNBugChange2githubácomágitábugágitábugáapiágraphqlámodelsáBugChange(ctx context.Context, sel ast.SelectionSet, v models.BugChange) graphql.Marshaler {
+ return ec._BugChange(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNBugChange2ágithubácomágitábugágitábugáapiágraphqlámodelsáBugChange(ctx context.Context, sel ast.SelectionSet, v *models.BugChange) 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._BugChange(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalNChangeType2githubácomágitábugágitábugáapiágraphqlámodelsáChangeType(ctx context.Context, v any) (models.ChangeType, error) {
+ var res models.ChangeType
+ err := res.UnmarshalGQL(v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNChangeType2githubácomágitábugágitábugáapiágraphqlámodelsáChangeType(ctx context.Context, sel ast.SelectionSet, v models.ChangeType) graphql.Marshaler {
+ return v
+}
+
+// endregion ***************************** type.gotpl *****************************
@@ -23,6 +23,9 @@ type Handler struct {
func NewHandler(mrc *cache.MultiRepoCache, errorOut io.Writer) Handler {
rootResolver := resolvers.NewRootResolver(mrc)
config := graph.Config{Resolvers: rootResolver}
+
+ // TODO? https://gqlgen.com/recipes/subscriptions/ says to configure a WS upgrader, but
+ // handler.NewDefaultServer doesn't do it.
h := handler.NewDefaultServer(graph.NewExecutableSchema(config))
if errorOut != nil {
@@ -3,6 +3,11 @@
package models
import (
+ "bytes"
+ "fmt"
+ "io"
+ "strconv"
+
"github.com/git-bug/git-bug/entities/bug"
"github.com/git-bug/git-bug/entities/common"
"github.com/git-bug/git-bug/entity/dag"
@@ -84,6 +89,11 @@ type BugAddCommentPayload struct {
Operation *bug.AddCommentOperation `json:"operation"`
}
+type BugChange struct {
+ Type ChangeType `json:"type"`
+ Bug BugWrapper `json:"bug"`
+}
+
type BugChangeLabelInput struct {
// A unique identifier for the client performing the mutation.
ClientMutationID *string `json:"clientMutationId,omitempty"`
@@ -308,3 +318,61 @@ type PageInfo struct {
type Query struct {
}
+
+type Subscription struct {
+}
+
+type ChangeType string
+
+const (
+ ChangeTypeCreated ChangeType = "CREATED"
+ ChangeTypeUpdated ChangeType = "UPDATED"
+)
+
+var AllChangeType = []ChangeType{
+ ChangeTypeCreated,
+ ChangeTypeUpdated,
+}
+
+func (e ChangeType) IsValid() bool {
+ switch e {
+ case ChangeTypeCreated, ChangeTypeUpdated:
+ return true
+ }
+ return false
+}
+
+func (e ChangeType) String() string {
+ return string(e)
+}
+
+func (e *ChangeType) UnmarshalGQL(v any) error {
+ str, ok := v.(string)
+ if !ok {
+ return fmt.Errorf("enums must be strings")
+ }
+
+ *e = ChangeType(str)
+ if !e.IsValid() {
+ return fmt.Errorf("%s is not a valid ChangeType", str)
+ }
+ return nil
+}
+
+func (e ChangeType) MarshalGQL(w io.Writer) {
+ fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+func (e *ChangeType) UnmarshalJSON(b []byte) error {
+ s, err := strconv.Unquote(string(b))
+ if err != nil {
+ return err
+ }
+ return e.UnmarshalGQL(s)
+}
+
+func (e ChangeType) MarshalJSON() ([]byte, error) {
+ var buf bytes.Buffer
+ e.MarshalGQL(&buf)
+ return buf.Bytes(), nil
+}
@@ -33,7 +33,7 @@ type BugWrapper interface {
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
@@ -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,75 @@
+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/entity"
+)
+
+var _ graph.SubscriptionResolver = &subscriptionResolver{}
+
+type subscriptionResolver struct {
+ cache *cache.MultiRepoCache
+}
+
+func (s subscriptionResolver) BugChanged(ctx context.Context, repoRef *string, query *string) (<-chan *models.BugChange, error) {
+ var repo *cache.RepoCache
+ var err error
+
+ if repoRef == nil {
+ repo, err = s.cache.DefaultRepo()
+ } else {
+ repo, err = s.cache.ResolveRepo(*repoRef)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ out := make(chan *models.BugChange)
+ sub := bugSubscription{out: out, repo: repo}
+ repo.RegisterObserver(bug.Typename, sub)
+
+ go func() {
+ <-ctx.Done()
+ repo.RegisterObserver(bug.Typename, sub)
+ }()
+
+ return out, nil
+}
+
+type bugSubscription struct {
+ out chan *models.BugChange
+ repo *cache.RepoCache
+}
+
+func (bs bugSubscription) EntityCreated(_ string, id entity.Id) {
+ excerpt, err := bs.repo.Bugs().ResolveExcerpt(id)
+ if err != nil {
+ // Should never happen
+ fmt.Printf("bug in the cache: could not resolve excerpt for %s: %s\n", id, err)
+ return
+ }
+ bs.out <- &models.BugChange{
+ Type: models.ChangeTypeCreated,
+ Bug: models.NewLazyBug(bs.repo, excerpt),
+ }
+}
+
+func (bs bugSubscription) EntityUpdated(_ string, id entity.Id) {
+ excerpt, err := bs.repo.Bugs().ResolveExcerpt(id)
+ if err != nil {
+ // Should never happen
+ fmt.Printf("bug in the cache: could not resolve excerpt for %s: %s\n", id, err)
+ return
+ }
+ bs.out <- &models.BugChange{
+ Type: models.ChangeTypeUpdated,
+ Bug: models.NewLazyBug(bs.repo, excerpt),
+ }
+}
@@ -0,0 +1,13 @@
+type Subscription {
+ bugChanged(repoRef: String, query: String): BugChange!
+}
+
+enum ChangeType {
+ CREATED
+ UPDATED
+}
+
+type BugChange {
+ type: ChangeType!
+ bug: Bug!
+}
@@ -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
@@ -7,6 +7,8 @@ import (
"strconv"
"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"
@@ -38,18 +40,29 @@ type cacheMgmt interface {
Close() error
}
+// Observer gets notified of changes in entities in the cache
+type Observer interface {
+ // EntityCreated notifies that an entity has been created.
+ // The body of that function should NOT block.
+ EntityCreated(typename string, id entity.Id)
+
+ // EntityUpdated notifies that an entity has been updated.
+ // The body of that function should NOT block.
+ EntityUpdated(typename string, id entity.Id)
+}
+
// RepoCache is a cache for a Repository. This cache has multiple functions:
//
// 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 +114,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() {
@@ -137,6 +150,28 @@ func NewRepoCacheNoEvents(r repository.ClockedRepo) (*RepoCache, error) {
return cache, nil
}
+func (c *RepoCache) RegisterObserver(typename string, observer Observer) {
+ switch typename {
+ case bug.Typename:
+ c.bugs.RegisterObserver(observer)
+ case identity.Typename:
+ c.identities.RegisterObserver(observer)
+ default:
+ panic(fmt.Sprintf("unknown typename %q", typename))
+ }
+}
+
+func (c *RepoCache) UnregisterObserver(typename string, observer Observer) {
+ switch typename {
+ case bug.Typename:
+ c.bugs.UnregisterObserver(observer)
+ case identity.Typename:
+ c.identities.UnregisterObserver(observer)
+ default:
+ panic(fmt.Sprintf("unknown typename %q", typename))
+ }
+}
+
// Bugs gives access to the Bug entities
func (c *RepoCache) Bugs() *RepoCacheBug {
return c.bugs
@@ -224,15 +259,15 @@ const (
// 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 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 not particular entity is involved.
+ // 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 element being built. Set if Event is BuildEventStarted.
+ // Total is the total number of elements being built. Set if Event is BuildEventStarted.
Total int64
- // Progress is the current count of processed element. Set if Event is BuildEventProgress.
+ // Progress is the current count of processed elements. Set if Event is BuildEventProgress.
Progress int64
}
@@ -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]struct{}
}
func NewSubCache[EntityT entity.Interface, ExcerptT Excerpt, CacheT CacheEntity](
@@ -332,6 +335,18 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) Close() error {
return nil
}
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) RegisterObserver(observer Observer) {
+ sc.muObservers.Lock()
+ defer sc.muObservers.Unlock()
+ sc.observers[observer] = struct{}{}
+}
+
+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()
@@ -460,7 +475,7 @@ 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.entityCreated(e.Id())
if err != nil {
return *new(CacheT), err
}
@@ -582,8 +597,28 @@ func (sc *SubCache[EntityT, ExcerptT, CacheT]) GetNamespace() string {
return sc.namespace
}
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) entityCreated(id entity.Id) error {
+ sc.muObservers.RLock()
+ for observer := range sc.observers {
+ observer.EntityCreated(sc.typename, id)
+ }
+ sc.muObservers.RUnlock()
+
+ return sc.updateExcerptAndIndex(id)
+}
+
// 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.muObservers.RLock()
+ for observer := range sc.observers {
+ observer.EntityCreated(sc.typename, id)
+ }
+ sc.muObservers.RUnlock()
+
+ return sc.updateExcerptAndIndex(id)
+}
+
+func (sc *SubCache[EntityT, ExcerptT, CacheT]) updateExcerptAndIndex(id entity.Id) error {
sc.mu.Lock()
e, ok := sc.cached[id]
if !ok {
@@ -597,7 +632,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
}