Merge pull request #835 from MichaelMure/op-base

entity/dag: proper base operation for simplified implementation
This commit is contained in:
Michael Muré 2022-08-01 17:16:45 +02:00 committed by GitHub
commit cd52872475
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 7050 additions and 4297 deletions

View File

@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.18.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
@ -46,7 +46,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.16.x
go-version: 1.18.x
- name: Checkout code
uses: actions/checkout@v2

View File

@ -1,7 +1,7 @@
//go:generate genny -in=connection_template.go -out=gen_lazy_bug.go gen "Name=LazyBug NodeType=entity.Id EdgeType=LazyBugEdge ConnectionType=models.BugConnection"
//go:generate genny -in=connection_template.go -out=gen_lazy_identity.go gen "Name=LazyIdentity NodeType=entity.Id EdgeType=LazyIdentityEdge ConnectionType=models.IdentityConnection"
//go:generate genny -in=connection_template.go -out=gen_identity.go gen "Name=Identity NodeType=models.IdentityWrapper EdgeType=models.IdentityEdge ConnectionType=models.IdentityConnection"
//go:generate genny -in=connection_template.go -out=gen_operation.go gen "Name=Operation NodeType=bug.Operation EdgeType=models.OperationEdge ConnectionType=models.OperationConnection"
//go:generate genny -in=connection_template.go -out=gen_operation.go gen "Name=Operation NodeType=dag.Operation EdgeType=models.OperationEdge ConnectionType=models.OperationConnection"
//go:generate genny -in=connection_template.go -out=gen_comment.go gen "Name=Comment NodeType=bug.Comment EdgeType=models.CommentEdge ConnectionType=models.CommentConnection"
//go:generate genny -in=connection_template.go -out=gen_timeline.go gen "Name=TimelineItem NodeType=bug.TimelineItem EdgeType=models.TimelineItemEdge ConnectionType=models.TimelineItemConnection"
//go:generate genny -in=connection_template.go -out=gen_label.go gen "Name=Label NodeType=bug.Label EdgeType=models.LabelEdge ConnectionType=models.LabelConnection"

View File

@ -8,23 +8,23 @@ import (
"fmt"
"github.com/MichaelMure/git-bug/api/graphql/models"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/entity/dag"
)
// BugOperationEdgeMaker define a function that take a bug.Operation and an offset and
// DagOperationEdgeMaker define a function that take a dag.Operation and an offset and
// create an Edge.
type OperationEdgeMaker func(value bug.Operation, offset int) Edge
type OperationEdgeMaker func(value dag.Operation, offset int) Edge
// OperationConMaker define a function that create a models.OperationConnection
type OperationConMaker func(
edges []*models.OperationEdge,
nodes []bug.Operation,
nodes []dag.Operation,
info *models.PageInfo,
totalCount int) (*models.OperationConnection, error)
// OperationCon will paginate a source according to the input of a relay connection
func OperationCon(source []bug.Operation, edgeMaker OperationEdgeMaker, conMaker OperationConMaker, input models.ConnectionInput) (*models.OperationConnection, error) {
var nodes []bug.Operation
func OperationCon(source []dag.Operation, edgeMaker OperationEdgeMaker, conMaker OperationConMaker, input models.ConnectionInput) (*models.OperationConnection, error) {
var nodes []dag.Operation
var edges []*models.OperationEdge
var cursors []string
var pageInfo = &models.PageInfo{}

View File

@ -1,4 +1,4 @@
// +build ignore
//go:build ignore
package main

View File

@ -33,7 +33,7 @@ models:
Hash:
model: github.com/MichaelMure/git-bug/repository.Hash
Operation:
model: github.com/MichaelMure/git-bug/bug.Operation
model: github.com/MichaelMure/git-bug/entity/dag.Operation
CreateOperation:
model: github.com/MichaelMure/git-bug/bug.CreateOperation
SetTitleOperation:

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ import (
"strconv"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/repository"
)
@ -250,7 +251,7 @@ type OpenBugPayload struct {
// The connection type for an Operation
type OperationConnection struct {
Edges []*OperationEdge `json:"edges"`
Nodes []bug.Operation `json:"nodes"`
Nodes []dag.Operation `json:"nodes"`
PageInfo *PageInfo `json:"pageInfo"`
TotalCount int `json:"totalCount"`
}
@ -258,7 +259,7 @@ type OperationConnection struct {
// Represent an Operation
type OperationEdge struct {
Cursor string `json:"cursor"`
Node bug.Operation `json:"node"`
Node dag.Operation `json:"node"`
}
// Information about pagination in a connection.

View File

@ -7,6 +7,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
)
// BugWrapper is an interface used by the GraphQL resolvers to handle a bug.
@ -24,7 +25,7 @@ type BugWrapper interface {
Participants() ([]IdentityWrapper, error)
CreatedAt() time.Time
Timeline() ([]bug.TimelineItem, error)
Operations() ([]bug.Operation, error)
Operations() ([]dag.Operation, error)
IsAuthored()
}
@ -144,7 +145,7 @@ func (lb *lazyBug) Timeline() ([]bug.TimelineItem, error) {
return lb.snap.Timeline, nil
}
func (lb *lazyBug) Operations() ([]bug.Operation, error) {
func (lb *lazyBug) Operations() ([]dag.Operation, error) {
err := lb.load()
if err != nil {
return nil, err
@ -210,6 +211,6 @@ func (l *loadedBug) Timeline() ([]bug.TimelineItem, error) {
return l.Snapshot.Timeline, nil
}
func (l *loadedBug) Operations() ([]bug.Operation, error) {
func (l *loadedBug) Operations() ([]dag.Operation, error) {
return l.Snapshot.Operations, nil
}

View File

@ -7,6 +7,7 @@ import (
"github.com/MichaelMure/git-bug/api/graphql/graph"
"github.com/MichaelMure/git-bug/api/graphql/models"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/entity/dag"
)
var _ graph.BugResolver = &bugResolver{}
@ -69,14 +70,14 @@ func (bugResolver) Operations(_ context.Context, obj models.BugWrapper, after *s
Last: last,
}
edger := func(op bug.Operation, offset int) connections.Edge {
edger := func(op dag.Operation, offset int) connections.Edge {
return models.OperationEdge{
Node: op,
Cursor: connections.OffsetToCursor(offset),
}
}
conMaker := func(edges []*models.OperationEdge, nodes []bug.Operation, info *models.PageInfo, totalCount int) (*models.OperationConnection, error) {
conMaker := func(edges []*models.OperationEdge, nodes []dag.Operation, info *models.PageInfo, totalCount int) (*models.OperationConnection, error) {
return &models.OperationConnection{
Edges: edges,
Nodes: nodes,

View File

@ -20,6 +20,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
)
@ -288,7 +289,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out
for _, op := range snapshot.Operations[1:] {
// ignore SetMetadata operations
if _, ok := op.(*bug.SetMetadataOperation); ok {
if _, ok := op.(dag.OperationDoesntChangeSnapshot); ok {
continue
}

View File

@ -15,8 +15,8 @@ import (
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/interrupt"
)
@ -245,7 +245,7 @@ func TestGithubPushPull(t *testing.T) {
// verify operation have correct metadata
for _, op := range tt.bug.Snapshot().Operations {
// Check if the originals operations (*not* SetMetadata) are tagged properly
if _, ok := op.(*bug.SetMetadataOperation); !ok {
if _, ok := op.(dag.OperationDoesntChangeSnapshot); !ok {
_, haveIDMetadata := op.GetMetadata(metaKeyGithubId)
require.True(t, haveIDMetadata)

View File

@ -13,6 +13,7 @@ import (
"github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/interrupt"
@ -44,7 +45,7 @@ func TestGithubImporter(t *testing.T) {
name: "simple issue",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/1",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "simple issue", "initial comment", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil),
bug.NewAddCommentOp(author, 0, "second comment", nil),
@ -55,7 +56,7 @@ func TestGithubImporter(t *testing.T) {
name: "empty issue",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/2",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "empty issue", "", nil),
},
},
@ -64,7 +65,7 @@ func TestGithubImporter(t *testing.T) {
name: "complex issue",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/3",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "complex issue", "initial comment", nil),
bug.NewLabelChangeOperation(author, 0, []bug.Label{"bug"}, []bug.Label{}),
bug.NewLabelChangeOperation(author, 0, []bug.Label{"duplicate"}, []bug.Label{}),
@ -81,7 +82,7 @@ func TestGithubImporter(t *testing.T) {
name: "editions",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/4",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "editions", "initial comment edited", nil),
bug.NewEditCommentOp(author, 0, "", "erased then edited again", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil),
@ -93,7 +94,7 @@ func TestGithubImporter(t *testing.T) {
name: "comment deletion",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/5",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "comment deletion", "", nil),
},
},
@ -102,7 +103,7 @@ func TestGithubImporter(t *testing.T) {
name: "edition deletion",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/6",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "edition deletion", "initial comment", nil),
bug.NewEditCommentOp(author, 0, "", "initial comment edited again", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil),
@ -114,7 +115,7 @@ func TestGithubImporter(t *testing.T) {
name: "hidden comment",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/7",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "hidden comment", "initial comment", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil),
},
@ -124,7 +125,7 @@ func TestGithubImporter(t *testing.T) {
name: "transfered issue",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/8",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "transfered issue", "", nil),
},
},
@ -133,7 +134,7 @@ func TestGithubImporter(t *testing.T) {
name: "unicode control characters",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/10",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "unicode control characters", "u0000: \nu0001: \nu0002: \nu0003: \nu0004: \nu0005: \nu0006: \nu0007: \nu0008: \nu0009: \t\nu0010: \nu0011: \nu0012: \nu0013: \nu0014: \nu0015: \nu0016: \nu0017: \nu0018: \nu0019:", nil),
},
},

View File

@ -15,6 +15,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
)
@ -256,7 +257,7 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, out
labelSet := make(map[string]struct{})
for _, op := range snapshot.Operations[1:] {
// ignore SetMetadata operations
if _, ok := op.(*bug.SetMetadataOperation); ok {
if _, ok := op.(dag.OperationDoesntChangeSnapshot); ok {
continue
}

View File

@ -11,11 +11,12 @@ import (
"github.com/xanzy/go-gitlab"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/interrupt"
@ -247,7 +248,7 @@ func TestGitlabPushPull(t *testing.T) {
// verify operation have correct metadata
for _, op := range tt.bug.Snapshot().Operations {
// Check if the originals operations (*not* SetMetadata) are tagged properly
if _, ok := op.(*bug.SetMetadataOperation); !ok {
if _, ok := op.(dag.OperationDoesntChangeSnapshot); !ok {
_, haveIDMetadata := op.GetMetadata(metaKeyGitlabId)
require.True(t, haveIDMetadata)
@ -272,7 +273,7 @@ func TestGitlabPushPull(t *testing.T) {
require.True(t, ok)
require.Equal(t, issueOrigin, target)
//TODO: maybe more tests to ensure bug final state
// TODO: maybe more tests to ensure bug final state
})
}
}

View File

@ -13,6 +13,7 @@ import (
"github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/interrupt"
@ -49,7 +50,7 @@ func TestGitlabImport(t *testing.T) {
name: "simple issue",
url: "https://gitlab.com/git-bug/test/-/issues/1",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "simple issue", "initial comment", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil),
bug.NewAddCommentOp(author, 0, "second comment", nil),
@ -60,7 +61,7 @@ func TestGitlabImport(t *testing.T) {
name: "empty issue",
url: "https://gitlab.com/git-bug/test/-/issues/2",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "empty issue", "", nil),
},
},
@ -69,7 +70,7 @@ func TestGitlabImport(t *testing.T) {
name: "complex issue",
url: "https://gitlab.com/git-bug/test/-/issues/3",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "complex issue", "initial comment", nil),
bug.NewAddCommentOp(author, 0, "### header\n\n**bold**\n\n_italic_\n\n> with quote\n\n`inline code`\n\n```\nmultiline code\n```\n\n- bulleted\n- list\n\n1. numbered\n1. list\n\n- [ ] task\n- [x] list\n\n@MichaelMure mention\n\n#2 reference issue\n#3 auto-reference issue", nil),
bug.NewSetTitleOp(author, 0, "complex issue edited", "complex issue"),
@ -86,7 +87,7 @@ func TestGitlabImport(t *testing.T) {
name: "editions",
url: "https://gitlab.com/git-bug/test/-/issues/4",
bug: &bug.Snapshot{
Operations: []bug.Operation{
Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "editions", "initial comment edited", nil),
bug.NewAddCommentOp(author, 0, "first comment edited", nil),
},

View File

@ -15,6 +15,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
)
@ -297,7 +298,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, out ch
for _, op := range snapshot.Operations[1:] {
// ignore SetMetadata operations
if _, ok := op.(*bug.SetMetadataOperation); ok {
if _, ok := op.(dag.OperationDoesntChangeSnapshot); ok {
continue
}

View File

@ -14,6 +14,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/util/text"
)
@ -377,7 +378,7 @@ func labelSetsMatch(jiraSet []string, gitbugSet []bug.Label) bool {
// Create a bug.Operation (or a series of operations) from a JIRA changelog
// entry
func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, entry ChangeLogEntry, potentialOp bug.Operation) error {
func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, entry ChangeLogEntry, potentialOp dag.Operation) error {
// If we have an operation which is already mapped to the entire changelog
// entry then that means this changelog entry was induced by an export

View File

@ -44,11 +44,7 @@ func NewBug() *Bug {
// Read will read a bug from a repository
func Read(repo repository.ClockedRepo, id entity.Id) (*Bug, error) {
e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id)
if err != nil {
return nil, err
}
return &Bug{Entity: e}, nil
return ReadWithResolver(repo, identity.NewSimpleResolver(repo), id)
}
// ReadWithResolver will read a bug from its Id, with a custom identity.Resolver
@ -144,21 +140,21 @@ func (bug *Bug) Operations() []Operation {
}
// Compile a bug in a easily usable snapshot
func (bug *Bug) Compile() Snapshot {
snap := Snapshot{
func (bug *Bug) Compile() *Snapshot {
snap := &Snapshot{
id: bug.Id(),
Status: OpenStatus,
}
for _, op := range bug.Operations() {
op.Apply(&snap)
op.Apply(snap)
snap.Operations = append(snap.Operations, op)
}
return snap
}
// Lookup for the very first operation of the bug.
// FirstOp lookup for the very first operation of the bug.
// For a valid Bug, this operation should be a CreateOp
func (bug *Bug) FirstOp() Operation {
if fo := bug.Entity.FirstOp(); fo != nil {
@ -167,7 +163,7 @@ func (bug *Bug) FirstOp() Operation {
return nil
}
// Lookup for the very last operation of the bug.
// LastOp lookup for the very last operation of the bug.
// For a valid Bug, should never be nil
func (bug *Bug) LastOp() Operation {
if lo := bug.Entity.LastOp(); lo != nil {

View File

@ -41,5 +41,5 @@ func (c Comment) FormatTime() string {
return c.UnixTime.Time().Format("Mon Jan 2 15:04:05 2006 +0200")
}
// Sign post method for gqlgen
// IsAuthored is a sign post method for gqlgen
func (c Comment) IsAuthored() {}

View File

@ -7,34 +7,34 @@ import (
)
type Interface interface {
// Id return the Bug identifier
// Id returns the Bug identifier
Id() entity.Id
// Validate check if the Bug data is valid
// Validate checks if the Bug data is valid
Validate() error
// Append an operation into the staging area, to be committed later
Append(op Operation)
// Operations return the ordered operations
// Operations returns the ordered operations
Operations() []Operation
// Indicate that the in-memory state changed and need to be commit in the repository
// NeedCommit indicates that the in-memory state changed and need to be commit in the repository
NeedCommit() bool
// Commit write the staging area in Git and move the operations to the packs
// Commit writes the staging area in Git and move the operations to the packs
Commit(repo repository.ClockedRepo) error
// Lookup for the very first operation of the bug.
// FirstOp lookup for the very first operation of the bug.
// For a valid Bug, this operation should be a CreateOp
FirstOp() Operation
// Lookup for the very last operation of the bug.
// LastOp lookup for the very last operation of the bug.
// For a valid Bug, should never be nil
LastOp() Operation
// Compile a bug in a easily usable snapshot
Compile() Snapshot
// Compile a bug in an easily usable snapshot
Compile() *Snapshot
// CreateLamportTime return the Lamport time of creation
CreateLamportTime() lamport.Time
@ -42,14 +42,3 @@ type Interface interface {
// EditLamportTime return the Lamport time of the last edit
EditLamportTime() lamport.Time
}
func bugFromInterface(bug Interface) *Bug {
switch bug := bug.(type) {
case *Bug:
return bug
case *WithSnapshot:
return bug.Bug
default:
panic("missing type case")
}
}

View File

@ -1,7 +1,6 @@
package bug
import (
"encoding/json"
"fmt"
"github.com/MichaelMure/git-bug/entity"
@ -17,24 +16,24 @@ var _ dag.OperationWithFiles = &AddCommentOperation{}
// AddCommentOperation will add a new comment in the bug
type AddCommentOperation struct {
OpBase
dag.OpBase
Message string `json:"message"`
// TODO: change for a map[string]util.hash to store the filename ?
Files []repository.Hash `json:"files"`
}
func (op *AddCommentOperation) Id() entity.Id {
return idOperation(op, &op.OpBase)
return dag.IdOperation(op, &op.OpBase)
}
func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
snapshot.addActor(op.Author_)
snapshot.addParticipant(op.Author_)
snapshot.addActor(op.Author())
snapshot.addParticipant(op.Author())
comment := Comment{
id: entity.CombineIds(snapshot.Id(), op.Id()),
Message: op.Message,
Author: op.Author_,
Author: op.Author(),
Files: op.Files,
UnixTime: timestamp.Timestamp(op.UnixTime),
}
@ -64,64 +63,31 @@ func (op *AddCommentOperation) Validate() error {
return nil
}
// UnmarshalJSON is a two-steps JSON unmarshalling
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
// overriding the outer op's MarshalJSON
func (op *AddCommentOperation) UnmarshalJSON(data []byte) error {
// Unmarshal OpBase and the op separately
base := OpBase{}
err := json.Unmarshal(data, &base)
if err != nil {
return err
}
aux := struct {
Message string `json:"message"`
Files []repository.Hash `json:"files"`
}{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
op.OpBase = base
op.Message = aux.Message
op.Files = aux.Files
return nil
}
// Sign post method for gqlgen
func (op *AddCommentOperation) IsAuthored() {}
func NewAddCommentOp(author identity.Interface, unixTime int64, message string, files []repository.Hash) *AddCommentOperation {
return &AddCommentOperation{
OpBase: newOpBase(AddCommentOp, author, unixTime),
OpBase: dag.NewOpBase(AddCommentOp, author, unixTime),
Message: message,
Files: files,
}
}
// CreateTimelineItem replace a AddComment operation in the Timeline and hold its edition history
// AddCommentTimelineItem hold a comment in the timeline
type AddCommentTimelineItem struct {
CommentTimelineItem
}
// Sign post method for gqlgen
// IsAuthored is a sign post method for gqlgen
func (a *AddCommentTimelineItem) IsAuthored() {}
// Convenience function to apply the operation
func AddComment(b Interface, author identity.Interface, unixTime int64, message string) (*AddCommentOperation, error) {
return AddCommentWithFiles(b, author, unixTime, message, nil)
}
func AddCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash) (*AddCommentOperation, error) {
addCommentOp := NewAddCommentOp(author, unixTime, message, files)
if err := addCommentOp.Validate(); err != nil {
// AddComment is a convenience function to add a comment to a bug
func AddComment(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*AddCommentOperation, error) {
op := NewAddCommentOp(author, unixTime, message, files)
for key, val := range metadata {
op.SetMetadata(key, val)
}
if err := op.Validate(); err != nil {
return nil, err
}
b.Append(addCommentOp)
return addCommentOp, nil
b.Append(op)
return op, nil
}

View File

@ -1,37 +1,18 @@
package bug
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
func TestAddCommentSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewAddCommentOp(rene, unix, "message", nil)
data, err := json.Marshal(before)
require.NoError(t, err)
var after AddCommentOperation
err = json.Unmarshal(data, &after)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
return NewAddCommentOp(author, unixTime, "message", nil)
})
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
return NewAddCommentOp(author, unixTime, "message", []repository.Hash{"hash1", "hash2"})
})
}

View File

@ -1,7 +1,6 @@
package bug
import (
"encoding/json"
"fmt"
"github.com/MichaelMure/git-bug/entity"
@ -17,53 +16,38 @@ var _ dag.OperationWithFiles = &CreateOperation{}
// CreateOperation define the initial creation of a bug
type CreateOperation struct {
OpBase
dag.OpBase
Title string `json:"title"`
Message string `json:"message"`
Files []repository.Hash `json:"files"`
}
func (op *CreateOperation) Id() entity.Id {
return idOperation(op, &op.OpBase)
}
// OVERRIDE
func (op *CreateOperation) SetMetadata(key string, value string) {
// sanity check: we make sure we are not in the following scenario:
// - the bug is created with a first operation
// - Id() is used
// - metadata are added, which will change the Id
// - Id() is used again
if op.id != entity.UnsetId {
panic("usage of Id() after changing the first operation")
}
op.OpBase.SetMetadata(key, value)
return dag.IdOperation(op, &op.OpBase)
}
func (op *CreateOperation) Apply(snapshot *Snapshot) {
// sanity check: will fail when adding a second Create
if snapshot.id != "" && snapshot.id != entity.UnsetId && snapshot.id != op.Id() {
panic("adding a second Create operation")
return
}
snapshot.id = op.Id()
snapshot.addActor(op.Author_)
snapshot.addParticipant(op.Author_)
snapshot.addActor(op.Author())
snapshot.addParticipant(op.Author())
snapshot.Title = op.Title
comment := Comment{
id: entity.CombineIds(snapshot.Id(), op.Id()),
Message: op.Message,
Author: op.Author_,
Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime),
}
snapshot.Comments = []Comment{comment}
snapshot.Author = op.Author_
snapshot.Author = op.Author()
snapshot.CreateTime = op.Time()
snapshot.Timeline = []TimelineItem{
@ -82,13 +66,6 @@ func (op *CreateOperation) Validate() error {
return err
}
if len(op.Nonce) > 64 {
return fmt.Errorf("create nonce is too big")
}
if len(op.Nonce) < 20 {
return fmt.Errorf("create nonce is too small")
}
if text.Empty(op.Title) {
return fmt.Errorf("title is empty")
}
@ -103,45 +80,9 @@ func (op *CreateOperation) Validate() error {
return nil
}
// UnmarshalJSON is a two step JSON unmarshalling
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
// overriding the outer op's MarshalJSON
func (op *CreateOperation) UnmarshalJSON(data []byte) error {
// Unmarshal OpBase and the op separately
base := OpBase{}
err := json.Unmarshal(data, &base)
if err != nil {
return err
}
aux := struct {
Nonce []byte `json:"nonce"`
Title string `json:"title"`
Message string `json:"message"`
Files []repository.Hash `json:"files"`
}{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
op.OpBase = base
op.Nonce = aux.Nonce
op.Title = aux.Title
op.Message = aux.Message
op.Files = aux.Files
return nil
}
// Sign post method for gqlgen
func (op *CreateOperation) IsAuthored() {}
func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation {
return &CreateOperation{
OpBase: newOpBase(CreateOp, author, unixTime),
OpBase: dag.NewOpBase(CreateOp, author, unixTime),
Title: title,
Message: message,
Files: files,
@ -153,23 +94,19 @@ type CreateTimelineItem struct {
CommentTimelineItem
}
// Sign post method for gqlgen
// IsAuthored is a sign post method for gqlgen
func (c *CreateTimelineItem) IsAuthored() {}
// Convenience function to apply the operation
func Create(author identity.Interface, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
return CreateWithFiles(author, unixTime, title, message, nil)
}
func CreateWithFiles(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) (*Bug, *CreateOperation, error) {
newBug := NewBug()
createOp := NewCreateOp(author, unixTime, title, message, files)
if err := createOp.Validate(); err != nil {
return nil, createOp, err
// Create is a convenience function to create a bug
func Create(author identity.Interface, unixTime int64, title, message string, files []repository.Hash, metadata map[string]string) (*Bug, *CreateOperation, error) {
b := NewBug()
op := NewCreateOp(author, unixTime, title, message, files)
for key, val := range metadata {
op.SetMetadata(key, val)
}
newBug.Append(createOp)
return newBug, createOp, nil
if err := op.Validate(); err != nil {
return nil, op, err
}
b.Append(op)
return b, op, nil
}

View File

@ -1,13 +1,13 @@
package bug
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/timestamp"
@ -58,26 +58,10 @@ func TestCreate(t *testing.T) {
}
func TestCreateSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewCreateOp(rene, unix, "title", "message", nil)
data, err := json.Marshal(before)
require.NoError(t, err)
var after CreateOperation
err = json.Unmarshal(data, &after)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
return NewCreateOp(author, unixTime, "title", "message", nil)
})
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
return NewCreateOp(author, unixTime, "title", "message", []repository.Hash{"hash1", "hash2"})
})
}

View File

@ -1,7 +1,6 @@
package bug
import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
@ -20,14 +19,14 @@ var _ dag.OperationWithFiles = &EditCommentOperation{}
// EditCommentOperation will change a comment in the bug
type EditCommentOperation struct {
OpBase
dag.OpBase
Target entity.Id `json:"target"`
Message string `json:"message"`
Files []repository.Hash `json:"files"`
}
func (op *EditCommentOperation) Id() entity.Id {
return idOperation(op, &op.OpBase)
return dag.IdOperation(op, &op.OpBase)
}
func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
@ -68,7 +67,7 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
return
}
snapshot.addActor(op.Author_)
snapshot.addActor(op.Author())
// Updating the corresponding comment
@ -101,43 +100,9 @@ func (op *EditCommentOperation) Validate() error {
return nil
}
// UnmarshalJSON is two steps JSON unmarshalling
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
// overriding the outer op's MarshalJSON
func (op *EditCommentOperation) UnmarshalJSON(data []byte) error {
// Unmarshal OpBase and the op separately
base := OpBase{}
err := json.Unmarshal(data, &base)
if err != nil {
return err
}
aux := struct {
Target entity.Id `json:"target"`
Message string `json:"message"`
Files []repository.Hash `json:"files"`
}{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
op.OpBase = base
op.Target = aux.Target
op.Message = aux.Message
op.Files = aux.Files
return nil
}
// Sign post method for gqlgen
func (op *EditCommentOperation) IsAuthored() {}
func NewEditCommentOp(author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash) *EditCommentOperation {
return &EditCommentOperation{
OpBase: newOpBase(EditCommentOp, author, unixTime),
OpBase: dag.NewOpBase(EditCommentOp, author, unixTime),
Target: target,
Message: message,
Files: files,
@ -145,27 +110,20 @@ func NewEditCommentOp(author identity.Interface, unixTime int64, target entity.I
}
// EditComment is a convenience function to apply the operation
func EditComment(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string) (*EditCommentOperation, error) {
return EditCommentWithFiles(b, author, unixTime, target, message, nil)
}
func EditCommentWithFiles(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash) (*EditCommentOperation, error) {
editCommentOp := NewEditCommentOp(author, unixTime, target, message, files)
if err := editCommentOp.Validate(); err != nil {
func EditComment(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash, metadata map[string]string) (*EditCommentOperation, error) {
op := NewEditCommentOp(author, unixTime, target, message, files)
for key, val := range metadata {
op.SetMetadata(key, val)
}
if err := op.Validate(); err != nil {
return nil, err
}
b.Append(editCommentOp)
return editCommentOp, nil
b.Append(op)
return op, nil
}
// EditCreateComment is a convenience function to edit the body of a bug (the first comment)
func EditCreateComment(b Interface, author identity.Interface, unixTime int64, message string) (*EditCommentOperation, error) {
func EditCreateComment(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*EditCommentOperation, error) {
createOp := b.FirstOp().(*CreateOperation)
return EditComment(b, author, unixTime, createOp.Id(), message)
}
// EditCreateCommentWithFiles is a convenience function to edit the body of a bug (the first comment)
func EditCreateCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash) (*EditCommentOperation, error) {
createOp := b.FirstOp().(*CreateOperation)
return EditCommentWithFiles(b, author, unixTime, createOp.Id(), message, files)
return EditComment(b, author, unixTime, createOp.Id(), message, files, metadata)
}

View File

@ -1,12 +1,12 @@
package bug
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
@ -75,26 +75,10 @@ func TestEdit(t *testing.T) {
}
func TestEditCommentSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewEditCommentOp(rene, unix, "target", "message", nil)
data, err := json.Marshal(before)
require.NoError(t, err)
var after EditCommentOperation
err = json.Unmarshal(data, &after)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
return NewEditCommentOp(author, unixTime, "target", "message", nil)
})
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
return NewEditCommentOp(author, unixTime, "target", "message", []repository.Hash{"hash1", "hash2"})
})
}

View File

@ -1,13 +1,13 @@
package bug
import (
"encoding/json"
"fmt"
"sort"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/timestamp"
)
@ -16,18 +16,18 @@ var _ Operation = &LabelChangeOperation{}
// LabelChangeOperation define a Bug operation to add or remove labels
type LabelChangeOperation struct {
OpBase
dag.OpBase
Added []Label `json:"added"`
Removed []Label `json:"removed"`
}
func (op *LabelChangeOperation) Id() entity.Id {
return idOperation(op, &op.OpBase)
return dag.IdOperation(op, &op.OpBase)
}
// Apply apply the operation
// Apply applies the operation
func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
snapshot.addActor(op.Author_)
snapshot.addActor(op.Author())
// Add in the set
AddLoop:
@ -59,7 +59,7 @@ AddLoop:
item := &LabelChangeTimelineItem{
id: op.Id(),
Author: op.Author_,
Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime),
Added: op.Added,
Removed: op.Removed,
@ -92,41 +92,9 @@ func (op *LabelChangeOperation) Validate() error {
return nil
}
// UnmarshalJSON is a two step JSON unmarshalling
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
// overriding the outer op's MarshalJSON
func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
// Unmarshal OpBase and the op separately
base := OpBase{}
err := json.Unmarshal(data, &base)
if err != nil {
return err
}
aux := struct {
Added []Label `json:"added"`
Removed []Label `json:"removed"`
}{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
op.OpBase = base
op.Added = aux.Added
op.Removed = aux.Removed
return nil
}
// Sign post method for gqlgen
func (op *LabelChangeOperation) IsAuthored() {}
func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
return &LabelChangeOperation{
OpBase: newOpBase(LabelChangeOp, author, unixTime),
OpBase: dag.NewOpBase(LabelChangeOp, author, unixTime),
Added: added,
Removed: removed,
}
@ -144,11 +112,11 @@ func (l LabelChangeTimelineItem) Id() entity.Id {
return l.id
}
// Sign post method for gqlgen
func (l *LabelChangeTimelineItem) IsAuthored() {}
// IsAuthored is a sign post method for gqlgen
func (l LabelChangeTimelineItem) IsAuthored() {}
// ChangeLabels is a convenience function to apply the operation
func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
// ChangeLabels is a convenience function to change labels on a bug
func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) ([]LabelChangeResult, *LabelChangeOperation, error) {
var added, removed []Label
var results []LabelChangeResult
@ -196,23 +164,25 @@ func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, r
return results, nil, fmt.Errorf("no label added or removed")
}
labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
if err := labelOp.Validate(); err != nil {
op := NewLabelChangeOperation(author, unixTime, added, removed)
for key, val := range metadata {
op.SetMetadata(key, val)
}
if err := op.Validate(); err != nil {
return nil, nil, err
}
b.Append(labelOp)
b.Append(op)
return results, labelOp, nil
return results, op, nil
}
// ForceChangeLabels is a convenience function to apply the operation
// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely
// responsible of what you are doing. In the general case, you want to use ChangeLabels instead.
// responsible for what you are doing. In the general case, you want to use ChangeLabels instead.
// The intended use of this function is to allow importers to create legal but unexpected label changes,
// like removing a label with no information of when it was added before.
func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) (*LabelChangeOperation, error) {
func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) (*LabelChangeOperation, error) {
added := make([]Label, len(add))
for i, str := range add {
added[i] = Label(str)
@ -223,15 +193,18 @@ func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, a
removed[i] = Label(str)
}
labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
op := NewLabelChangeOperation(author, unixTime, added, removed)
if err := labelOp.Validate(); err != nil {
for key, val := range metadata {
op.SetMetadata(key, val)
}
if err := op.Validate(); err != nil {
return nil, err
}
b.Append(labelOp)
b.Append(op)
return labelOp, nil
return op, nil
}
func labelExist(labels []Label, label Label) bool {

View File

@ -1,37 +1,20 @@
package bug
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
func TestLabelChangeSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
data, err := json.Marshal(before)
require.NoError(t, err)
var after LabelChangeOperation
err = json.Unmarshal(data, &after)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
return NewLabelChangeOperation(author, unixTime, []Label{"added"}, []Label{"removed"})
})
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
return NewLabelChangeOperation(author, unixTime, []Label{"added"}, nil)
})
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
return NewLabelChangeOperation(author, unixTime, nil, []Label{"removed"})
})
}

View File

@ -1,77 +0,0 @@
package bug
import (
"encoding/json"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/identity"
)
var _ Operation = &NoOpOperation{}
// NoOpOperation is an operation that does not change the bug state. It can
// however be used to store arbitrary metadata in the bug history, for example
// to support a bridge feature.
type NoOpOperation struct {
OpBase
}
func (op *NoOpOperation) Id() entity.Id {
return idOperation(op, &op.OpBase)
}
func (op *NoOpOperation) Apply(snapshot *Snapshot) {
// Nothing to do
}
func (op *NoOpOperation) Validate() error {
return op.OpBase.Validate(op, NoOpOp)
}
// UnmarshalJSON is a two step JSON unmarshalling
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
// overriding the outer op's MarshalJSON
func (op *NoOpOperation) UnmarshalJSON(data []byte) error {
// Unmarshal OpBase and the op separately
base := OpBase{}
err := json.Unmarshal(data, &base)
if err != nil {
return err
}
aux := struct{}{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
op.OpBase = base
return nil
}
// Sign post method for gqlgen
func (op *NoOpOperation) IsAuthored() {}
func NewNoOpOp(author identity.Interface, unixTime int64) *NoOpOperation {
return &NoOpOperation{
OpBase: newOpBase(NoOpOp, author, unixTime),
}
}
// Convenience function to apply the operation
func NoOp(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*NoOpOperation, error) {
op := NewNoOpOp(author, unixTime)
for key, value := range metadata {
op.SetMetadata(key, value)
}
if err := op.Validate(); err != nil {
return nil, err
}
b.Append(op)
return op, nil
}

View File

@ -1,39 +0,0 @@
package bug
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/stretchr/testify/assert"
)
func TestNoopSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewNoOpOp(rene, unix)
data, err := json.Marshal(before)
assert.NoError(t, err)
var after NoOpOperation
err = json.Unmarshal(data, &after)
assert.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity as it's not serialized
after.Author_ = rene
assert.Equal(t, before, &after)
}

View File

@ -1,108 +1,21 @@
package bug
import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/text"
)
var _ Operation = &SetMetadataOperation{}
type SetMetadataOperation struct {
OpBase
Target entity.Id `json:"target"`
NewMetadata map[string]string `json:"new_metadata"`
func NewSetMetadataOp(author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *dag.SetMetadataOperation[*Snapshot] {
return dag.NewSetMetadataOp[*Snapshot](SetMetadataOp, author, unixTime, target, newMetadata)
}
func (op *SetMetadataOperation) Id() entity.Id {
return idOperation(op, &op.OpBase)
}
func (op *SetMetadataOperation) Apply(snapshot *Snapshot) {
for _, target := range snapshot.Operations {
if target.Id() == op.Target {
// Apply the metadata in an immutable way: if a metadata already
// exist, it's not possible to override it.
for key, value := range op.NewMetadata {
target.setExtraMetadataImmutable(key, value)
}
return
}
}
}
func (op *SetMetadataOperation) Validate() error {
if err := op.OpBase.Validate(op, SetMetadataOp); err != nil {
return err
}
if err := op.Target.Validate(); err != nil {
return errors.Wrap(err, "target invalid")
}
for key, val := range op.NewMetadata {
if !text.SafeOneLine(key) {
return fmt.Errorf("metadata key is unsafe")
}
if !text.Safe(val) {
return fmt.Errorf("metadata value is not fully printable")
}
}
return nil
}
// UnmarshalJSON is a two step JSON unmarshalling
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
// overriding the outer op's MarshalJSON
func (op *SetMetadataOperation) UnmarshalJSON(data []byte) error {
// Unmarshal OpBase and the op separately
base := OpBase{}
err := json.Unmarshal(data, &base)
if err != nil {
return err
}
aux := struct {
Target entity.Id `json:"target"`
NewMetadata map[string]string `json:"new_metadata"`
}{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
op.OpBase = base
op.Target = aux.Target
op.NewMetadata = aux.NewMetadata
return nil
}
// Sign post method for gqlgen
func (op *SetMetadataOperation) IsAuthored() {}
func NewSetMetadataOp(author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *SetMetadataOperation {
return &SetMetadataOperation{
OpBase: newOpBase(SetMetadataOp, author, unixTime),
Target: target,
NewMetadata: newMetadata,
}
}
// Convenience function to apply the operation
func SetMetadata(b Interface, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*SetMetadataOperation, error) {
SetMetadataOp := NewSetMetadataOp(author, unixTime, target, newMetadata)
if err := SetMetadataOp.Validate(); err != nil {
// SetMetadata is a convenience function to add metadata on another operation
func SetMetadata(b Interface, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*Snapshot], error) {
op := NewSetMetadataOp(author, unixTime, target, newMetadata)
if err := op.Validate(); err != nil {
return nil, err
}
b.Append(SetMetadataOp)
return SetMetadataOp, nil
b.Append(op)
return op, nil
}

View File

@ -1,126 +0,0 @@
package bug
import (
"encoding/json"
"testing"
"time"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/stretchr/testify/require"
)
func TestSetMetadata(t *testing.T) {
snapshot := Snapshot{}
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
create := NewCreateOp(rene, unix, "title", "create", nil)
create.SetMetadata("key", "value")
create.Apply(&snapshot)
snapshot.Operations = append(snapshot.Operations, create)
id1 := create.Id()
require.NoError(t, id1.Validate())
comment := NewAddCommentOp(rene, unix, "comment", nil)
comment.SetMetadata("key2", "value2")
comment.Apply(&snapshot)
snapshot.Operations = append(snapshot.Operations, comment)
id2 := comment.Id()
require.NoError(t, id2.Validate())
op1 := NewSetMetadataOp(rene, unix, id1, map[string]string{
"key": "override",
"key2": "value",
})
op1.Apply(&snapshot)
snapshot.Operations = append(snapshot.Operations, op1)
createMetadata := snapshot.Operations[0].AllMetadata()
require.Len(t, createMetadata, 2)
// original key is not overrided
require.Equal(t, createMetadata["key"], "value")
// new key is set
require.Equal(t, createMetadata["key2"], "value")
commentMetadata := snapshot.Operations[1].AllMetadata()
require.Len(t, commentMetadata, 1)
require.Equal(t, commentMetadata["key2"], "value2")
op2 := NewSetMetadataOp(rene, unix, id2, map[string]string{
"key2": "value",
"key3": "value3",
})
op2.Apply(&snapshot)
snapshot.Operations = append(snapshot.Operations, op2)
createMetadata = snapshot.Operations[0].AllMetadata()
require.Len(t, createMetadata, 2)
require.Equal(t, createMetadata["key"], "value")
require.Equal(t, createMetadata["key2"], "value")
commentMetadata = snapshot.Operations[1].AllMetadata()
require.Len(t, commentMetadata, 2)
// original key is not overrided
require.Equal(t, commentMetadata["key2"], "value2")
// new key is set
require.Equal(t, commentMetadata["key3"], "value3")
op3 := NewSetMetadataOp(rene, unix, id1, map[string]string{
"key": "override",
"key2": "override",
})
op3.Apply(&snapshot)
snapshot.Operations = append(snapshot.Operations, op3)
createMetadata = snapshot.Operations[0].AllMetadata()
require.Len(t, createMetadata, 2)
// original key is not overrided
require.Equal(t, createMetadata["key"], "value")
// previously set key is not overrided
require.Equal(t, createMetadata["key2"], "value")
commentMetadata = snapshot.Operations[1].AllMetadata()
require.Len(t, commentMetadata, 2)
require.Equal(t, commentMetadata["key2"], "value2")
require.Equal(t, commentMetadata["key3"], "value3")
}
func TestSetMetadataSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewSetMetadataOp(rene, unix, "message", map[string]string{
"key1": "value1",
"key2": "value2",
})
data, err := json.Marshal(before)
require.NoError(t, err)
var after SetMetadataOperation
err = json.Unmarshal(data, &after)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
}

View File

@ -1,11 +1,10 @@
package bug
import (
"encoding/json"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/timestamp"
)
@ -14,21 +13,21 @@ var _ Operation = &SetStatusOperation{}
// SetStatusOperation will change the status of a bug
type SetStatusOperation struct {
OpBase
dag.OpBase
Status Status `json:"status"`
}
func (op *SetStatusOperation) Id() entity.Id {
return idOperation(op, &op.OpBase)
return dag.IdOperation(op, &op.OpBase)
}
func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
snapshot.Status = op.Status
snapshot.addActor(op.Author_)
snapshot.addActor(op.Author())
item := &SetStatusTimelineItem{
id: op.Id(),
Author: op.Author_,
Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime),
Status: op.Status,
}
@ -48,39 +47,9 @@ func (op *SetStatusOperation) Validate() error {
return nil
}
// UnmarshalJSON is a two step JSON unmarshalling
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
// overriding the outer op's MarshalJSON
func (op *SetStatusOperation) UnmarshalJSON(data []byte) error {
// Unmarshal OpBase and the op separately
base := OpBase{}
err := json.Unmarshal(data, &base)
if err != nil {
return err
}
aux := struct {
Status Status `json:"status"`
}{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
op.OpBase = base
op.Status = aux.Status
return nil
}
// Sign post method for gqlgen
func (op *SetStatusOperation) IsAuthored() {}
func NewSetStatusOp(author identity.Interface, unixTime int64, status Status) *SetStatusOperation {
return &SetStatusOperation{
OpBase: newOpBase(SetStatusOp, author, unixTime),
OpBase: dag.NewOpBase(SetStatusOp, author, unixTime),
Status: status,
}
}
@ -96,12 +65,15 @@ func (s SetStatusTimelineItem) Id() entity.Id {
return s.id
}
// Sign post method for gqlgen
func (s *SetStatusTimelineItem) IsAuthored() {}
// IsAuthored is a sign post method for gqlgen
func (s SetStatusTimelineItem) IsAuthored() {}
// Convenience function to apply the operation
func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
// Open is a convenience function to change a bugs state to Open
func Open(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) {
op := NewSetStatusOp(author, unixTime, OpenStatus)
for key, value := range metadata {
op.SetMetadata(key, value)
}
if err := op.Validate(); err != nil {
return nil, err
}
@ -109,9 +81,12 @@ func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOpe
return op, nil
}
// Convenience function to apply the operation
func Close(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
// Close is a convenience function to change a bugs state to Close
func Close(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) {
op := NewSetStatusOp(author, unixTime, ClosedStatus)
for key, value := range metadata {
op.SetMetadata(key, value)
}
if err := op.Validate(); err != nil {
return nil, err
}

View File

@ -1,37 +1,14 @@
package bug
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
func TestSetStatusSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewSetStatusOp(rene, unix, ClosedStatus)
data, err := json.Marshal(before)
require.NoError(t, err)
var after SetStatusOperation
err = json.Unmarshal(data, &after)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetStatusOperation {
return NewSetStatusOp(author, unixTime, ClosedStatus)
})
}

View File

@ -1,10 +1,10 @@
package bug
import (
"encoding/json"
"fmt"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/timestamp"
@ -15,22 +15,22 @@ var _ Operation = &SetTitleOperation{}
// SetTitleOperation will change the title of a bug
type SetTitleOperation struct {
OpBase
dag.OpBase
Title string `json:"title"`
Was string `json:"was"`
}
func (op *SetTitleOperation) Id() entity.Id {
return idOperation(op, &op.OpBase)
return dag.IdOperation(op, &op.OpBase)
}
func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
snapshot.Title = op.Title
snapshot.addActor(op.Author_)
snapshot.addActor(op.Author())
item := &SetTitleTimelineItem{
id: op.Id(),
Author: op.Author_,
Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime),
Title: op.Title,
Was: op.Was,
@ -59,41 +59,9 @@ func (op *SetTitleOperation) Validate() error {
return nil
}
// UnmarshalJSON is a two step JSON unmarshalling
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
// overriding the outer op's MarshalJSON
func (op *SetTitleOperation) UnmarshalJSON(data []byte) error {
// Unmarshal OpBase and the op separately
base := OpBase{}
err := json.Unmarshal(data, &base)
if err != nil {
return err
}
aux := struct {
Title string `json:"title"`
Was string `json:"was"`
}{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
op.OpBase = base
op.Title = aux.Title
op.Was = aux.Was
return nil
}
// Sign post method for gqlgen
func (op *SetTitleOperation) IsAuthored() {}
func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was string) *SetTitleOperation {
return &SetTitleOperation{
OpBase: newOpBase(SetTitleOp, author, unixTime),
OpBase: dag.NewOpBase(SetTitleOp, author, unixTime),
Title: title,
Was: was,
}
@ -111,11 +79,11 @@ func (s SetTitleTimelineItem) Id() entity.Id {
return s.id
}
// Sign post method for gqlgen
func (s *SetTitleTimelineItem) IsAuthored() {}
// IsAuthored is a sign post method for gqlgen
func (s SetTitleTimelineItem) IsAuthored() {}
// Convenience function to apply the operation
func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) {
// SetTitle is a convenience function to change a bugs title
func SetTitle(b Interface, author identity.Interface, unixTime int64, title string, metadata map[string]string) (*SetTitleOperation, error) {
var lastTitleOp *SetTitleOperation
for _, op := range b.Operations() {
switch op := op.(type) {
@ -131,12 +99,14 @@ func SetTitle(b Interface, author identity.Interface, unixTime int64, title stri
was = b.FirstOp().(*CreateOperation).Title
}
setTitleOp := NewSetTitleOp(author, unixTime, title, was)
if err := setTitleOp.Validate(); err != nil {
op := NewSetTitleOp(author, unixTime, title, was)
for key, value := range metadata {
op.SetMetadata(key, value)
}
if err := op.Validate(); err != nil {
return nil, err
}
b.Append(setTitleOp)
return setTitleOp, nil
b.Append(op)
return op, nil
}

View File

@ -1,37 +1,14 @@
package bug
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
func TestSetTitleSerialize(t *testing.T) {
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
before := NewSetTitleOp(rene, unix, "title", "was")
data, err := json.Marshal(before)
require.NoError(t, err)
var after SetTitleOperation
err = json.Unmarshal(data, &after)
require.NoError(t, err)
// enforce creating the ID
before.Id()
// Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetTitleOperation {
return NewSetTitleOp(author, unixTime, "title", "was")
})
}

View File

@ -1,23 +1,15 @@
package bug
import (
"crypto/rand"
"encoding/json"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
)
// OperationType is an operation type identifier
type OperationType int
const (
_ OperationType = iota
_ dag.OperationType = iota
CreateOp
SetTitleOp
AddCommentOp
@ -32,55 +24,24 @@ const (
type Operation interface {
dag.Operation
// Type return the type of the operation
Type() OperationType
// Time return the time when the operation was added
Time() time.Time
// Apply the operation to a Snapshot to create the final state
Apply(snapshot *Snapshot)
// SetMetadata store arbitrary metadata about the operation
SetMetadata(key string, value string)
// GetMetadata retrieve arbitrary metadata about the operation
GetMetadata(key string) (string, bool)
// AllMetadata return all metadata for this operation
AllMetadata() map[string]string
setExtraMetadataImmutable(key string, value string)
}
func idOperation(op Operation, base *OpBase) entity.Id {
if base.id == "" {
// something went really wrong
panic("op's id not set")
}
if base.id == entity.UnsetId {
// This means we are trying to get the op's Id *before* it has been stored, for instance when
// adding multiple ops in one go in an OperationPack.
// As the Id is computed based on the actual bytes written on the disk, we are going to predict
// those and then get the Id. This is safe as it will be the exact same code writing on disk later.
// make sure that package external operations do conform to our interface
var _ Operation = &dag.NoOpOperation[*Snapshot]{}
var _ Operation = &dag.SetMetadataOperation[*Snapshot]{}
data, err := json.Marshal(op)
if err != nil {
panic(err)
}
base.id = entity.DeriveId(data)
}
return base.id
}
func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
func operationUnmarshaller(raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
var t struct {
OperationType OperationType `json:"type"`
OperationType dag.OperationType `json:"type"`
}
if err := json.Unmarshal(raw, &t); err != nil {
return nil, err
}
var op Operation
var op dag.Operation
switch t.OperationType {
case AddCommentOp:
@ -92,9 +53,9 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resol
case LabelChangeOp:
op = &LabelChangeOperation{}
case NoOpOp:
op = &NoOpOperation{}
op = &dag.NoOpOperation[*Snapshot]{}
case SetMetadataOp:
op = &SetMetadataOperation{}
op = &dag.SetMetadataOperation[*Snapshot]{}
case SetStatusOp:
op = &SetStatusOperation{}
case SetTitleOp:
@ -108,188 +69,5 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resol
return nil, err
}
switch op := op.(type) {
case *AddCommentOperation:
op.Author_ = author
case *CreateOperation:
op.Author_ = author
case *EditCommentOperation:
op.Author_ = author
case *LabelChangeOperation:
op.Author_ = author
case *NoOpOperation:
op.Author_ = author
case *SetMetadataOperation:
op.Author_ = author
case *SetStatusOperation:
op.Author_ = author
case *SetTitleOperation:
op.Author_ = author
default:
panic(fmt.Sprintf("unknown operation type %T", op))
}
return op, nil
}
// OpBase implement the common code for all operations
type OpBase struct {
OperationType OperationType `json:"type"`
Author_ identity.Interface `json:"-"` // not serialized
// TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
UnixTime int64 `json:"timestamp"`
Metadata map[string]string `json:"metadata,omitempty"`
// mandatory random bytes to ensure a better randomness of the data used to later generate the ID
// len(Nonce) should be > 20 and < 64 bytes
// It has no functional purpose and should be ignored.
Nonce []byte `json:"nonce"`
// Not serialized. Store the op's id in memory.
id entity.Id
// Not serialized. Store the extra metadata in memory,
// compiled from SetMetadataOperation.
extraMetadata map[string]string
}
// newOpBase is the constructor for an OpBase
func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
return OpBase{
OperationType: opType,
Author_: author,
UnixTime: unixTime,
Nonce: makeNonce(20),
id: entity.UnsetId,
}
}
func makeNonce(len int) []byte {
result := make([]byte, len)
_, err := rand.Read(result)
if err != nil {
panic(err)
}
return result
}
func (base *OpBase) UnmarshalJSON(data []byte) error {
// Compute the Id when loading the op from disk.
base.id = entity.DeriveId(data)
aux := struct {
OperationType OperationType `json:"type"`
UnixTime int64 `json:"timestamp"`
Metadata map[string]string `json:"metadata,omitempty"`
Nonce []byte `json:"nonce"`
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
base.OperationType = aux.OperationType
base.UnixTime = aux.UnixTime
base.Metadata = aux.Metadata
base.Nonce = aux.Nonce
return nil
}
func (base *OpBase) Type() OperationType {
return base.OperationType
}
// Time return the time when the operation was added
func (base *OpBase) Time() time.Time {
return time.Unix(base.UnixTime, 0)
}
// Validate check the OpBase for errors
func (base *OpBase) Validate(op Operation, opType OperationType) error {
if base.OperationType != opType {
return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
}
if op.Time().Unix() == 0 {
return fmt.Errorf("time not set")
}
if base.Author_ == nil {
return fmt.Errorf("author not set")
}
if err := op.Author().Validate(); err != nil {
return errors.Wrap(err, "author")
}
if op, ok := op.(dag.OperationWithFiles); ok {
for _, hash := range op.GetFiles() {
if !hash.IsValid() {
return fmt.Errorf("file with invalid hash %v", hash)
}
}
}
if len(base.Nonce) > 64 {
return fmt.Errorf("nonce is too big")
}
if len(base.Nonce) < 20 {
return fmt.Errorf("nonce is too small")
}
return nil
}
// SetMetadata store arbitrary metadata about the operation
func (base *OpBase) SetMetadata(key string, value string) {
if base.Metadata == nil {
base.Metadata = make(map[string]string)
}
base.Metadata[key] = value
base.id = entity.UnsetId
}
// GetMetadata retrieve arbitrary metadata about the operation
func (base *OpBase) GetMetadata(key string) (string, bool) {
val, ok := base.Metadata[key]
if ok {
return val, true
}
// extraMetadata can't replace the original operations value if any
val, ok = base.extraMetadata[key]
return val, ok
}
// AllMetadata return all metadata for this operation
func (base *OpBase) AllMetadata() map[string]string {
result := make(map[string]string)
for key, val := range base.extraMetadata {
result[key] = val
}
// Original metadata take precedence
for key, val := range base.Metadata {
result[key] = val
}
return result
}
func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
if base.extraMetadata == nil {
base.extraMetadata = make(map[string]string)
}
if _, exist := base.extraMetadata[key]; !exist {
base.extraMetadata[key] = value
}
}
// Author return author identity
func (base *OpBase) Author() identity.Interface {
return base.Author_
}

View File

@ -6,10 +6,13 @@ import (
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
// TODO: move to entity/dag?
func TestValidate(t *testing.T) {
repo := repository.NewMockRepoClock()
@ -44,11 +47,7 @@ func TestValidate(t *testing.T) {
NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus),
NewSetStatusOp(makeIdentity(t, "René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus),
NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus),
&CreateOperation{OpBase: OpBase{
Author_: rene,
UnixTime: 0,
OperationType: CreateOp,
},
&CreateOperation{OpBase: dag.NewOpBase(CreateOp, rene, 0),
Title: "title",
Message: "message",
},
@ -105,7 +104,7 @@ func TestID(t *testing.T) {
err = rene.Commit(repo)
require.NoError(t, err)
b, op, err := Create(rene, time.Now().Unix(), "title", "message")
b, op, err := Create(rene, time.Now().Unix(), "title", "message", nil, nil)
require.NoError(t, err)
id1 := op.Id()

View File

@ -5,9 +5,12 @@ import (
"time"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
)
var _ dag.Snapshot = &Snapshot{}
// Snapshot is a compiled form of the Bug data structure used for storage and merge
type Snapshot struct {
id entity.Id
@ -23,7 +26,7 @@ type Snapshot struct {
Timeline []TimelineItem
Operations []Operation
Operations []dag.Operation
}
// Id returns the Bug identifier
@ -35,6 +38,10 @@ func (snap *Snapshot) Id() entity.Id {
return snap.id
}
func (snap *Snapshot) AllOperations() []dag.Operation {
return snap.Operations
}
// EditTime returns the last time a bug was modified
func (snap *Snapshot) EditTime() time.Time {
if len(snap.Operations) == 0 {
@ -133,5 +140,5 @@ func (snap *Snapshot) HasAnyActor(ids ...entity.Id) bool {
return false
}
// Sign post method for gqlgen
// IsAuthored is a sign post method for gqlgen
func (snap *Snapshot) IsAuthored() {}

View File

@ -1,6 +1,8 @@
package bug
import "github.com/MichaelMure/git-bug/repository"
import (
"github.com/MichaelMure/git-bug/repository"
)
var _ Interface = &WithSnapshot{}
@ -10,11 +12,10 @@ type WithSnapshot struct {
snap *Snapshot
}
// Snapshot return the current snapshot
func (b *WithSnapshot) Snapshot() *Snapshot {
func (b *WithSnapshot) Compile() *Snapshot {
if b.snap == nil {
snap := b.Bug.Compile()
b.snap = &snap
b.snap = snap
}
return b.snap
}

105
cache/bug_cache.go vendored
View File

@ -7,6 +7,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/repository"
)
@ -33,7 +34,7 @@ func NewBugCache(repoCache *RepoCache, b *bug.Bug) *BugCache {
func (c *BugCache) Snapshot() *bug.Snapshot {
c.mu.RLock()
defer c.mu.RUnlock()
return c.bug.Snapshot()
return c.bug.Compile()
}
func (c *BugCache) Id() entity.Id {
@ -84,18 +85,11 @@ func (c *BugCache) AddCommentWithFiles(message string, files []repository.Hash)
func (c *BugCache) AddCommentRaw(author *IdentityCache, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*bug.AddCommentOperation, error) {
c.mu.Lock()
op, err := bug.AddCommentWithFiles(c.bug, author.Identity, unixTime, message, files)
op, err := bug.AddComment(c.bug, author, unixTime, message, files, metadata)
c.mu.Unlock()
if err != nil {
c.mu.Unlock()
return nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated()
}
@ -110,24 +104,12 @@ func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelCh
func (c *BugCache) ChangeLabelsRaw(author *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) {
c.mu.Lock()
changes, op, err := bug.ChangeLabels(c.bug, author.Identity, unixTime, added, removed)
changes, op, err := bug.ChangeLabels(c.bug, author.Identity, unixTime, added, removed, metadata)
c.mu.Unlock()
if err != nil {
c.mu.Unlock()
return changes, nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
err = c.notifyUpdated()
if err != nil {
return nil, nil, err
}
return changes, op, nil
return changes, op, c.notifyUpdated()
}
func (c *BugCache) ForceChangeLabels(added []string, removed []string) (*bug.LabelChangeOperation, error) {
@ -141,23 +123,12 @@ func (c *BugCache) ForceChangeLabels(added []string, removed []string) (*bug.Lab
func (c *BugCache) ForceChangeLabelsRaw(author *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) (*bug.LabelChangeOperation, error) {
c.mu.Lock()
op, err := bug.ForceChangeLabels(c.bug, author.Identity, unixTime, added, removed)
if err != nil {
c.mu.Unlock()
return nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
op, err := bug.ForceChangeLabels(c.bug, author.Identity, unixTime, added, removed, metadata)
c.mu.Unlock()
err = c.notifyUpdated()
if err != nil {
return nil, err
}
return op, nil
return op, c.notifyUpdated()
}
func (c *BugCache) Open() (*bug.SetStatusOperation, error) {
@ -171,17 +142,11 @@ func (c *BugCache) Open() (*bug.SetStatusOperation, error) {
func (c *BugCache) OpenRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) {
c.mu.Lock()
op, err := bug.Open(c.bug, author.Identity, unixTime)
op, err := bug.Open(c.bug, author.Identity, unixTime, metadata)
c.mu.Unlock()
if err != nil {
c.mu.Unlock()
return nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated()
}
@ -196,17 +161,11 @@ func (c *BugCache) Close() (*bug.SetStatusOperation, error) {
func (c *BugCache) CloseRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) {
c.mu.Lock()
op, err := bug.Close(c.bug, author.Identity, unixTime)
op, err := bug.Close(c.bug, author.Identity, unixTime, metadata)
c.mu.Unlock()
if err != nil {
c.mu.Unlock()
return nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated()
}
@ -221,17 +180,11 @@ func (c *BugCache) SetTitle(title string) (*bug.SetTitleOperation, error) {
func (c *BugCache) SetTitleRaw(author *IdentityCache, unixTime int64, title string, metadata map[string]string) (*bug.SetTitleOperation, error) {
c.mu.Lock()
op, err := bug.SetTitle(c.bug, author.Identity, unixTime, title)
op, err := bug.SetTitle(c.bug, author.Identity, unixTime, title, metadata)
c.mu.Unlock()
if err != nil {
c.mu.Unlock()
return nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated()
}
@ -248,17 +201,11 @@ func (c *BugCache) EditCreateComment(body string) (*bug.EditCommentOperation, er
// EditCreateCommentRaw is a convenience function to edit the body of a bug (the first comment)
func (c *BugCache) EditCreateCommentRaw(author *IdentityCache, unixTime int64, body string, metadata map[string]string) (*bug.EditCommentOperation, error) {
c.mu.Lock()
op, err := bug.EditCreateComment(c.bug, author.Identity, unixTime, body)
op, err := bug.EditCreateComment(c.bug, author.Identity, unixTime, body, nil, metadata)
c.mu.Unlock()
if err != nil {
c.mu.Unlock()
return nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated()
}
@ -273,21 +220,15 @@ func (c *BugCache) EditComment(target entity.Id, message string) (*bug.EditComme
func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target entity.Id, message string, metadata map[string]string) (*bug.EditCommentOperation, error) {
c.mu.Lock()
op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message)
op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message, nil, metadata)
c.mu.Unlock()
if err != nil {
c.mu.Unlock()
return nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated()
}
func (c *BugCache) SetMetadata(target entity.Id, newMetadata map[string]string) (*bug.SetMetadataOperation, error) {
func (c *BugCache) SetMetadata(target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*bug.Snapshot], error) {
author, err := c.repoCache.GetUserIdentity()
if err != nil {
return nil, err
@ -296,15 +237,13 @@ func (c *BugCache) SetMetadata(target entity.Id, newMetadata map[string]string)
return c.SetMetadataRaw(author, time.Now().Unix(), target, newMetadata)
}
func (c *BugCache) SetMetadataRaw(author *IdentityCache, unixTime int64, target entity.Id, newMetadata map[string]string) (*bug.SetMetadataOperation, error) {
func (c *BugCache) SetMetadataRaw(author *IdentityCache, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*bug.Snapshot], error) {
c.mu.Lock()
op, err := bug.SetMetadata(c.bug, author.Identity, unixTime, target, newMetadata)
c.mu.Unlock()
if err != nil {
c.mu.Unlock()
return nil, err
}
c.mu.Unlock()
return op, c.notifyUpdated()
}

6
cache/repo_cache.go vendored
View File

@ -209,9 +209,9 @@ func (c *RepoCache) buildCache() error {
}
snap := b.Bug.Compile()
c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, snap)
if err := c.addBugToSearchIndex(&snap); err != nil {
if err := c.addBugToSearchIndex(snap); err != nil {
return err
}
}
@ -222,7 +222,7 @@ func (c *RepoCache) buildCache() error {
}
// repoIsAvailable check is the given repository is locked by a Cache.
// Note: this is a smart function that will cleanup the lock file if the
// Note: this is a smart function that will clean the lock file if the
// corresponding process is not there anymore.
// If no error is returned, the repo is free to edit.
func repoIsAvailable(repo repository.RepoStorage) error {

View File

@ -461,15 +461,11 @@ func (c *RepoCache) NewBugWithFiles(title string, message string, files []reposi
// well as metadata for the Create operation.
// The new bug is written in the repository (commit)
func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []repository.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) {
b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files)
b, op, err := bug.Create(author.Identity, unixTime, title, message, files, metadata)
if err != nil {
return nil, nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
err = b.Commit(c.repo)
if err != nil {
return nil, nil, err

View File

@ -36,7 +36,7 @@ func (c *RepoCache) Keyring() repository.Keyring {
return c.repo.Keyring()
}
// GetUserName returns the name the the user has used to configure git
// GetUserName returns the name the user has used to configure git
func (c *RepoCache) GetUserName() (string, error) {
return c.repo.GetUserName()
}
@ -131,7 +131,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult {
b := result.Entity.(*bug.Bug)
snap := b.Compile()
c.muBug.Lock()
c.bugExcerpts[result.Id] = NewBugExcerpt(b, &snap)
c.bugExcerpts[result.Id] = NewBugExcerpt(b, snap)
c.muBug.Unlock()
}
}

View File

@ -18,78 +18,73 @@ import (
Operations
*/
type op1 struct {
author identity.Interface
const (
_ OperationType = iota
Op1
Op2
)
OperationType int `json:"type"`
Field1 string `json:"field_1"`
Files []repository.Hash `json:"files"`
type op1 struct {
OpBase
Field1 string `json:"field_1"`
Files []repository.Hash `json:"files"`
}
func newOp1(author identity.Interface, field1 string, files ...repository.Hash) *op1 {
return &op1{author: author, OperationType: 1, Field1: field1, Files: files}
return &op1{OpBase: NewOpBase(Op1, author, 0), Field1: field1, Files: files}
}
func (o *op1) Id() entity.Id {
data, _ := json.Marshal(o)
return entity.DeriveId(data)
func (op *op1) Id() entity.Id {
return IdOperation(op, &op.OpBase)
}
func (o *op1) Validate() error { return nil }
func (op *op1) Validate() error { return nil }
func (o *op1) Author() identity.Interface {
return o.author
}
func (o *op1) GetFiles() []repository.Hash {
return o.Files
func (op *op1) GetFiles() []repository.Hash {
return op.Files
}
type op2 struct {
author identity.Interface
OperationType int `json:"type"`
Field2 string `json:"field_2"`
OpBase
Field2 string `json:"field_2"`
}
func newOp2(author identity.Interface, field2 string) *op2 {
return &op2{author: author, OperationType: 2, Field2: field2}
return &op2{OpBase: NewOpBase(Op2, author, 0), Field2: field2}
}
func (o *op2) Id() entity.Id {
data, _ := json.Marshal(o)
return entity.DeriveId(data)
func (op *op2) Id() entity.Id {
return IdOperation(op, &op.OpBase)
}
func (o *op2) Validate() error { return nil }
func (op *op2) Validate() error { return nil }
func (o *op2) Author() identity.Interface {
return o.author
}
func unmarshaler(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (Operation, error) {
func unmarshaler(raw json.RawMessage, resolver identity.Resolver) (Operation, error) {
var t struct {
OperationType int `json:"type"`
OperationType OperationType `json:"type"`
}
if err := json.Unmarshal(raw, &t); err != nil {
return nil, err
}
var op Operation
switch t.OperationType {
case 1:
op := &op1{}
err := json.Unmarshal(raw, &op)
op.author = author
return op, err
case 2:
op := &op2{}
err := json.Unmarshal(raw, &op)
op.author = author
return op, err
case Op1:
op = &op1{}
case Op2:
op = &op2{}
default:
return nil, fmt.Errorf("unknown operation type %v", t.OperationType)
}
err := json.Unmarshal(raw, &op)
if err != nil {
return nil, err
}
return op, nil
}
/*

View File

@ -26,7 +26,7 @@ type Definition struct {
// the Namespace in git references (bugs, prs, ...)
Namespace string
// a function decoding a JSON message into an Operation
OperationUnmarshaler func(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (Operation, error)
OperationUnmarshaler func(raw json.RawMessage, resolver identity.Resolver) (Operation, error)
// the expected format version number, that can be used for data migration/upgrade
FormatVersion uint
}

View File

@ -50,6 +50,8 @@ func TestWriteReadMultipleAuthor(t *testing.T) {
}
func assertEqualEntities(t *testing.T, a, b *Entity) {
t.Helper()
// testify doesn't support comparing functions and systematically fail if they are not nil
// so we have to set them to nil temporarily

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
@ -64,10 +65,8 @@ type Operation interface {
Apply(snapshot *Snapshot)
}
type OperationType int
const (
_ OperationType = iota
_ dag.OperationType = iota
SetSignatureRequiredOp
AddAdministratorOp
RemoveAdministratorOp
@ -75,37 +74,30 @@ const (
// SetSignatureRequired is an operation to set/unset if git signature are required.
type SetSignatureRequired struct {
author identity.Interface
OperationType OperationType `json:"type"`
Value bool `json:"value"`
dag.OpBase
Value bool `json:"value"`
}
func NewSetSignatureRequired(author identity.Interface, value bool) *SetSignatureRequired {
return &SetSignatureRequired{author: author, OperationType: SetSignatureRequiredOp, Value: value}
return &SetSignatureRequired{
OpBase: dag.NewOpBase(SetSignatureRequiredOp, author, time.Now().Unix()),
Value: value,
}
}
func (ssr *SetSignatureRequired) Id() entity.Id {
// the Id of the operation is the hash of the serialized data.
// we could memorize the Id when deserializing, but that will do
data, _ := json.Marshal(ssr)
return entity.DeriveId(data)
return dag.IdOperation(ssr, &ssr.OpBase)
}
func (ssr *SetSignatureRequired) Validate() error {
if ssr.author == nil {
return fmt.Errorf("author not set")
}
return ssr.author.Validate()
}
func (ssr *SetSignatureRequired) Author() identity.Interface {
return ssr.author
return ssr.OpBase.Validate(ssr, SetSignatureRequiredOp)
}
// Apply is the function that makes changes on the snapshot
func (ssr *SetSignatureRequired) Apply(snapshot *Snapshot) {
// check that we are allowed to change the config
if _, ok := snapshot.Administrator[ssr.author]; !ok {
if _, ok := snapshot.Administrator[ssr.Author()]; !ok {
return
}
snapshot.SignatureRequired = ssr.Value
@ -113,24 +105,20 @@ func (ssr *SetSignatureRequired) Apply(snapshot *Snapshot) {
// AddAdministrator is an operation to add a new administrator in the set
type AddAdministrator struct {
author identity.Interface
OperationType OperationType `json:"type"`
ToAdd []identity.Interface `json:"to_add"`
}
// addAdministratorJson is a helper struct to deserialize identities with a concrete type.
type addAdministratorJson struct {
ToAdd []identity.IdentityStub `json:"to_add"`
dag.OpBase
ToAdd []identity.Interface `json:"to_add"`
}
func NewAddAdministratorOp(author identity.Interface, toAdd ...identity.Interface) *AddAdministrator {
return &AddAdministrator{author: author, OperationType: AddAdministratorOp, ToAdd: toAdd}
return &AddAdministrator{
OpBase: dag.NewOpBase(AddAdministratorOp, author, time.Now().Unix()),
ToAdd: toAdd,
}
}
func (aa *AddAdministrator) Id() entity.Id {
// we could memorize the Id when deserializing, but that will do
data, _ := json.Marshal(aa)
return entity.DeriveId(data)
// the Id of the operation is the hash of the serialized data.
return dag.IdOperation(aa, &aa.OpBase)
}
func (aa *AddAdministrator) Validate() error {
@ -138,20 +126,13 @@ func (aa *AddAdministrator) Validate() error {
if len(aa.ToAdd) == 0 {
return fmt.Errorf("nothing to add")
}
if aa.author == nil {
return fmt.Errorf("author not set")
}
return aa.author.Validate()
}
func (aa *AddAdministrator) Author() identity.Interface {
return aa.author
return aa.OpBase.Validate(aa, AddAdministratorOp)
}
// Apply is the function that makes changes on the snapshot
func (aa *AddAdministrator) Apply(snapshot *Snapshot) {
// check that we are allowed to change the config ... or if there is no admin yet
if !snapshot.HasAdministrator(aa.author) && len(snapshot.Administrator) != 0 {
if !snapshot.HasAdministrator(aa.Author()) && len(snapshot.Administrator) != 0 {
return
}
for _, toAdd := range aa.ToAdd {
@ -161,25 +142,20 @@ func (aa *AddAdministrator) Apply(snapshot *Snapshot) {
// RemoveAdministrator is an operation to remove an administrator from the set
type RemoveAdministrator struct {
author identity.Interface
OperationType OperationType `json:"type"`
ToRemove []identity.Interface `json:"to_remove"`
}
// removeAdministratorJson is a helper struct to deserialize identities with a concrete type.
type removeAdministratorJson struct {
dag.OpBase
ToRemove []identity.Interface `json:"to_remove"`
}
func NewRemoveAdministratorOp(author identity.Interface, toRemove ...identity.Interface) *RemoveAdministrator {
return &RemoveAdministrator{author: author, OperationType: RemoveAdministratorOp, ToRemove: toRemove}
return &RemoveAdministrator{
OpBase: dag.NewOpBase(RemoveAdministratorOp, author, time.Now().Unix()),
ToRemove: toRemove,
}
}
func (ra *RemoveAdministrator) Id() entity.Id {
// the Id of the operation is the hash of the serialized data.
// we could memorize the Id when deserializing, but that will do
data, _ := json.Marshal(ra)
return entity.DeriveId(data)
return dag.IdOperation(ra, &ra.OpBase)
}
func (ra *RemoveAdministrator) Validate() error {
@ -188,26 +164,19 @@ func (ra *RemoveAdministrator) Validate() error {
if len(ra.ToRemove) == 0 {
return fmt.Errorf("nothing to remove")
}
if ra.author == nil {
return fmt.Errorf("author not set")
}
return ra.author.Validate()
}
func (ra *RemoveAdministrator) Author() identity.Interface {
return ra.author
return ra.OpBase.Validate(ra, RemoveAdministratorOp)
}
// Apply is the function that makes changes on the snapshot
func (ra *RemoveAdministrator) Apply(snapshot *Snapshot) {
// check if we are allowed to make changes
if !snapshot.HasAdministrator(ra.author) {
if !snapshot.HasAdministrator(ra.Author()) {
return
}
// special rule: we can't end up with no administrator
stillSome := false
for admin, _ := range snapshot.Administrator {
if admin != ra.author {
if admin != ra.Author() {
stillSome = true
break
}
@ -245,71 +214,52 @@ var def = dag.Definition{
// operationUnmarshaller is a function doing the de-serialization of the JSON data into our own
// concrete Operations. If needed, we can use the resolver to connect to other entities.
func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
func operationUnmarshaller(raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
var t struct {
OperationType OperationType `json:"type"`
OperationType dag.OperationType `json:"type"`
}
if err := json.Unmarshal(raw, &t); err != nil {
return nil, err
}
var value interface{}
var op dag.Operation
switch t.OperationType {
case AddAdministratorOp:
value = &addAdministratorJson{}
op = &AddAdministrator{}
case RemoveAdministratorOp:
value = &removeAdministratorJson{}
op = &RemoveAdministrator{}
case SetSignatureRequiredOp:
value = &SetSignatureRequired{}
op = &SetSignatureRequired{}
default:
panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
}
err := json.Unmarshal(raw, &value)
err := json.Unmarshal(raw, &op)
if err != nil {
return nil, err
}
var op Operation
switch value := value.(type) {
case *SetSignatureRequired:
value.author = author
op = value
case *addAdministratorJson:
// We need something less straightforward to deserialize and resolve identities
aa := &AddAdministrator{
author: author,
OperationType: AddAdministratorOp,
ToAdd: make([]identity.Interface, len(value.ToAdd)),
}
for i, stub := range value.ToAdd {
switch op := op.(type) {
case *AddAdministrator:
// We need to resolve identities
for i, stub := range op.ToAdd {
iden, err := resolver.ResolveIdentity(stub.Id())
if err != nil {
return nil, err
}
aa.ToAdd[i] = iden
op.ToAdd[i] = iden
}
op = aa
case *removeAdministratorJson:
// We need something less straightforward to deserialize and resolve identities
ra := &RemoveAdministrator{
author: author,
OperationType: RemoveAdministratorOp,
ToRemove: make([]identity.Interface, len(value.ToRemove)),
}
for i, stub := range value.ToRemove {
case *RemoveAdministrator:
// We need to resolve identities
for i, stub := range op.ToRemove {
iden, err := resolver.ResolveIdentity(stub.Id())
if err != nil {
return nil, err
}
ra.ToRemove[i] = iden
op.ToRemove[i] = iden
}
op = ra
default:
panic(fmt.Sprintf("unknown operation type %T", value))
}
return op, nil

39
entity/dag/op_noop.go Normal file
View File

@ -0,0 +1,39 @@
package dag
import (
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/identity"
)
var _ Operation = &NoOpOperation[Snapshot]{}
var _ OperationDoesntChangeSnapshot = &NoOpOperation[Snapshot]{}
// NoOpOperation is an operation that does not change the entity state. It can
// however be used to store arbitrary metadata in the entity history, for example
// to support a bridge feature.
type NoOpOperation[SnapT Snapshot] struct {
OpBase
}
func NewNoOpOp[SnapT Snapshot](opType OperationType, author identity.Interface, unixTime int64) *NoOpOperation[SnapT] {
return &NoOpOperation[SnapT]{
OpBase: NewOpBase(opType, author, unixTime),
}
}
func (op *NoOpOperation[SnapT]) Id() entity.Id {
return IdOperation(op, &op.OpBase)
}
func (op *NoOpOperation[SnapT]) Apply(snapshot SnapT) {
// Nothing to do
}
func (op *NoOpOperation[SnapT]) Validate() error {
if err := op.OpBase.Validate(op, op.OperationType); err != nil {
return err
}
return nil
}
func (op *NoOpOperation[SnapT]) DoesntChangeSnapshot() {}

View File

@ -0,0 +1,13 @@
package dag
import (
"testing"
"github.com/MichaelMure/git-bug/identity"
)
func TestNoopSerialize(t *testing.T) {
SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *NoOpOperation[*snapshotMock] {
return NewNoOpOp[*snapshotMock](1, author, unixTime)
})
}

View File

@ -0,0 +1,68 @@
package dag
import (
"fmt"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/text"
)
var _ Operation = &SetMetadataOperation[Snapshot]{}
var _ OperationDoesntChangeSnapshot = &SetMetadataOperation[Snapshot]{}
type SetMetadataOperation[SnapT Snapshot] struct {
OpBase
Target entity.Id `json:"target"`
NewMetadata map[string]string `json:"new_metadata"`
}
func NewSetMetadataOp[SnapT Snapshot](opType OperationType, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *SetMetadataOperation[SnapT] {
return &SetMetadataOperation[SnapT]{
OpBase: NewOpBase(opType, author, unixTime),
Target: target,
NewMetadata: newMetadata,
}
}
func (op *SetMetadataOperation[SnapT]) Id() entity.Id {
return IdOperation(op, &op.OpBase)
}
func (op *SetMetadataOperation[SnapT]) Apply(snapshot SnapT) {
for _, target := range snapshot.AllOperations() {
if target.Id() == op.Target {
// Apply the metadata in an immutable way: if a metadata already
// exist, it's not possible to override it.
for key, value := range op.NewMetadata {
target.setExtraMetadataImmutable(key, value)
}
return
}
}
}
func (op *SetMetadataOperation[SnapT]) Validate() error {
if err := op.OpBase.Validate(op, op.OperationType); err != nil {
return err
}
if err := op.Target.Validate(); err != nil {
return errors.Wrap(err, "target invalid")
}
for key, val := range op.NewMetadata {
if !text.SafeOneLine(key) {
return fmt.Errorf("metadata key is unsafe")
}
if !text.Safe(val) {
return fmt.Errorf("metadata value is not fully printable")
}
}
return nil
}
func (op *SetMetadataOperation[SnapT]) DoesntChangeSnapshot() {}

View File

@ -0,0 +1,106 @@
package dag
import (
"testing"
"time"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/stretchr/testify/require"
)
type snapshotMock struct {
ops []Operation
}
func (s *snapshotMock) AllOperations() []Operation {
return s.ops
}
func TestSetMetadata(t *testing.T) {
snap := &snapshotMock{}
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
unix := time.Now().Unix()
target1 := NewNoOpOp[*snapshotMock](1, rene, unix)
target1.SetMetadata("key", "value")
snap.ops = append(snap.ops, target1)
target2 := NewNoOpOp[*snapshotMock](1, rene, unix)
target2.SetMetadata("key2", "value2")
snap.ops = append(snap.ops, target2)
op1 := NewSetMetadataOp[*snapshotMock](2, rene, unix, target1.Id(), map[string]string{
"key": "override",
"key2": "value",
})
op1.Apply(snap)
snap.ops = append(snap.ops, op1)
target1Metadata := snap.AllOperations()[0].AllMetadata()
require.Len(t, target1Metadata, 2)
// original key is not overrided
require.Equal(t, target1Metadata["key"], "value")
// new key is set
require.Equal(t, target1Metadata["key2"], "value")
target2Metadata := snap.AllOperations()[1].AllMetadata()
require.Len(t, target2Metadata, 1)
require.Equal(t, target2Metadata["key2"], "value2")
op2 := NewSetMetadataOp[*snapshotMock](2, rene, unix, target2.Id(), map[string]string{
"key2": "value",
"key3": "value3",
})
op2.Apply(snap)
snap.ops = append(snap.ops, op2)
target1Metadata = snap.AllOperations()[0].AllMetadata()
require.Len(t, target1Metadata, 2)
require.Equal(t, target1Metadata["key"], "value")
require.Equal(t, target1Metadata["key2"], "value")
target2Metadata = snap.AllOperations()[1].AllMetadata()
require.Len(t, target2Metadata, 2)
// original key is not overrided
require.Equal(t, target2Metadata["key2"], "value2")
// new key is set
require.Equal(t, target2Metadata["key3"], "value3")
op3 := NewSetMetadataOp[*snapshotMock](2, rene, unix, target1.Id(), map[string]string{
"key": "override",
"key2": "override",
})
op3.Apply(snap)
snap.ops = append(snap.ops, op3)
target1Metadata = snap.AllOperations()[0].AllMetadata()
require.Len(t, target1Metadata, 2)
// original key is not overrided
require.Equal(t, target1Metadata["key"], "value")
// previously set key is not overrided
require.Equal(t, target1Metadata["key2"], "value")
target2Metadata = snap.AllOperations()[1].AllMetadata()
require.Len(t, target2Metadata, 2)
require.Equal(t, target2Metadata["key2"], "value2")
require.Equal(t, target2Metadata["key3"], "value3")
}
func TestSetMetadataSerialize(t *testing.T) {
SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetMetadataOperation[*snapshotMock] {
return NewSetMetadataOp[*snapshotMock](1, author, unixTime, "message", map[string]string{
"key1": "value1",
"key2": "value2",
})
})
}

View File

@ -1,11 +1,21 @@
package dag
import (
"crypto/rand"
"encoding/json"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
// OperationType is an operation type identifier
type OperationType int
// Operation is a piece of data defining a change to reflect on the state of an Entity.
// What this Operation or Entity's state looks like is not of the resort of this package as it only deals with the
// data structure and storage.
@ -22,23 +32,39 @@ type Operation interface {
// a minimal amount of entropy and avoid collision.
//
// Author's note: I tried to find a clever way around that inelegance (stuffing random useless data into the stored
// structure is not exactly elegant) but I failed to find a proper way. Essentially, anything that would reuse some
// structure is not exactly elegant), but I failed to find a proper way. Essentially, anything that would reuse some
// other data (parent operation's Id, lamport clock) or the graph structure (depth) impose that the Id would only
// make sense in the context of the graph and yield some deep coupling between Entity and Operation. This in turn
// make the whole thing even less elegant.
//
// A common way to derive an Id will be to use the entity.DeriveId() function on the serialized operation data.
Id() entity.Id
// Type return the type of the operation
Type() OperationType
// Validate check if the Operation data is valid
Validate() error
// Author returns the author of this operation
Author() identity.Interface
// Time return the time when the operation was added
Time() time.Time
// SetMetadata store arbitrary metadata about the operation
SetMetadata(key string, value string)
// GetMetadata retrieve arbitrary metadata about the operation
GetMetadata(key string) (string, bool)
// AllMetadata return all metadata for this operation
AllMetadata() map[string]string
// setId allow to set the Id, used when unmarshalling only
setId(id entity.Id)
// setAuthor allow to set the author, used when unmarshalling only
setAuthor(author identity.Interface)
// setExtraMetadataImmutable add a metadata not carried by the operation itself on the operation
setExtraMetadataImmutable(key string, value string)
}
// OperationWithFiles is an extended Operation that has files dependency, stored in git.
// OperationWithFiles is an optional extension for an Operation that has files dependency, stored in git.
type OperationWithFiles interface {
Operation
// GetFiles return the files needed by this operation
// This implies that the Operation maintain and store internally the references to those files. This is how
// this information is read later, when loading from storage.
@ -46,3 +72,201 @@ type OperationWithFiles interface {
// hash).
GetFiles() []repository.Hash
}
// OperationDoesntChangeSnapshot is an interface signaling that the Operation implementing it doesn't change the
// snapshot, for example a metadata operation that act on other operations.
type OperationDoesntChangeSnapshot interface {
DoesntChangeSnapshot()
}
// Snapshot is the minimal interface that a snapshot need to implement
type Snapshot interface {
// AllOperations returns all the operations that have been applied to that snapshot, in order
AllOperations() []Operation
}
// OpBase implement the common feature that every Operation should support.
type OpBase struct {
// Not serialized. Store the op's id in memory.
id entity.Id
// Not serialized
author identity.Interface
OperationType OperationType `json:"type"`
UnixTime int64 `json:"timestamp"`
// mandatory random bytes to ensure a better randomness of the data used to later generate the ID
// len(Nonce) should be > 20 and < 64 bytes
// It has no functional purpose and should be ignored.
Nonce []byte `json:"nonce"`
Metadata map[string]string `json:"metadata,omitempty"`
// Not serialized. Store the extra metadata in memory,
// compiled from SetMetadataOperation.
extraMetadata map[string]string
}
func NewOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
return OpBase{
OperationType: opType,
author: author,
UnixTime: unixTime,
Nonce: makeNonce(20),
id: entity.UnsetId,
}
}
func makeNonce(len int) []byte {
result := make([]byte, len)
_, err := rand.Read(result)
if err != nil {
panic(err)
}
return result
}
func IdOperation(op Operation, base *OpBase) entity.Id {
if base.id == "" {
// something went really wrong
panic("op's id not set")
}
if base.id == entity.UnsetId {
// This means we are trying to get the op's Id *before* it has been stored, for instance when
// adding multiple ops in one go in an OperationPack.
// As the Id is computed based on the actual bytes written on the disk, we are going to predict
// those and then get the Id. This is safe as it will be the exact same code writing on disk later.
data, err := json.Marshal(op)
if err != nil {
panic(err)
}
base.id = entity.DeriveId(data)
}
return base.id
}
func (base *OpBase) Type() OperationType {
return base.OperationType
}
// Time return the time when the operation was added
func (base *OpBase) Time() time.Time {
return time.Unix(base.UnixTime, 0)
}
// Validate check the OpBase for errors
func (base *OpBase) Validate(op Operation, opType OperationType) error {
if base.OperationType == 0 {
return fmt.Errorf("operation type unset")
}
if base.OperationType != opType {
return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
}
if op.Time().Unix() == 0 {
return fmt.Errorf("time not set")
}
if base.author == nil {
return fmt.Errorf("author not set")
}
if err := op.Author().Validate(); err != nil {
return errors.Wrap(err, "author")
}
if op, ok := op.(OperationWithFiles); ok {
for _, hash := range op.GetFiles() {
if !hash.IsValid() {
return fmt.Errorf("file with invalid hash %v", hash)
}
}
}
if len(base.Nonce) > 64 {
return fmt.Errorf("nonce is too big")
}
if len(base.Nonce) < 20 {
return fmt.Errorf("nonce is too small")
}
return nil
}
// IsAuthored is a sign post method for gqlgen
func (base *OpBase) IsAuthored() {}
// Author return author identity
func (base *OpBase) Author() identity.Interface {
return base.author
}
// IdIsSet returns true if the id has been set already
func (base *OpBase) IdIsSet() bool {
return base.id != "" && base.id != entity.UnsetId
}
// SetMetadata store arbitrary metadata about the operation
func (base *OpBase) SetMetadata(key string, value string) {
if base.IdIsSet() {
panic("set metadata on an operation with already an Id")
}
if base.Metadata == nil {
base.Metadata = make(map[string]string)
}
base.Metadata[key] = value
}
// GetMetadata retrieve arbitrary metadata about the operation
func (base *OpBase) GetMetadata(key string) (string, bool) {
val, ok := base.Metadata[key]
if ok {
return val, true
}
// extraMetadata can't replace the original operations value if any
val, ok = base.extraMetadata[key]
return val, ok
}
// AllMetadata return all metadata for this operation
func (base *OpBase) AllMetadata() map[string]string {
result := make(map[string]string)
for key, val := range base.extraMetadata {
result[key] = val
}
// Original metadata take precedence
for key, val := range base.Metadata {
result[key] = val
}
return result
}
// setId allow to set the Id, used when unmarshalling only
func (base *OpBase) setId(id entity.Id) {
if base.id != "" && base.id != entity.UnsetId {
panic("trying to set id again")
}
base.id = id
}
// setAuthor allow to set the author, used when unmarshalling only
func (base *OpBase) setAuthor(author identity.Interface) {
base.author = author
}
func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
if base.extraMetadata == nil {
base.extraMetadata = make(map[string]string)
}
if _, exist := base.extraMetadata[key]; !exist {
base.extraMetadata[key] = value
}
}

View File

@ -314,10 +314,15 @@ func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([]
for _, raw := range aux.Operations {
// delegate to specialized unmarshal function
op, err := def.OperationUnmarshaler(author, raw, resolver)
op, err := def.OperationUnmarshaler(raw, resolver)
if err != nil {
return nil, nil, err
}
// Set the id from the serialized data
op.setId(entity.DeriveId(raw))
// Set the author, taken from the OperationPack
op.setAuthor(author)
ops = append(ops, op)
}

View File

@ -11,13 +11,13 @@ import (
)
func TestOperationPackReadWrite(t *testing.T) {
repo, id1, _, resolver, def := makeTestContext()
repo, author, _, resolver, def := makeTestContext()
opp := &operationPack{
Author: id1,
Author: author,
Operations: []Operation{
newOp1(id1, "foo"),
newOp2(id1, "bar"),
newOp1(author, "foo"),
newOp2(author, "bar"),
},
CreateTime: 123,
EditTime: 456,
@ -32,34 +32,26 @@ func TestOperationPackReadWrite(t *testing.T) {
opp2, err := readOperationPack(def, repo, resolver, commit)
require.NoError(t, err)
require.Equal(t, opp, opp2)
// make sure we get the same Id with the same data
opp3 := &operationPack{
Author: id1,
Operations: []Operation{
newOp1(id1, "foo"),
newOp2(id1, "bar"),
},
CreateTime: 123,
EditTime: 456,
for _, op := range opp.Operations {
// force the creation of the id
op.Id()
}
require.Equal(t, opp.Id(), opp3.Id())
require.Equal(t, opp, opp2)
}
func TestOperationPackSignedReadWrite(t *testing.T) {
repo, id1, _, resolver, def := makeTestContext()
repo, author, _, resolver, def := makeTestContext()
err := id1.(*identity.Identity).Mutate(repo, func(orig *identity.Mutator) {
err := author.(*identity.Identity).Mutate(repo, func(orig *identity.Mutator) {
orig.Keys = append(orig.Keys, identity.GenerateKey())
})
require.NoError(t, err)
opp := &operationPack{
Author: id1,
Author: author,
Operations: []Operation{
newOp1(id1, "foo"),
newOp2(id1, "bar"),
newOp1(author, "foo"),
newOp2(author, "bar"),
},
CreateTime: 123,
EditTime: 456,
@ -74,23 +66,15 @@ func TestOperationPackSignedReadWrite(t *testing.T) {
opp2, err := readOperationPack(def, repo, resolver, commit)
require.NoError(t, err)
require.Equal(t, opp, opp2)
// make sure we get the same Id with the same data
opp3 := &operationPack{
Author: id1,
Operations: []Operation{
newOp1(id1, "foo"),
newOp2(id1, "bar"),
},
CreateTime: 123,
EditTime: 456,
for _, op := range opp.Operations {
// force the creation of the id
op.Id()
}
require.Equal(t, opp.Id(), opp3.Id())
require.Equal(t, opp, opp2)
}
func TestOperationPackFiles(t *testing.T) {
repo, id1, _, resolver, def := makeTestContext()
repo, author, _, resolver, def := makeTestContext()
blobHash1, err := repo.StoreData(randomData())
require.NoError(t, err)
@ -99,10 +83,10 @@ func TestOperationPackFiles(t *testing.T) {
require.NoError(t, err)
opp := &operationPack{
Author: id1,
Author: author,
Operations: []Operation{
newOp1(id1, "foo", blobHash1, blobHash2),
newOp1(id1, "foo", blobHash2),
newOp1(author, "foo", blobHash1, blobHash2),
newOp1(author, "foo", blobHash2),
},
CreateTime: 123,
EditTime: 456,
@ -117,6 +101,10 @@ func TestOperationPackFiles(t *testing.T) {
opp2, err := readOperationPack(def, repo, resolver, commit)
require.NoError(t, err)
for _, op := range opp.Operations {
// force the creation of the id
op.Id()
}
require.Equal(t, opp, opp2)
require.ElementsMatch(t, opp2.Operations[0].(OperationWithFiles).GetFiles(), []repository.Hash{

View File

@ -0,0 +1,57 @@
package dag
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
// SerializeRoundTripTest realize a marshall/unmarshall round-trip in the same condition as with OperationPack,
// and check if the recovered operation is identical.
func SerializeRoundTripTest[OpT Operation](t *testing.T, maker func(author identity.Interface, unixTime int64) OpT) {
repo := repository.NewMockRepo()
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
require.NoError(t, err)
op := maker(rene, time.Now().Unix())
// enforce having an id
op.Id()
rdt := &roundTripper[OpT]{Before: op, author: rene}
data, err := json.Marshal(rdt)
require.NoError(t, err)
err = json.Unmarshal(data, &rdt)
require.NoError(t, err)
require.Equal(t, op, rdt.after)
}
type roundTripper[OpT Operation] struct {
Before OpT
author identity.Interface
after OpT
}
func (r *roundTripper[OpT]) MarshalJSON() ([]byte, error) {
return json.Marshal(r.Before)
}
func (r *roundTripper[OpT]) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &r.after); err != nil {
return err
}
// Set the id from the serialized data
r.after.setId(entity.DeriveId(data))
// Set the author, as OperationPack would do
r.after.setAuthor(r.author)
return nil
}

78
go.mod
View File

@ -1,17 +1,16 @@
module github.com/MichaelMure/git-bug
go 1.16
go 1.18
require (
github.com/99designs/gqlgen v0.17.1
github.com/99designs/gqlgen v0.17.13
github.com/99designs/keyring v1.2.1
github.com/MichaelMure/go-term-text v0.3.1
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7
github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195
github.com/awesome-gocui/gocui v1.1.0
github.com/blevesearch/bleve v1.0.14
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9
github.com/corpix/uarand v0.1.1 // indirect
github.com/cheekybits/genny v1.0.0
github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.13.0
github.com/go-git/go-billy/v5 v5.3.1
@ -23,11 +22,10 @@ require (
github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5
github.com/pkg/errors v0.9.1
github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e
github.com/spf13/cobra v1.4.0
github.com/stretchr/testify v1.7.2
github.com/vektah/gqlparser/v2 v2.4.1
github.com/vektah/gqlparser/v2 v2.4.6
github.com/xanzy/go-gitlab v0.68.0
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
@ -35,3 +33,71 @@ require (
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a
golang.org/x/text v0.3.7
)
require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/RoaringBitmap/roaring v0.4.23 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/mmap-go v1.0.2 // indirect
github.com/blevesearch/segment v0.9.0 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/zap/v11 v11.0.14 // indirect
github.com/blevesearch/zap/v12 v12.0.14 // indirect
github.com/blevesearch/zap/v13 v13.0.6 // indirect
github.com/blevesearch/zap/v14 v14.0.5 // indirect
github.com/blevesearch/zap/v15 v15.0.3 // indirect
github.com/corpix/uarand v0.1.1 // indirect
github.com/couchbase/vellum v1.0.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.4.0 // indirect
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.3.1 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/philhofer/fwd v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.1.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/steveyen/gtreap v0.1.0 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/tinylib/msgp v1.1.0 // indirect
github.com/willf/bitset v1.1.10 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
go.etcd.io/bbolt v1.3.5 // indirect
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

51
go.sum
View File

@ -33,11 +33,12 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/gqlgen v0.17.1 h1:i2qQMPKHQjHgBWYIpO4TsaQpPqMHCPK1+h95ipvH8VU=
github.com/99designs/gqlgen v0.17.1/go.mod h1:K5fzLKwtph+FFgh9j7nFbRUdBKvTcGnsta51fsMTn3o=
github.com/99designs/gqlgen v0.17.13 h1:ETUEqvRg5Zvr1lXtpoRdj026fzVay0ZlJPwI33qXLIw=
github.com/99designs/gqlgen v0.17.13/go.mod h1:w1brbeOdqVyNJI553BGwtwdVcYu1LKeYE1opLWN9RgQ=
github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o=
github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
@ -54,8 +55,8 @@ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
@ -101,8 +102,8 @@ github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9L
github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY=
github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 h1:a1zrFsLFac2xoM6zG1u72DWJwZG3ayttYLfmLbxVETk=
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -118,9 +119,7 @@ github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiG
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw=
github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -236,8 +235,8 @@ github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKp
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@ -289,11 +288,10 @@ github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@ -303,8 +301,8 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.2.3 h1:f/MjBEBDLttYCGfRaKBbKSRVF5aV2O6fnBpzknuE3jU=
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA=
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
@ -331,9 +329,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
@ -342,7 +338,6 @@ github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7 h1:Vk3RiBQpF0Ja+
github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e h1:VAzdS5Nw68fbf5RZ8RDVlUvPXNU6Z3jtPCK/qvm4FoQ=
github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
@ -366,6 +361,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
@ -376,10 +372,9 @@ github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/vektah/gqlparser/v2 v2.4.1 h1:QOyEn8DAPMUMARGMeshKDkDgNmVoEaEGiDB0uWxcSlQ=
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
github.com/vektah/gqlparser/v2 v2.4.6 h1:Yjzp66g6oVq93Jihbi0qhGnf/6zIWjcm8H6gA27zstE=
github.com/vektah/gqlparser/v2 v2.4.6/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xanzy/go-gitlab v0.68.0 h1:b2iMQHgZ1V+NyRqLRJVv6RFfr4xnd/AASeS/PETYL0Y=
@ -387,6 +382,7 @@ github.com/xanzy/go-gitlab v0.68.0/go.mod h1:o4yExCtdaqlM8YGdDJWuZoBmfxBsmA9TPEj
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -407,6 +403,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -440,6 +437,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -470,7 +468,6 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@ -497,7 +494,6 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -532,6 +528,7 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE=
@ -592,9 +589,9 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -693,14 +690,12 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -87,6 +87,7 @@ func generateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug {
time.Now().Unix(),
fake.Sentence(),
paragraphs(),
nil, nil,
)
if err != nil {
@ -143,19 +144,19 @@ func paragraphs() string {
}
func comment(b bug.Interface, p identity.Interface, timestamp int64) {
_, _ = bug.AddComment(b, p, timestamp, paragraphs())
_, _ = bug.AddComment(b, p, timestamp, paragraphs(), nil, nil)
}
func title(b bug.Interface, p identity.Interface, timestamp int64) {
_, _ = bug.SetTitle(b, p, timestamp, fake.Sentence())
_, _ = bug.SetTitle(b, p, timestamp, fake.Sentence(), nil)
}
func open(b bug.Interface, p identity.Interface, timestamp int64) {
_, _ = bug.Open(b, p, timestamp)
_, _ = bug.Open(b, p, timestamp, nil)
}
func close(b bug.Interface, p identity.Interface, timestamp int64) {
_, _ = bug.Close(b, p, timestamp)
_, _ = bug.Close(b, p, timestamp, nil)
}
var addedLabels []string
@ -182,5 +183,5 @@ func labels(b bug.Interface, p identity.Interface, timestamp int64) {
// ignore error
// if the randomisation produce no changes, no op
// is added to the bug
_, _, _ = bug.ChangeLabels(b, p, timestamp, added, removed)
_, _, _ = bug.ChangeLabels(b, p, timestamp, added, removed, nil)
}

View File

@ -235,7 +235,7 @@ func (repo *GoGitRepo) Keyring() Keyring {
return repo.keyring
}
// GetUserName returns the name the the user has used to configure git
// GetUserName returns the name the user has used to configure git
func (repo *GoGitRepo) GetUserName() (string, error) {
return repo.AnyConfig().ReadString("user.name")
}

View File

@ -60,9 +60,9 @@ type RepoKeyring interface {
Keyring() Keyring
}
// RepoCommon represent the common function the we want all the repo to implement
// RepoCommon represent the common function we want all repos to implement
type RepoCommon interface {
// GetUserName returns the name the the user has used to configure git
// GetUserName returns the name the user has used to configure git
GetUserName() (string, error)
// GetUserEmail returns the email address that the user has used to configure git.

View File

@ -1,4 +1,4 @@
// +build debugwebui
//go:build debugwebui
package webui

View File

@ -1,4 +1,4 @@
// +build ignore
//go:build ignore
package main

View File

@ -1,6 +1,6 @@
// Code generated by vfsgen; DO NOT EDIT.
// +build !debugwebui
//go:build !debugwebui
package webui