Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

project board #843

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
124 changes: 124 additions & 0 deletions cache/board_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package cache

import (
"time"

"github.com/MichaelMure/git-bug/entities/board"
"github.com/MichaelMure/git-bug/entities/identity"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
)

// BoardCache is a wrapper around a Board. It provides multiple functions:
//
// 1. Provide a higher level API to use than the raw API from Board.
// 2. Maintain an up-to-date Snapshot available.
// 3. Deal with concurrency.
type BoardCache struct {
CachedEntityBase[*board.Snapshot, board.Operation]
}

func NewBoardCache(b *board.Board, repo repository.ClockedRepo, getUserIdentity getUserIdentityFunc, entityUpdated func(id entity.Id) error) *BoardCache {
return &BoardCache{
CachedEntityBase: CachedEntityBase[*board.Snapshot, board.Operation]{
repo: repo,
entityUpdated: entityUpdated,
getUserIdentity: getUserIdentity,
entity: &withSnapshot[*board.Snapshot, board.Operation]{Interface: b},
},
}
}

func (c *BoardCache) AddItemDraft(columnId entity.CombinedId, title, message string, files []repository.Hash) (entity.CombinedId, *board.AddItemDraftOperation, error) {
author, err := c.getUserIdentity()
if err != nil {
return entity.UnsetCombinedId, nil, err
}

return c.AddItemDraftRaw(author, time.Now().Unix(), columnId, title, message, files, nil)
}

func (c *BoardCache) AddItemDraftRaw(author identity.Interface, unixTime int64, columnId entity.CombinedId, title, message string, files []repository.Hash, metadata map[string]string) (entity.CombinedId, *board.AddItemDraftOperation, error) {
column, err := c.Snapshot().SearchColumn(columnId)
if err != nil {
return entity.UnsetCombinedId, nil, err
}

c.mu.Lock()
itemId, op, err := board.AddItemDraft(c.entity, author, unixTime, column.Id, title, message, files, metadata)
c.mu.Unlock()
if err != nil {
return entity.UnsetCombinedId, nil, err
}
return itemId, op, c.notifyUpdated()
}

func (c *BoardCache) AddItemEntity(columnId entity.CombinedId, e entity.Interface) (entity.CombinedId, *board.AddItemEntityOperation, error) {
author, err := c.getUserIdentity()
if err != nil {
return entity.UnsetCombinedId, nil, err
}

return c.AddItemEntityRaw(author, time.Now().Unix(), columnId, e, nil)
}

func (c *BoardCache) AddItemEntityRaw(author identity.Interface, unixTime int64, columnId entity.CombinedId, e entity.Interface, metadata map[string]string) (entity.CombinedId, *board.AddItemEntityOperation, error) {
column, err := c.Snapshot().SearchColumn(columnId)
if err != nil {
return entity.UnsetCombinedId, nil, err
}

var entityType board.ItemEntityType
switch e.(type) {
case *BugCache:
entityType = board.EntityTypeBug
default:
panic("unknown entity type")
}

c.mu.Lock()
itemId, op, err := board.AddItemEntity(c.entity, author, unixTime, column.Id, entityType, e, metadata)
c.mu.Unlock()
if err != nil {
return entity.UnsetCombinedId, nil, err
}
return itemId, op, c.notifyUpdated()
}

func (c *BoardCache) SetDescription(description string) (*board.SetDescriptionOperation, error) {
author, err := c.getUserIdentity()
if err != nil {
return nil, err
}

return c.SetDescriptionRaw(author, time.Now().Unix(), description, nil)
}

func (c *BoardCache) SetDescriptionRaw(author identity.Interface, unixTime int64, description string, metadata map[string]string) (*board.SetDescriptionOperation, error) {
c.mu.Lock()
op, err := board.SetDescription(c.entity, author, unixTime, description, metadata)
c.mu.Unlock()
if err != nil {
return nil, err
}
return op, c.notifyUpdated()
}

func (c *BoardCache) SetTitle(title string) (*board.SetTitleOperation, error) {
author, err := c.getUserIdentity()
if err != nil {
return nil, err
}

return c.SetTitleRaw(author, time.Now().Unix(), title, nil)
}

func (c *BoardCache) SetTitleRaw(author identity.Interface, unixTime int64, title string, metadata map[string]string) (*board.SetTitleOperation, error) {
c.mu.Lock()
op, err := board.SetTitle(c.entity, author, unixTime, title, metadata)
c.mu.Unlock()
if err != nil {
return nil, err
}
return op, c.notifyUpdated()
}
72 changes: 72 additions & 0 deletions cache/board_excerpt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cache

import (
"encoding/gob"
"time"

"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/util/lamport"
)

// Package initialisation used to register the type for (de)serialization
func init() {
gob.Register(BoardExcerpt{})
}

var _ Excerpt = &BoardExcerpt{}

// BoardExcerpt hold a subset of the board values to be able to sort and filter boards
// efficiently without having to read and compile each raw boards.
type BoardExcerpt struct {
id entity.Id

CreateLamportTime lamport.Time
EditLamportTime lamport.Time
CreateUnixTime int64
EditUnixTime int64

Title string
Description string
ItemCount int
Participants []entity.Id

CreateMetadata map[string]string
}

func NewBoardExcerpt(b *BoardCache) *BoardExcerpt {
snap := b.Snapshot()

participantsIds := make([]entity.Id, 0, len(snap.Participants))
for _, participant := range snap.Participants {
participantsIds = append(participantsIds, participant.Id())
}

return &BoardExcerpt{
id: b.Id(),
CreateLamportTime: b.CreateLamportTime(),
EditLamportTime: b.EditLamportTime(),
CreateUnixTime: b.FirstOp().Time().Unix(),
EditUnixTime: snap.EditTime().Unix(),
Title: snap.Title,
Description: snap.Description,
ItemCount: snap.ItemCount(),
Participants: participantsIds,
CreateMetadata: b.FirstOp().AllMetadata(),
}
}

func (b *BoardExcerpt) Id() entity.Id {
return b.id
}

func (b *BoardExcerpt) setId(id entity.Id) {
b.id = id
}

func (b *BoardExcerpt) CreateTime() time.Time {
return time.Unix(b.CreateUnixTime, 0)
}

func (b *BoardExcerpt) EditTime() time.Time {
return time.Unix(b.EditUnixTime, 0)
}
123 changes: 123 additions & 0 deletions cache/board_subcache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cache

import (
"errors"
"time"

"github.com/MichaelMure/git-bug/entities/board"
"github.com/MichaelMure/git-bug/entities/identity"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
)

type RepoCacheBoard struct {
*SubCache[*board.Board, *BoardExcerpt, *BoardCache]
}

func NewRepoCacheBoard(repo repository.ClockedRepo,
resolvers func() entity.Resolvers,
getUserIdentity getUserIdentityFunc) *RepoCacheBoard {

makeCached := func(b *board.Board, entityUpdated func(id entity.Id) error) *BoardCache {
return NewBoardCache(b, repo, getUserIdentity, entityUpdated)
}

makeIndexData := func(b *BoardCache) []string {
// no indexing
return nil
}

actions := Actions[*board.Board]{
ReadWithResolver: board.ReadWithResolver,
ReadAllWithResolver: board.ReadAllWithResolver,
Remove: board.Remove,
MergeAll: board.MergeAll,
}

sc := NewSubCache[*board.Board, *BoardExcerpt, *BoardCache](
repo, resolvers, getUserIdentity,
makeCached, NewBoardExcerpt, makeIndexData, actions,
board.Typename, board.Namespace,
formatVersion, defaultMaxLoadedBugs,
)

return &RepoCacheBoard{SubCache: sc}
}

func (c *RepoCacheBoard) ResolveColumn(prefix string) (*BoardCache, entity.CombinedId, error) {
boardPrefix, _ := entity.SeparateIds(prefix)
boardCandidate := make([]entity.Id, 0, 5)

// build a list of possible matching boards
c.mu.RLock()
for _, excerpt := range c.excerpts {
if excerpt.Id().HasPrefix(boardPrefix) {
boardCandidate = append(boardCandidate, excerpt.Id())
}
}
c.mu.RUnlock()

matchingBoardIds := make([]entity.Id, 0, 5)
matchingColumnId := entity.UnsetCombinedId
var matchingBoard *BoardCache

// search for matching columns
// searching every board candidate allow for some collision with the board prefix only,
// before being refined with the full column prefix
for _, boardId := range boardCandidate {
b, err := c.Resolve(boardId)
if err != nil {
return nil, entity.UnsetCombinedId, err
}

for _, column := range b.Snapshot().Columns {
if column.CombinedId.HasPrefix(prefix) {
matchingBoardIds = append(matchingBoardIds, boardId)
matchingBoard = b
matchingColumnId = column.CombinedId
}
}
}

if len(matchingBoardIds) > 1 {
return nil, entity.UnsetCombinedId, entity.NewErrMultipleMatch("board/column", matchingBoardIds)
} else if len(matchingBoardIds) == 0 {
return nil, entity.UnsetCombinedId, errors.New("column doesn't exist")
}

return matchingBoard, matchingColumnId, nil
}

func (c *RepoCacheBoard) New(title, description string, columns []string) (*BoardCache, *board.CreateOperation, error) {
author, err := c.getUserIdentity()
if err != nil {
return nil, nil, err
}

return c.NewRaw(author, time.Now().Unix(), title, description, columns, nil)
}

func (c *RepoCacheBoard) NewDefaultColumns(title, description string) (*BoardCache, *board.CreateOperation, error) {
return c.New(title, description, board.DefaultColumns)
}

// NewRaw create a new board with the given title, description and columns.
// The new board is written in the repository (commit).
func (c *RepoCacheBoard) NewRaw(author identity.Interface, unixTime int64, title, description string, columns []string, metadata map[string]string) (*BoardCache, *board.CreateOperation, error) {
b, op, err := board.Create(author, unixTime, title, description, columns, metadata)
if err != nil {
return nil, nil, err
}

err = b.Commit(c.repo)
if err != nil {
return nil, nil, err
}

cached, err := c.add(b)
if err != nil {
return nil, nil, err
}

return cached, op, nil
}
27 changes: 23 additions & 4 deletions cache/repo_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"strconv"
"sync"

"github.com/MichaelMure/git-bug/entities/board"
"github.com/MichaelMure/git-bug/entities/bug"
"github.com/MichaelMure/git-bug/entities/identity"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/multierr"
Expand Down Expand Up @@ -62,6 +65,7 @@ type RepoCache struct {
// resolvers for all known entities and excerpts
resolvers entity.Resolvers

boards *RepoCacheBoard
bugs *RepoCacheBug
identities *RepoCacheIdentity

Expand Down Expand Up @@ -94,11 +98,21 @@ func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, chan
c.bugs = NewRepoCacheBug(r, c.getResolvers, c.GetUserIdentity)
c.subcaches = append(c.subcaches, c.bugs)

c.boards = NewRepoCacheBoard(r, c.getResolvers, c.GetUserIdentity)
c.subcaches = append(c.subcaches, c.boards)

c.resolvers = entity.Resolvers{
&IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve),
&IdentityExcerpt{}: entity.ResolverFunc[*IdentityExcerpt](c.identities.ResolveExcerpt),
&BugCache{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve),
&BugExcerpt{}: entity.ResolverFunc[*BugExcerpt](c.bugs.ResolveExcerpt),
identity.Interface(nil): entity.ResolverFunc[*IdentityCache](c.identities.Resolve),
&IdentityCache{}: entity.ResolverFunc[*IdentityCache](c.identities.Resolve),
&IdentityExcerpt{}: entity.ResolverFunc[*IdentityExcerpt](c.identities.ResolveExcerpt),
bug.Interface(nil): entity.ResolverFunc[*BugCache](c.bugs.Resolve),
&bug.Bug{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve),
&BugCache{}: entity.ResolverFunc[*BugCache](c.bugs.Resolve),
&BugExcerpt{}: entity.ResolverFunc[*BugExcerpt](c.bugs.ResolveExcerpt),
board.Interface(nil): entity.ResolverFunc[*BoardCache](c.boards.Resolve),
&bug.Bug{}: entity.ResolverFunc[*BoardCache](c.boards.Resolve),
&BoardCache{}: entity.ResolverFunc[*BoardCache](c.boards.Resolve),
&BoardExcerpt{}: entity.ResolverFunc[*BoardExcerpt](c.boards.ResolveExcerpt),
}

// small buffer so that below functions can emit an event without blocking
Expand Down Expand Up @@ -137,6 +151,11 @@ func NewRepoCacheNoEvents(r repository.ClockedRepo) (*RepoCache, error) {
return cache, nil
}

// Boards gives access to the Board entities
func (c *RepoCache) Boards() *RepoCacheBoard {
return c.boards
}

// Bugs gives access to the Bug entities
func (c *RepoCache) Bugs() *RepoCacheBug {
return c.bugs
Expand Down