diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index c1e0838b..8cde20e8 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -29,6 +29,9 @@ func (op *AddCommentOperation) Hash() (git.Hash, error) { } func (op *AddCommentOperation) Apply(snapshot *Snapshot) { + snapshot.addActor(op.Author) + snapshot.addParticipant(op.Author) + hash, err := op.Hash() if err != nil { // Should never error unless a programming error happened diff --git a/bug/op_create.go b/bug/op_create.go index d5852db9..f3757f89 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -30,6 +30,9 @@ func (op *CreateOperation) Hash() (git.Hash, error) { } func (op *CreateOperation) Apply(snapshot *Snapshot) { + snapshot.addActor(op.Author) + snapshot.addParticipant(op.Author) + hash, err := op.Hash() if err != nil { // Should never error unless a programming error happened diff --git a/bug/op_create_test.go b/bug/op_create_test.go index aa1d8c10..2a88f256 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -35,8 +35,10 @@ func TestCreate(t *testing.T) { Comments: []Comment{ comment, }, - Author: rene, - CreatedAt: create.Time(), + Author: rene, + Participants: []identity.Interface{rene}, + Actors: []identity.Interface{rene}, + CreatedAt: create.Time(), Timeline: []TimelineItem{ &CreateTimelineItem{ CommentTimelineItem: NewCommentTimelineItem(hash, comment), diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index 5a223e01..cf5c2d51 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -33,6 +33,8 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) { // Todo: currently any message can be edited, even by a different author // crypto signature are needed. + snapshot.addActor(op.Author) + var target TimelineItem var commentIndex int diff --git a/bug/op_label_change.go b/bug/op_label_change.go index 84542b6e..a2108941 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -31,6 +31,8 @@ func (op *LabelChangeOperation) Hash() (git.Hash, error) { // Apply apply the operation func (op *LabelChangeOperation) Apply(snapshot *Snapshot) { + snapshot.addActor(op.Author) + // Add in the set AddLoop: for _, added := range op.Added { diff --git a/bug/op_set_status.go b/bug/op_set_status.go index 0105d78d..57d4cf22 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -27,6 +27,7 @@ func (op *SetStatusOperation) Hash() (git.Hash, error) { func (op *SetStatusOperation) Apply(snapshot *Snapshot) { snapshot.Status = op.Status + snapshot.addActor(op.Author) hash, err := op.Hash() if err != nil { diff --git a/bug/op_set_title.go b/bug/op_set_title.go index 084838cb..ca27adee 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -31,6 +31,7 @@ func (op *SetTitleOperation) Hash() (git.Hash, error) { func (op *SetTitleOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title + snapshot.addActor(op.Author) hash, err := op.Hash() if err != nil { diff --git a/bug/snapshot.go b/bug/snapshot.go index 83b94416..53f6873a 100644 --- a/bug/snapshot.go +++ b/bug/snapshot.go @@ -12,12 +12,14 @@ import ( type Snapshot struct { id string - Status Status - Title string - Comments []Comment - Labels []Label - Author identity.Interface - CreatedAt time.Time + Status Status + Title string + Comments []Comment + Labels []Label + Author identity.Interface + Actors []identity.Interface + Participants []identity.Interface + CreatedAt time.Time Timeline []TimelineItem @@ -62,3 +64,25 @@ func (snap *Snapshot) SearchTimelineItem(hash git.Hash) (TimelineItem, error) { return nil, fmt.Errorf("timeline item not found") } + +// append the operation author to the actors list +func (snap *Snapshot) addActor(actor identity.Interface) { + for _, a := range snap.Actors { + if actor.Id() == a.Id() { + return + } + } + + snap.Actors = append(snap.Actors, actor) +} + +// append the operation author to the participants list +func (snap *Snapshot) addParticipant(participant identity.Interface) { + for _, p := range snap.Participants { + if participant.Id() == p.Id() { + return + } + } + + snap.Participants = append(snap.Participants, participant) +} diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index a50d8c66..d78def4e 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -23,10 +23,12 @@ type BugExcerpt struct { CreateUnixTime int64 EditUnixTime int64 - Status bug.Status - Labels []bug.Label - Title string - LenComments int + Status bug.Status + Labels []bug.Label + Title string + LenComments int + Actors []string + Participants []string // If author is identity.Bare, LegacyAuthor is set // If author is identity.Identity, AuthorId is set and data is deported @@ -44,6 +46,16 @@ type LegacyAuthorExcerpt struct { } func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { + participantsIds := make([]string, len(snap.Participants)) + for i, participant := range snap.Participants { + participantsIds[i] = participant.Id() + } + + actorsIds := make([]string, len(snap.Actors)) + for i, actor := range snap.Actors { + actorsIds[i] = actor.Id() + } + e := &BugExcerpt{ Id: b.Id(), CreateLamportTime: b.CreateLamportTime(), @@ -52,6 +64,8 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { EditUnixTime: snap.LastEditUnix(), Status: snap.Status, Labels: snap.Labels, + Actors: actorsIds, + Participants: participantsIds, Title: snap.Title, LenComments: len(snap.Comments), CreateMetadata: b.FirstOp().AllMetadata(), diff --git a/cache/filter.go b/cache/filter.go index 7f010608..48ee6678 100644 --- a/cache/filter.go +++ b/cache/filter.go @@ -55,6 +55,40 @@ func LabelFilter(label string) Filter { } } +// ActorFilter return a Filter that match a bug actor +func ActorFilter(actor string) Filter { + return func(repoCache *RepoCache, excerpt *BugExcerpt) bool { + for _, identityExcerpt := range repoCache.identitiesExcerpts { + if strings.Contains(strings.ToLower(identityExcerpt.Name), actor) || + actor == identityExcerpt.Id || actor == identityExcerpt.Login { + for _, actorId := range excerpt.Actors { + if identityExcerpt.Id == actorId { + return true + } + } + } + } + return false + } +} + +// ParticipantFilter return a Filter that match a bug participant +func ParticipantFilter(participant string) Filter { + return func(repoCache *RepoCache, excerpt *BugExcerpt) bool { + for _, identityExcerpt := range repoCache.identitiesExcerpts { + if strings.Contains(strings.ToLower(identityExcerpt.Name), participant) || + participant == identityExcerpt.Id || participant == identityExcerpt.Login { + for _, participantId := range excerpt.Participants { + if identityExcerpt.Id == participantId { + return true + } + } + } + } + return false + } +} + // TitleFilter return a Filter that match if the title contains the given query func TitleFilter(query string) Filter { return func(repo *RepoCache, excerpt *BugExcerpt) bool { @@ -74,11 +108,13 @@ func NoLabelFilter() Filter { // Filters is a collection of Filter that implement a complex filter type Filters struct { - Status []Filter - Author []Filter - Label []Filter - Title []Filter - NoFilters []Filter + Status []Filter + Author []Filter + Actor []Filter + Participant []Filter + Label []Filter + Title []Filter + NoFilters []Filter } // Match check if a bug match the set of filters @@ -91,6 +127,14 @@ func (f *Filters) Match(repoCache *RepoCache, excerpt *BugExcerpt) bool { return false } + if match := f.orMatch(f.Participant, repoCache, excerpt); !match { + return false + } + + if match := f.orMatch(f.Actor, repoCache, excerpt); !match { + return false + } + if match := f.andMatch(f.Label, repoCache, excerpt); !match { return false } diff --git a/cache/query.go b/cache/query.go index 39815d32..633ef1c2 100644 --- a/cache/query.go +++ b/cache/query.go @@ -56,13 +56,21 @@ func ParseQuery(query string) (*Query, error) { f := AuthorFilter(qualifierQuery) result.Author = append(result.Author, f) + case "actor": + f := ActorFilter(qualifierQuery) + result.Actor = append(result.Actor, f) + + case "participant": + f := ParticipantFilter(qualifierQuery) + result.Participant = append(result.Participant, f) + case "label": f := LabelFilter(qualifierQuery) result.Label = append(result.Label, f) case "title": f := TitleFilter(qualifierQuery) - result.Label = append(result.Title, f) + result.Title = append(result.Title, f) case "no": err := result.parseNoFilter(qualifierQuery) diff --git a/cache/query_test.go b/cache/query_test.go index f34b3e6a..9ae62ac4 100644 --- a/cache/query_test.go +++ b/cache/query_test.go @@ -19,6 +19,9 @@ func TestQueryParse(t *testing.T) { {"author:rene", true}, {`author:"René Descartes"`, true}, + {"actor:bernhard", true}, + {"participant:leonhard", true}, + {"label:hello", true}, {`label:"Good first issue"`, true}, diff --git a/doc/queries.md b/doc/queries.md index 224b59a0..857600e8 100644 --- a/doc/queries.md +++ b/doc/queries.md @@ -33,6 +33,27 @@ You can filter based on the person who opened the bug. | `author:QUERY` | `author:descartes` matches bugs opened by `René Descartes` or `Robert Descartes` | | | `author:"rené descartes"` matches bugs opened by `René Descartes` | +### Filtering by participant + +You can filter based on the person who participated in the bug (Opened the bug or added a comment). + +| Qualifier | Example | +| --- | --- | +| `participant:QUERY` | `participant:descartes` matches bugs opened or commented by `René Descartes` or `Robert Descartes` | +| | `participant:"rené descartes"` matches bugs opened or commented by `René Descartes` | + +### Filtering by actor + +You can filter based on the person who interacted with the bug. + +| Qualifier | Example | +| --- | --- | +| `actor:QUERY` | `actor:descartes` matches bugs edited by `René Descartes` or `Robert Descartes` | +| | `actor:"rené descartes"` matches bugs edited by `René Descartes` | +| ` + +**NOTE**: interaction with bugs include: opening the bug, adding comments, adding/removing labels etc... + ### Filtering by label You can filter based on the bug's label. diff --git a/graphql/graph/gen_graph.go b/graphql/graph/gen_graph.go index 06ebecc7..9da0c665 100644 --- a/graphql/graph/gen_graph.go +++ b/graphql/graph/gen_graph.go @@ -81,17 +81,19 @@ type ComplexityRoot struct { } Bug struct { - Id func(childComplexity int) int - HumanId func(childComplexity int) int - Status func(childComplexity int) int - Title func(childComplexity int) int - Labels func(childComplexity int) int - Author func(childComplexity int) int - CreatedAt func(childComplexity int) int - LastEdit func(childComplexity int) int - Comments func(childComplexity int, after *string, before *string, first *int, last *int) int - Timeline func(childComplexity int, after *string, before *string, first *int, last *int) int - Operations func(childComplexity int, after *string, before *string, first *int, last *int) int + Id func(childComplexity int) int + HumanId func(childComplexity int) int + Status func(childComplexity int) int + Title func(childComplexity int) int + Labels func(childComplexity int) int + Author func(childComplexity int) int + Actors func(childComplexity int) int + Participants func(childComplexity int) int + CreatedAt func(childComplexity int) int + LastEdit func(childComplexity int) int + Comments func(childComplexity int, after *string, before *string, first *int, last *int) int + Timeline func(childComplexity int, after *string, before *string, first *int, last *int) int + Operations func(childComplexity int, after *string, before *string, first *int, last *int) int } BugConnection struct { @@ -293,6 +295,9 @@ type AddCommentTimelineItemResolver interface { type BugResolver interface { Status(ctx context.Context, obj *bug.Snapshot) (models.Status, error) + Actors(ctx context.Context, obj *bug.Snapshot) ([]*identity.Interface, error) + Participants(ctx context.Context, obj *bug.Snapshot) ([]*identity.Interface, error) + LastEdit(ctx context.Context, obj *bug.Snapshot) (time.Time, error) Comments(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.CommentConnection, error) Timeline(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.TimelineItemConnection, error) @@ -1239,6 +1244,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Bug.Author(childComplexity), true + case "Bug.actors": + if e.complexity.Bug.Actors == nil { + break + } + + return e.complexity.Bug.Actors(childComplexity), true + + case "Bug.participants": + if e.complexity.Bug.Participants == nil { + break + } + + return e.complexity.Bug.Participants(childComplexity), true + case "Bug.createdAt": if e.complexity.Bug.CreatedAt == nil { break @@ -2779,6 +2798,24 @@ func (ec *executionContext) _Bug(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalid = true } + case "actors": + wg.Add(1) + go func(i int, field graphql.CollectedField) { + out.Values[i] = ec._Bug_actors(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + wg.Done() + }(i, field) + case "participants": + wg.Add(1) + go func(i int, field graphql.CollectedField) { + out.Values[i] = ec._Bug_participants(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalid = true + } + wg.Done() + }(i, field) case "createdAt": out.Values[i] = ec._Bug_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -3003,6 +3040,134 @@ func (ec *executionContext) _Bug_author(ctx context.Context, field graphql.Colle return ec._Identity(ctx, field.Selections, &res) } +// nolint: vetshadow +func (ec *executionContext) _Bug_actors(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Bug", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Bug().Actors(rctx, obj) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*identity.Interface) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + + arr1 := make(graphql.Array, len(res)) + var wg sync.WaitGroup + + isLen1 := len(res) == 1 + if !isLen1 { + wg.Add(len(res)) + } + + for idx1 := range res { + idx1 := idx1 + rctx := &graphql.ResolverContext{ + Index: &idx1, + Result: res[idx1], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(idx1 int) { + if !isLen1 { + defer wg.Done() + } + arr1[idx1] = func() graphql.Marshaler { + + if res[idx1] == nil { + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res[idx1]) + }() + } + if isLen1 { + f(idx1) + } else { + go f(idx1) + } + + } + wg.Wait() + return arr1 +} + +// nolint: vetshadow +func (ec *executionContext) _Bug_participants(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Bug", + Args: nil, + Field: field, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Bug().Participants(rctx, obj) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*identity.Interface) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + + arr1 := make(graphql.Array, len(res)) + var wg sync.WaitGroup + + isLen1 := len(res) == 1 + if !isLen1 { + wg.Add(len(res)) + } + + for idx1 := range res { + idx1 := idx1 + rctx := &graphql.ResolverContext{ + Index: &idx1, + Result: res[idx1], + } + ctx := graphql.WithResolverContext(ctx, rctx) + f := func(idx1 int) { + if !isLen1 { + defer wg.Done() + } + arr1[idx1] = func() graphql.Marshaler { + + if res[idx1] == nil { + return graphql.Null + } + + return ec._Identity(ctx, field.Selections, res[idx1]) + }() + } + if isLen1 { + f(idx1) + } else { + go f(idx1) + } + + } + wg.Wait() + return arr1 +} + // nolint: vetshadow func (ec *executionContext) _Bug_createdAt(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) @@ -9637,6 +9802,8 @@ type Bug { title: String! labels: [Label!]! author: Identity! + actors: [Identity]! + participants: [Identity]! createdAt: Time! lastEdit: Time! diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go index d571ce51..0b522b5e 100644 --- a/graphql/graphql_test.go +++ b/graphql/graphql_test.go @@ -50,6 +50,16 @@ func TestQueries(t *testing.T) { email avatarUrl } + actors { + name + email + avatarUrl + } + participants { + name + email + avatarUrl + } createdAt humanId @@ -112,7 +122,7 @@ func TestQueries(t *testing.T) { } }` - type Person struct { + type Identity struct { Name string `json:"name"` Email string `json:"email"` AvatarUrl string `json:"avatarUrl"` @@ -123,13 +133,15 @@ func TestQueries(t *testing.T) { AllBugs struct { PageInfo models.PageInfo Nodes []struct { - Author Person - CreatedAt string `json:"createdAt"` - HumanId string `json:"humanId"` - Id string - LastEdit string `json:"lastEdit"` - Status string - Title string + Author Identity + Actors []Identity + Participants []Identity + CreatedAt string `json:"createdAt"` + HumanId string `json:"humanId"` + Id string + LastEdit string `json:"lastEdit"` + Status string + Title string Comments struct { PageInfo models.PageInfo @@ -142,7 +154,7 @@ func TestQueries(t *testing.T) { Operations struct { PageInfo models.PageInfo Nodes []struct { - Author Person + Author Identity Date string Title string Files []string diff --git a/graphql/resolvers/bug.go b/graphql/resolvers/bug.go index 7af04934..f48ff0a7 100644 --- a/graphql/resolvers/bug.go +++ b/graphql/resolvers/bug.go @@ -8,6 +8,7 @@ import ( "github.com/MichaelMure/git-bug/graphql/connections" "github.com/MichaelMure/git-bug/graphql/graph" "github.com/MichaelMure/git-bug/graphql/models" + "github.com/MichaelMure/git-bug/identity" ) var _ graph.BugResolver = &bugResolver{} @@ -102,3 +103,23 @@ func (bugResolver) Timeline(ctx context.Context, obj *bug.Snapshot, after *strin func (bugResolver) LastEdit(ctx context.Context, obj *bug.Snapshot) (time.Time, error) { return obj.LastEditTime(), nil } + +func (bugResolver) Actors(ctx context.Context, obj *bug.Snapshot) ([]*identity.Interface, error) { + actorsp := make([]*identity.Interface, len(obj.Actors)) + + for i, actor := range obj.Actors { + actorsp[i] = &actor + } + + return actorsp, nil +} + +func (bugResolver) Participants(ctx context.Context, obj *bug.Snapshot) ([]*identity.Interface, error) { + participantsp := make([]*identity.Interface, len(obj.Participants)) + + for i, participant := range obj.Participants { + participantsp[i] = &participant + } + + return participantsp, nil +} diff --git a/graphql/schema/bug.graphql b/graphql/schema/bug.graphql index a1a61e7e..e294a363 100644 --- a/graphql/schema/bug.graphql +++ b/graphql/schema/bug.graphql @@ -36,6 +36,8 @@ type Bug { title: String! labels: [Label!]! author: Identity! + actors: [Identity]! + participants: [Identity]! createdAt: Time! lastEdit: Time!