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: strategy:
matrix: matrix:
go-version: [1.16.x] go-version: [1.18.x]
platform: [ubuntu-latest, macos-latest, windows-latest] platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
@ -46,7 +46,7 @@ jobs:
- name: Install Go - name: Install Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.16.x go-version: 1.18.x
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 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_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_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_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_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_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" //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" "fmt"
"github.com/MichaelMure/git-bug/api/graphql/models" "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. // 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 // OperationConMaker define a function that create a models.OperationConnection
type OperationConMaker func( type OperationConMaker func(
edges []*models.OperationEdge, edges []*models.OperationEdge,
nodes []bug.Operation, nodes []dag.Operation,
info *models.PageInfo, info *models.PageInfo,
totalCount int) (*models.OperationConnection, error) totalCount int) (*models.OperationConnection, error)
// OperationCon will paginate a source according to the input of a relay connection // 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) { func OperationCon(source []dag.Operation, edgeMaker OperationEdgeMaker, conMaker OperationConMaker, input models.ConnectionInput) (*models.OperationConnection, error) {
var nodes []bug.Operation var nodes []dag.Operation
var edges []*models.OperationEdge var edges []*models.OperationEdge
var cursors []string var cursors []string
var pageInfo = &models.PageInfo{} var pageInfo = &models.PageInfo{}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -7,6 +7,7 @@ import (
"github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity" "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. // BugWrapper is an interface used by the GraphQL resolvers to handle a bug.
@ -24,7 +25,7 @@ type BugWrapper interface {
Participants() ([]IdentityWrapper, error) Participants() ([]IdentityWrapper, error)
CreatedAt() time.Time CreatedAt() time.Time
Timeline() ([]bug.TimelineItem, error) Timeline() ([]bug.TimelineItem, error)
Operations() ([]bug.Operation, error) Operations() ([]dag.Operation, error)
IsAuthored() IsAuthored()
} }
@ -144,7 +145,7 @@ func (lb *lazyBug) Timeline() ([]bug.TimelineItem, error) {
return lb.snap.Timeline, nil return lb.snap.Timeline, nil
} }
func (lb *lazyBug) Operations() ([]bug.Operation, error) { func (lb *lazyBug) Operations() ([]dag.Operation, error) {
err := lb.load() err := lb.load()
if err != nil { if err != nil {
return nil, err return nil, err
@ -210,6 +211,6 @@ func (l *loadedBug) Timeline() ([]bug.TimelineItem, error) {
return l.Snapshot.Timeline, nil return l.Snapshot.Timeline, nil
} }
func (l *loadedBug) Operations() ([]bug.Operation, error) { func (l *loadedBug) Operations() ([]dag.Operation, error) {
return l.Snapshot.Operations, nil 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/graph"
"github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/api/graphql/models"
"github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/entity/dag"
) )
var _ graph.BugResolver = &bugResolver{} var _ graph.BugResolver = &bugResolver{}
@ -69,14 +70,14 @@ func (bugResolver) Operations(_ context.Context, obj models.BugWrapper, after *s
Last: last, Last: last,
} }
edger := func(op bug.Operation, offset int) connections.Edge { edger := func(op dag.Operation, offset int) connections.Edge {
return models.OperationEdge{ return models.OperationEdge{
Node: op, Node: op,
Cursor: connections.OffsetToCursor(offset), 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{ return &models.OperationConnection{
Edges: edges, Edges: edges,
Nodes: nodes, Nodes: nodes,

View File

@ -20,6 +20,7 @@ import (
"github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "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:] { for _, op := range snapshot.Operations[1:] {
// ignore SetMetadata operations // ignore SetMetadata operations
if _, ok := op.(*bug.SetMetadataOperation); ok { if _, ok := op.(dag.OperationDoesntChangeSnapshot); ok {
continue continue
} }

View File

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

View File

@ -13,6 +13,7 @@ import (
"github.com/MichaelMure/git-bug/bridge/core/auth" "github.com/MichaelMure/git-bug/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/interrupt" "github.com/MichaelMure/git-bug/util/interrupt"
@ -44,7 +45,7 @@ func TestGithubImporter(t *testing.T) {
name: "simple issue", name: "simple issue",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/1", url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/1",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "simple issue", "initial comment", nil), bug.NewCreateOp(author, 0, "simple issue", "initial comment", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil), bug.NewAddCommentOp(author, 0, "first comment", nil),
bug.NewAddCommentOp(author, 0, "second comment", nil), bug.NewAddCommentOp(author, 0, "second comment", nil),
@ -55,7 +56,7 @@ func TestGithubImporter(t *testing.T) {
name: "empty issue", name: "empty issue",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/2", url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/2",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "empty issue", "", nil), bug.NewCreateOp(author, 0, "empty issue", "", nil),
}, },
}, },
@ -64,7 +65,7 @@ func TestGithubImporter(t *testing.T) {
name: "complex issue", name: "complex issue",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/3", url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/3",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "complex issue", "initial comment", nil), bug.NewCreateOp(author, 0, "complex issue", "initial comment", nil),
bug.NewLabelChangeOperation(author, 0, []bug.Label{"bug"}, []bug.Label{}), bug.NewLabelChangeOperation(author, 0, []bug.Label{"bug"}, []bug.Label{}),
bug.NewLabelChangeOperation(author, 0, []bug.Label{"duplicate"}, []bug.Label{}), bug.NewLabelChangeOperation(author, 0, []bug.Label{"duplicate"}, []bug.Label{}),
@ -81,7 +82,7 @@ func TestGithubImporter(t *testing.T) {
name: "editions", name: "editions",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/4", url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/4",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "editions", "initial comment edited", nil), bug.NewCreateOp(author, 0, "editions", "initial comment edited", nil),
bug.NewEditCommentOp(author, 0, "", "erased then edited again", nil), bug.NewEditCommentOp(author, 0, "", "erased then edited again", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil), bug.NewAddCommentOp(author, 0, "first comment", nil),
@ -93,7 +94,7 @@ func TestGithubImporter(t *testing.T) {
name: "comment deletion", name: "comment deletion",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/5", url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/5",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "comment deletion", "", nil), bug.NewCreateOp(author, 0, "comment deletion", "", nil),
}, },
}, },
@ -102,7 +103,7 @@ func TestGithubImporter(t *testing.T) {
name: "edition deletion", name: "edition deletion",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/6", url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/6",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "edition deletion", "initial comment", nil), bug.NewCreateOp(author, 0, "edition deletion", "initial comment", nil),
bug.NewEditCommentOp(author, 0, "", "initial comment edited again", nil), bug.NewEditCommentOp(author, 0, "", "initial comment edited again", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil), bug.NewAddCommentOp(author, 0, "first comment", nil),
@ -114,7 +115,7 @@ func TestGithubImporter(t *testing.T) {
name: "hidden comment", name: "hidden comment",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/7", url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/7",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "hidden comment", "initial comment", nil), bug.NewCreateOp(author, 0, "hidden comment", "initial comment", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil), bug.NewAddCommentOp(author, 0, "first comment", nil),
}, },
@ -124,7 +125,7 @@ func TestGithubImporter(t *testing.T) {
name: "transfered issue", name: "transfered issue",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/8", url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/8",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "transfered issue", "", nil), bug.NewCreateOp(author, 0, "transfered issue", "", nil),
}, },
}, },
@ -133,7 +134,7 @@ func TestGithubImporter(t *testing.T) {
name: "unicode control characters", name: "unicode control characters",
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/10", url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/10",
bug: &bug.Snapshot{ 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), 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/bug"
"github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "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{}) labelSet := make(map[string]struct{})
for _, op := range snapshot.Operations[1:] { for _, op := range snapshot.Operations[1:] {
// ignore SetMetadata operations // ignore SetMetadata operations
if _, ok := op.(*bug.SetMetadataOperation); ok { if _, ok := op.(dag.OperationDoesntChangeSnapshot); ok {
continue continue
} }

View File

@ -11,11 +11,12 @@ import (
"github.com/xanzy/go-gitlab" "github.com/xanzy/go-gitlab"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/bridge/core/auth" "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/cache"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/interrupt" "github.com/MichaelMure/git-bug/util/interrupt"
@ -247,7 +248,7 @@ func TestGitlabPushPull(t *testing.T) {
// verify operation have correct metadata // verify operation have correct metadata
for _, op := range tt.bug.Snapshot().Operations { for _, op := range tt.bug.Snapshot().Operations {
// Check if the originals operations (*not* SetMetadata) are tagged properly // 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) _, haveIDMetadata := op.GetMetadata(metaKeyGitlabId)
require.True(t, haveIDMetadata) require.True(t, haveIDMetadata)
@ -272,7 +273,7 @@ func TestGitlabPushPull(t *testing.T) {
require.True(t, ok) require.True(t, ok)
require.Equal(t, issueOrigin, target) 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/bridge/core/auth"
"github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/interrupt" "github.com/MichaelMure/git-bug/util/interrupt"
@ -49,7 +50,7 @@ func TestGitlabImport(t *testing.T) {
name: "simple issue", name: "simple issue",
url: "https://gitlab.com/git-bug/test/-/issues/1", url: "https://gitlab.com/git-bug/test/-/issues/1",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "simple issue", "initial comment", nil), bug.NewCreateOp(author, 0, "simple issue", "initial comment", nil),
bug.NewAddCommentOp(author, 0, "first comment", nil), bug.NewAddCommentOp(author, 0, "first comment", nil),
bug.NewAddCommentOp(author, 0, "second comment", nil), bug.NewAddCommentOp(author, 0, "second comment", nil),
@ -60,7 +61,7 @@ func TestGitlabImport(t *testing.T) {
name: "empty issue", name: "empty issue",
url: "https://gitlab.com/git-bug/test/-/issues/2", url: "https://gitlab.com/git-bug/test/-/issues/2",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "empty issue", "", nil), bug.NewCreateOp(author, 0, "empty issue", "", nil),
}, },
}, },
@ -69,7 +70,7 @@ func TestGitlabImport(t *testing.T) {
name: "complex issue", name: "complex issue",
url: "https://gitlab.com/git-bug/test/-/issues/3", url: "https://gitlab.com/git-bug/test/-/issues/3",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "complex issue", "initial comment", nil), 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.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"), bug.NewSetTitleOp(author, 0, "complex issue edited", "complex issue"),
@ -86,7 +87,7 @@ func TestGitlabImport(t *testing.T) {
name: "editions", name: "editions",
url: "https://gitlab.com/git-bug/test/-/issues/4", url: "https://gitlab.com/git-bug/test/-/issues/4",
bug: &bug.Snapshot{ bug: &bug.Snapshot{
Operations: []bug.Operation{ Operations: []dag.Operation{
bug.NewCreateOp(author, 0, "editions", "initial comment edited", nil), bug.NewCreateOp(author, 0, "editions", "initial comment edited", nil),
bug.NewAddCommentOp(author, 0, "first 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/bug"
"github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "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:] { for _, op := range snapshot.Operations[1:] {
// ignore SetMetadata operations // ignore SetMetadata operations
if _, ok := op.(*bug.SetMetadataOperation); ok { if _, ok := op.(dag.OperationDoesntChangeSnapshot); ok {
continue continue
} }

View File

@ -14,6 +14,7 @@ import (
"github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/util/text" "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 // Create a bug.Operation (or a series of operations) from a JIRA changelog
// entry // 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 // 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 // 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 // Read will read a bug from a repository
func Read(repo repository.ClockedRepo, id entity.Id) (*Bug, error) { func Read(repo repository.ClockedRepo, id entity.Id) (*Bug, error) {
e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id) return ReadWithResolver(repo, identity.NewSimpleResolver(repo), id)
if err != nil {
return nil, err
}
return &Bug{Entity: e}, nil
} }
// ReadWithResolver will read a bug from its Id, with a custom identity.Resolver // 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 // Compile a bug in a easily usable snapshot
func (bug *Bug) Compile() Snapshot { func (bug *Bug) Compile() *Snapshot {
snap := Snapshot{ snap := &Snapshot{
id: bug.Id(), id: bug.Id(),
Status: OpenStatus, Status: OpenStatus,
} }
for _, op := range bug.Operations() { for _, op := range bug.Operations() {
op.Apply(&snap) op.Apply(snap)
snap.Operations = append(snap.Operations, op) snap.Operations = append(snap.Operations, op)
} }
return snap 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 // For a valid Bug, this operation should be a CreateOp
func (bug *Bug) FirstOp() Operation { func (bug *Bug) FirstOp() Operation {
if fo := bug.Entity.FirstOp(); fo != nil { if fo := bug.Entity.FirstOp(); fo != nil {
@ -167,7 +163,7 @@ func (bug *Bug) FirstOp() Operation {
return nil 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 // For a valid Bug, should never be nil
func (bug *Bug) LastOp() Operation { func (bug *Bug) LastOp() Operation {
if lo := bug.Entity.LastOp(); lo != nil { 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") 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() {} func (c Comment) IsAuthored() {}

View File

@ -7,34 +7,34 @@ import (
) )
type Interface interface { type Interface interface {
// Id return the Bug identifier // Id returns the Bug identifier
Id() entity.Id Id() entity.Id
// Validate check if the Bug data is valid // Validate checks if the Bug data is valid
Validate() error Validate() error
// Append an operation into the staging area, to be committed later // Append an operation into the staging area, to be committed later
Append(op Operation) Append(op Operation)
// Operations return the ordered operations // Operations returns the ordered operations
Operations() []Operation 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 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 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 // For a valid Bug, this operation should be a CreateOp
FirstOp() Operation 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 // For a valid Bug, should never be nil
LastOp() Operation LastOp() Operation
// Compile a bug in a easily usable snapshot // Compile a bug in an easily usable snapshot
Compile() Snapshot Compile() *Snapshot
// CreateLamportTime return the Lamport time of creation // CreateLamportTime return the Lamport time of creation
CreateLamportTime() lamport.Time CreateLamportTime() lamport.Time
@ -42,14 +42,3 @@ type Interface interface {
// EditLamportTime return the Lamport time of the last edit // EditLamportTime return the Lamport time of the last edit
EditLamportTime() lamport.Time 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 package bug
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
@ -17,24 +16,24 @@ var _ dag.OperationWithFiles = &AddCommentOperation{}
// AddCommentOperation will add a new comment in the bug // AddCommentOperation will add a new comment in the bug
type AddCommentOperation struct { type AddCommentOperation struct {
OpBase dag.OpBase
Message string `json:"message"` Message string `json:"message"`
// TODO: change for a map[string]util.hash to store the filename ? // TODO: change for a map[string]util.hash to store the filename ?
Files []repository.Hash `json:"files"` Files []repository.Hash `json:"files"`
} }
func (op *AddCommentOperation) Id() entity.Id { func (op *AddCommentOperation) Id() entity.Id {
return idOperation(op, &op.OpBase) return dag.IdOperation(op, &op.OpBase)
} }
func (op *AddCommentOperation) Apply(snapshot *Snapshot) { func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
snapshot.addActor(op.Author_) snapshot.addActor(op.Author())
snapshot.addParticipant(op.Author_) snapshot.addParticipant(op.Author())
comment := Comment{ comment := Comment{
id: entity.CombineIds(snapshot.Id(), op.Id()), id: entity.CombineIds(snapshot.Id(), op.Id()),
Message: op.Message, Message: op.Message,
Author: op.Author_, Author: op.Author(),
Files: op.Files, Files: op.Files,
UnixTime: timestamp.Timestamp(op.UnixTime), UnixTime: timestamp.Timestamp(op.UnixTime),
} }
@ -64,64 +63,31 @@ func (op *AddCommentOperation) Validate() error {
return nil 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 { func NewAddCommentOp(author identity.Interface, unixTime int64, message string, files []repository.Hash) *AddCommentOperation {
return &AddCommentOperation{ return &AddCommentOperation{
OpBase: newOpBase(AddCommentOp, author, unixTime), OpBase: dag.NewOpBase(AddCommentOp, author, unixTime),
Message: message, Message: message,
Files: files, 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 { type AddCommentTimelineItem struct {
CommentTimelineItem CommentTimelineItem
} }
// Sign post method for gqlgen // IsAuthored is a sign post method for gqlgen
func (a *AddCommentTimelineItem) IsAuthored() {} func (a *AddCommentTimelineItem) IsAuthored() {}
// Convenience function to apply the operation // AddComment is a convenience function to add a comment to a bug
func AddComment(b Interface, author identity.Interface, unixTime int64, message string) (*AddCommentOperation, error) { func AddComment(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*AddCommentOperation, error) {
return AddCommentWithFiles(b, author, unixTime, message, nil) op := NewAddCommentOp(author, unixTime, message, files)
} for key, val := range metadata {
op.SetMetadata(key, val)
func AddCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash) (*AddCommentOperation, error) { }
addCommentOp := NewAddCommentOp(author, unixTime, message, files) if err := op.Validate(); err != nil {
if err := addCommentOp.Validate(); err != nil {
return nil, err return nil, err
} }
b.Append(addCommentOp) b.Append(op)
return addCommentOp, nil return op, nil
} }

View File

@ -1,37 +1,18 @@
package bug package bug
import ( import (
"encoding/json"
"testing" "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/identity"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
) )
func TestAddCommentSerialize(t *testing.T) { func TestAddCommentSerialize(t *testing.T) {
repo := repository.NewMockRepo() dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
return NewAddCommentOp(author, unixTime, "message", nil)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") })
require.NoError(t, err) dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
return NewAddCommentOp(author, unixTime, "message", []repository.Hash{"hash1", "hash2"})
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)
} }

View File

@ -1,7 +1,6 @@
package bug package bug
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
@ -17,53 +16,38 @@ var _ dag.OperationWithFiles = &CreateOperation{}
// CreateOperation define the initial creation of a bug // CreateOperation define the initial creation of a bug
type CreateOperation struct { type CreateOperation struct {
OpBase dag.OpBase
Title string `json:"title"` Title string `json:"title"`
Message string `json:"message"` Message string `json:"message"`
Files []repository.Hash `json:"files"` Files []repository.Hash `json:"files"`
} }
func (op *CreateOperation) Id() entity.Id { func (op *CreateOperation) Id() entity.Id {
return idOperation(op, &op.OpBase) return dag.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)
} }
func (op *CreateOperation) Apply(snapshot *Snapshot) { func (op *CreateOperation) Apply(snapshot *Snapshot) {
// sanity check: will fail when adding a second Create // sanity check: will fail when adding a second Create
if snapshot.id != "" && snapshot.id != entity.UnsetId && snapshot.id != op.Id() { if snapshot.id != "" && snapshot.id != entity.UnsetId && snapshot.id != op.Id() {
panic("adding a second Create operation") return
} }
snapshot.id = op.Id() snapshot.id = op.Id()
snapshot.addActor(op.Author_) snapshot.addActor(op.Author())
snapshot.addParticipant(op.Author_) snapshot.addParticipant(op.Author())
snapshot.Title = op.Title snapshot.Title = op.Title
comment := Comment{ comment := Comment{
id: entity.CombineIds(snapshot.Id(), op.Id()), id: entity.CombineIds(snapshot.Id(), op.Id()),
Message: op.Message, Message: op.Message,
Author: op.Author_, Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime), UnixTime: timestamp.Timestamp(op.UnixTime),
} }
snapshot.Comments = []Comment{comment} snapshot.Comments = []Comment{comment}
snapshot.Author = op.Author_ snapshot.Author = op.Author()
snapshot.CreateTime = op.Time() snapshot.CreateTime = op.Time()
snapshot.Timeline = []TimelineItem{ snapshot.Timeline = []TimelineItem{
@ -82,13 +66,6 @@ func (op *CreateOperation) Validate() error {
return err 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) { if text.Empty(op.Title) {
return fmt.Errorf("title is empty") return fmt.Errorf("title is empty")
} }
@ -103,45 +80,9 @@ func (op *CreateOperation) Validate() error {
return nil 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 { func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation {
return &CreateOperation{ return &CreateOperation{
OpBase: newOpBase(CreateOp, author, unixTime), OpBase: dag.NewOpBase(CreateOp, author, unixTime),
Title: title, Title: title,
Message: message, Message: message,
Files: files, Files: files,
@ -153,23 +94,19 @@ type CreateTimelineItem struct {
CommentTimelineItem CommentTimelineItem
} }
// Sign post method for gqlgen // IsAuthored is a sign post method for gqlgen
func (c *CreateTimelineItem) IsAuthored() {} func (c *CreateTimelineItem) IsAuthored() {}
// Convenience function to apply the operation // Create is a convenience function to create a bug
func Create(author identity.Interface, unixTime int64, title, message string) (*Bug, *CreateOperation, error) { func Create(author identity.Interface, unixTime int64, title, message string, files []repository.Hash, metadata map[string]string) (*Bug, *CreateOperation, error) {
return CreateWithFiles(author, unixTime, title, message, nil) b := NewBug()
} op := NewCreateOp(author, unixTime, title, message, files)
for key, val := range metadata {
func CreateWithFiles(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) (*Bug, *CreateOperation, error) { op.SetMetadata(key, val)
newBug := NewBug()
createOp := NewCreateOp(author, unixTime, title, message, files)
if err := createOp.Validate(); err != nil {
return nil, createOp, err
} }
if err := op.Validate(); err != nil {
newBug.Append(createOp) return nil, op, err
}
return newBug, createOp, nil b.Append(op)
return b, op, nil
} }

View File

@ -1,13 +1,13 @@
package bug package bug
import ( import (
"encoding/json"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/timestamp" "github.com/MichaelMure/git-bug/util/timestamp"
@ -58,26 +58,10 @@ func TestCreate(t *testing.T) {
} }
func TestCreateSerialize(t *testing.T) { func TestCreateSerialize(t *testing.T) {
repo := repository.NewMockRepo() dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
return NewCreateOp(author, unixTime, "title", "message", nil)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") })
require.NoError(t, err) dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
return NewCreateOp(author, unixTime, "title", "message", []repository.Hash{"hash1", "hash2"})
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)
} }

View File

@ -1,7 +1,6 @@
package bug package bug
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -20,14 +19,14 @@ var _ dag.OperationWithFiles = &EditCommentOperation{}
// EditCommentOperation will change a comment in the bug // EditCommentOperation will change a comment in the bug
type EditCommentOperation struct { type EditCommentOperation struct {
OpBase dag.OpBase
Target entity.Id `json:"target"` Target entity.Id `json:"target"`
Message string `json:"message"` Message string `json:"message"`
Files []repository.Hash `json:"files"` Files []repository.Hash `json:"files"`
} }
func (op *EditCommentOperation) Id() entity.Id { func (op *EditCommentOperation) Id() entity.Id {
return idOperation(op, &op.OpBase) return dag.IdOperation(op, &op.OpBase)
} }
func (op *EditCommentOperation) Apply(snapshot *Snapshot) { func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
@ -68,7 +67,7 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
return return
} }
snapshot.addActor(op.Author_) snapshot.addActor(op.Author())
// Updating the corresponding comment // Updating the corresponding comment
@ -101,43 +100,9 @@ func (op *EditCommentOperation) Validate() error {
return nil 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 { func NewEditCommentOp(author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash) *EditCommentOperation {
return &EditCommentOperation{ return &EditCommentOperation{
OpBase: newOpBase(EditCommentOp, author, unixTime), OpBase: dag.NewOpBase(EditCommentOp, author, unixTime),
Target: target, Target: target,
Message: message, Message: message,
Files: files, 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 // 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) { func EditComment(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash, metadata map[string]string) (*EditCommentOperation, error) {
return EditCommentWithFiles(b, author, unixTime, target, message, nil) op := NewEditCommentOp(author, unixTime, target, message, files)
} for key, val := range metadata {
op.SetMetadata(key, val)
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 := op.Validate(); err != nil {
if err := editCommentOp.Validate(); err != nil {
return nil, err return nil, err
} }
b.Append(editCommentOp) b.Append(op)
return editCommentOp, nil return op, nil
} }
// EditCreateComment is a convenience function to edit the body of a bug (the first comment) // 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) createOp := b.FirstOp().(*CreateOperation)
return EditComment(b, author, unixTime, createOp.Id(), message) return EditComment(b, author, unixTime, createOp.Id(), message, files, metadata)
}
// 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)
} }

View File

@ -1,12 +1,12 @@
package bug package bug
import ( import (
"encoding/json"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
) )
@ -75,26 +75,10 @@ func TestEdit(t *testing.T) {
} }
func TestEditCommentSerialize(t *testing.T) { func TestEditCommentSerialize(t *testing.T) {
repo := repository.NewMockRepo() dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
return NewEditCommentOp(author, unixTime, "target", "message", nil)
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") })
require.NoError(t, err) dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
return NewEditCommentOp(author, unixTime, "target", "message", []repository.Hash{"hash1", "hash2"})
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)
} }

View File

@ -1,13 +1,13 @@
package bug package bug
import ( import (
"encoding/json"
"fmt" "fmt"
"sort" "sort"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/timestamp" "github.com/MichaelMure/git-bug/util/timestamp"
) )
@ -16,18 +16,18 @@ var _ Operation = &LabelChangeOperation{}
// LabelChangeOperation define a Bug operation to add or remove labels // LabelChangeOperation define a Bug operation to add or remove labels
type LabelChangeOperation struct { type LabelChangeOperation struct {
OpBase dag.OpBase
Added []Label `json:"added"` Added []Label `json:"added"`
Removed []Label `json:"removed"` Removed []Label `json:"removed"`
} }
func (op *LabelChangeOperation) Id() entity.Id { 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) { func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
snapshot.addActor(op.Author_) snapshot.addActor(op.Author())
// Add in the set // Add in the set
AddLoop: AddLoop:
@ -59,7 +59,7 @@ AddLoop:
item := &LabelChangeTimelineItem{ item := &LabelChangeTimelineItem{
id: op.Id(), id: op.Id(),
Author: op.Author_, Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime), UnixTime: timestamp.Timestamp(op.UnixTime),
Added: op.Added, Added: op.Added,
Removed: op.Removed, Removed: op.Removed,
@ -92,41 +92,9 @@ func (op *LabelChangeOperation) Validate() error {
return nil 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 { func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
return &LabelChangeOperation{ return &LabelChangeOperation{
OpBase: newOpBase(LabelChangeOp, author, unixTime), OpBase: dag.NewOpBase(LabelChangeOp, author, unixTime),
Added: added, Added: added,
Removed: removed, Removed: removed,
} }
@ -144,11 +112,11 @@ func (l LabelChangeTimelineItem) Id() entity.Id {
return l.id return l.id
} }
// Sign post method for gqlgen // IsAuthored is a sign post method for gqlgen
func (l *LabelChangeTimelineItem) IsAuthored() {} func (l LabelChangeTimelineItem) IsAuthored() {}
// ChangeLabels is a convenience function to apply the operation // ChangeLabels is a convenience function to change labels on a bug
func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) { func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) ([]LabelChangeResult, *LabelChangeOperation, error) {
var added, removed []Label var added, removed []Label
var results []LabelChangeResult 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") return results, nil, fmt.Errorf("no label added or removed")
} }
labelOp := NewLabelChangeOperation(author, unixTime, added, removed) op := NewLabelChangeOperation(author, unixTime, added, removed)
for key, val := range metadata {
if err := labelOp.Validate(); err != nil { op.SetMetadata(key, val)
}
if err := op.Validate(); err != nil {
return nil, nil, err 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 // ForceChangeLabels is a convenience function to apply the operation
// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely // 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, // 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. // 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)) added := make([]Label, len(add))
for i, str := range add { for i, str := range add {
added[i] = Label(str) added[i] = Label(str)
@ -223,15 +193,18 @@ func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, a
removed[i] = Label(str) 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 return nil, err
} }
b.Append(labelOp) b.Append(op)
return labelOp, nil return op, nil
} }
func labelExist(labels []Label, label Label) bool { func labelExist(labels []Label, label Label) bool {

View File

@ -1,37 +1,20 @@
package bug package bug
import ( import (
"encoding/json"
"testing" "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/identity"
"github.com/MichaelMure/git-bug/repository"
) )
func TestLabelChangeSerialize(t *testing.T) { func TestLabelChangeSerialize(t *testing.T) {
repo := repository.NewMockRepo() dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
return NewLabelChangeOperation(author, unixTime, []Label{"added"}, []Label{"removed"})
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") })
require.NoError(t, err) dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
return NewLabelChangeOperation(author, unixTime, []Label{"added"}, nil)
unix := time.Now().Unix() })
before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"}) dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
return NewLabelChangeOperation(author, unixTime, nil, []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)
} }

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 package bug
import ( import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/text"
) )
var _ Operation = &SetMetadataOperation{} 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)
type SetMetadataOperation struct {
OpBase
Target entity.Id `json:"target"`
NewMetadata map[string]string `json:"new_metadata"`
} }
func (op *SetMetadataOperation) Id() entity.Id { // SetMetadata is a convenience function to add metadata on another operation
return idOperation(op, &op.OpBase) 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 {
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 {
return nil, err return nil, err
} }
b.Append(SetMetadataOp) b.Append(op)
return SetMetadataOp, nil 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 package bug
import ( import (
"encoding/json"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/timestamp" "github.com/MichaelMure/git-bug/util/timestamp"
) )
@ -14,21 +13,21 @@ var _ Operation = &SetStatusOperation{}
// SetStatusOperation will change the status of a bug // SetStatusOperation will change the status of a bug
type SetStatusOperation struct { type SetStatusOperation struct {
OpBase dag.OpBase
Status Status `json:"status"` Status Status `json:"status"`
} }
func (op *SetStatusOperation) Id() entity.Id { func (op *SetStatusOperation) Id() entity.Id {
return idOperation(op, &op.OpBase) return dag.IdOperation(op, &op.OpBase)
} }
func (op *SetStatusOperation) Apply(snapshot *Snapshot) { func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
snapshot.Status = op.Status snapshot.Status = op.Status
snapshot.addActor(op.Author_) snapshot.addActor(op.Author())
item := &SetStatusTimelineItem{ item := &SetStatusTimelineItem{
id: op.Id(), id: op.Id(),
Author: op.Author_, Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime), UnixTime: timestamp.Timestamp(op.UnixTime),
Status: op.Status, Status: op.Status,
} }
@ -48,39 +47,9 @@ func (op *SetStatusOperation) Validate() error {
return nil 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 { func NewSetStatusOp(author identity.Interface, unixTime int64, status Status) *SetStatusOperation {
return &SetStatusOperation{ return &SetStatusOperation{
OpBase: newOpBase(SetStatusOp, author, unixTime), OpBase: dag.NewOpBase(SetStatusOp, author, unixTime),
Status: status, Status: status,
} }
} }
@ -96,12 +65,15 @@ func (s SetStatusTimelineItem) Id() entity.Id {
return s.id return s.id
} }
// Sign post method for gqlgen // IsAuthored is a sign post method for gqlgen
func (s *SetStatusTimelineItem) IsAuthored() {} func (s SetStatusTimelineItem) IsAuthored() {}
// Convenience function to apply the operation // Open is a convenience function to change a bugs state to Open
func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) { func Open(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) {
op := NewSetStatusOp(author, unixTime, OpenStatus) op := NewSetStatusOp(author, unixTime, OpenStatus)
for key, value := range metadata {
op.SetMetadata(key, value)
}
if err := op.Validate(); err != nil { if err := op.Validate(); err != nil {
return nil, err return nil, err
} }
@ -109,9 +81,12 @@ func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOpe
return op, nil return op, nil
} }
// Convenience function to apply the operation // Close is a convenience function to change a bugs state to Close
func Close(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) { func Close(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) {
op := NewSetStatusOp(author, unixTime, ClosedStatus) op := NewSetStatusOp(author, unixTime, ClosedStatus)
for key, value := range metadata {
op.SetMetadata(key, value)
}
if err := op.Validate(); err != nil { if err := op.Validate(); err != nil {
return nil, err return nil, err
} }

View File

@ -1,37 +1,14 @@
package bug package bug
import ( import (
"encoding/json"
"testing" "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/identity"
"github.com/MichaelMure/git-bug/repository"
) )
func TestSetStatusSerialize(t *testing.T) { func TestSetStatusSerialize(t *testing.T) {
repo := repository.NewMockRepo() dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetStatusOperation {
return NewSetStatusOp(author, unixTime, ClosedStatus)
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)
} }

View File

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

View File

@ -1,37 +1,14 @@
package bug package bug
import ( import (
"encoding/json"
"testing" "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/identity"
"github.com/MichaelMure/git-bug/repository"
) )
func TestSetTitleSerialize(t *testing.T) { func TestSetTitleSerialize(t *testing.T) {
repo := repository.NewMockRepo() dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetTitleOperation {
return NewSetTitleOp(author, unixTime, "title", "was")
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)
} }

View File

@ -1,23 +1,15 @@
package bug package bug
import ( import (
"crypto/rand"
"encoding/json" "encoding/json"
"fmt" "fmt"
"time"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
) )
// OperationType is an operation type identifier
type OperationType int
const ( const (
_ OperationType = iota _ dag.OperationType = iota
CreateOp CreateOp
SetTitleOp SetTitleOp
AddCommentOp AddCommentOp
@ -32,55 +24,24 @@ const (
type Operation interface { type Operation interface {
dag.Operation 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 the operation to a Snapshot to create the final state
Apply(snapshot *Snapshot) 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 { // make sure that package external operations do conform to our interface
if base.id == "" { var _ Operation = &dag.NoOpOperation[*Snapshot]{}
// something went really wrong var _ Operation = &dag.SetMetadataOperation[*Snapshot]{}
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) func operationUnmarshaller(raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
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) {
var t struct { var t struct {
OperationType OperationType `json:"type"` OperationType dag.OperationType `json:"type"`
} }
if err := json.Unmarshal(raw, &t); err != nil { if err := json.Unmarshal(raw, &t); err != nil {
return nil, err return nil, err
} }
var op Operation var op dag.Operation
switch t.OperationType { switch t.OperationType {
case AddCommentOp: case AddCommentOp:
@ -92,9 +53,9 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resol
case LabelChangeOp: case LabelChangeOp:
op = &LabelChangeOperation{} op = &LabelChangeOperation{}
case NoOpOp: case NoOpOp:
op = &NoOpOperation{} op = &dag.NoOpOperation[*Snapshot]{}
case SetMetadataOp: case SetMetadataOp:
op = &SetMetadataOperation{} op = &dag.SetMetadataOperation[*Snapshot]{}
case SetStatusOp: case SetStatusOp:
op = &SetStatusOperation{} op = &SetStatusOperation{}
case SetTitleOp: case SetTitleOp:
@ -108,188 +69,5 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resol
return nil, err 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 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/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
) )
// TODO: move to entity/dag?
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
repo := repository.NewMockRepoClock() 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é Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus),
NewSetStatusOp(makeIdentity(t, "René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus), NewSetStatusOp(makeIdentity(t, "René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus),
NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus), NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus),
&CreateOperation{OpBase: OpBase{ &CreateOperation{OpBase: dag.NewOpBase(CreateOp, rene, 0),
Author_: rene,
UnixTime: 0,
OperationType: CreateOp,
},
Title: "title", Title: "title",
Message: "message", Message: "message",
}, },
@ -105,7 +104,7 @@ func TestID(t *testing.T) {
err = rene.Commit(repo) err = rene.Commit(repo)
require.NoError(t, err) 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) require.NoError(t, err)
id1 := op.Id() id1 := op.Id()

View File

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

View File

@ -1,6 +1,8 @@
package bug package bug
import "github.com/MichaelMure/git-bug/repository" import (
"github.com/MichaelMure/git-bug/repository"
)
var _ Interface = &WithSnapshot{} var _ Interface = &WithSnapshot{}
@ -10,11 +12,10 @@ type WithSnapshot struct {
snap *Snapshot snap *Snapshot
} }
// Snapshot return the current snapshot func (b *WithSnapshot) Compile() *Snapshot {
func (b *WithSnapshot) Snapshot() *Snapshot {
if b.snap == nil { if b.snap == nil {
snap := b.Bug.Compile() snap := b.Bug.Compile()
b.snap = &snap b.snap = snap
} }
return b.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/bug"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
) )
@ -33,7 +34,7 @@ func NewBugCache(repoCache *RepoCache, b *bug.Bug) *BugCache {
func (c *BugCache) Snapshot() *bug.Snapshot { func (c *BugCache) Snapshot() *bug.Snapshot {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
return c.bug.Snapshot() return c.bug.Compile()
} }
func (c *BugCache) Id() entity.Id { 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) { func (c *BugCache) AddCommentRaw(author *IdentityCache, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*bug.AddCommentOperation, error) {
c.mu.Lock() 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 { if err != nil {
c.mu.Unlock()
return nil, err return nil, err
} }
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated() 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) { func (c *BugCache) ChangeLabelsRaw(author *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) {
c.mu.Lock() 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 { if err != nil {
c.mu.Unlock()
return changes, nil, err return changes, nil, err
} }
return changes, op, c.notifyUpdated()
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
} }
func (c *BugCache) ForceChangeLabels(added []string, removed []string) (*bug.LabelChangeOperation, error) { 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) { func (c *BugCache) ForceChangeLabelsRaw(author *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) (*bug.LabelChangeOperation, error) {
c.mu.Lock() c.mu.Lock()
op, err := bug.ForceChangeLabels(c.bug, author.Identity, unixTime, added, removed) op, err := bug.ForceChangeLabels(c.bug, author.Identity, unixTime, added, removed, metadata)
if err != nil {
c.mu.Unlock()
return nil, err
}
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock() c.mu.Unlock()
err = c.notifyUpdated()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return op, c.notifyUpdated()
return op, nil
} }
func (c *BugCache) Open() (*bug.SetStatusOperation, error) { 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) { func (c *BugCache) OpenRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) {
c.mu.Lock() 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 { if err != nil {
c.mu.Unlock()
return nil, err return nil, err
} }
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated() 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) { func (c *BugCache) CloseRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) {
c.mu.Lock() 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 { if err != nil {
c.mu.Unlock()
return nil, err return nil, err
} }
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated() 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) { func (c *BugCache) SetTitleRaw(author *IdentityCache, unixTime int64, title string, metadata map[string]string) (*bug.SetTitleOperation, error) {
c.mu.Lock() 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 { if err != nil {
c.mu.Unlock()
return nil, err return nil, err
} }
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated() 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) // 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) { func (c *BugCache) EditCreateCommentRaw(author *IdentityCache, unixTime int64, body string, metadata map[string]string) (*bug.EditCommentOperation, error) {
c.mu.Lock() 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 { if err != nil {
c.mu.Unlock()
return nil, err return nil, err
} }
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated() 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) { func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target entity.Id, message string, metadata map[string]string) (*bug.EditCommentOperation, error) {
c.mu.Lock() 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 { if err != nil {
c.mu.Unlock()
return nil, err return nil, err
} }
for key, value := range metadata {
op.SetMetadata(key, value)
}
c.mu.Unlock()
return op, c.notifyUpdated() 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() author, err := c.repoCache.GetUserIdentity()
if err != nil { if err != nil {
return nil, err 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) 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() c.mu.Lock()
op, err := bug.SetMetadata(c.bug, author.Identity, unixTime, target, newMetadata) op, err := bug.SetMetadata(c.bug, author.Identity, unixTime, target, newMetadata)
c.mu.Unlock()
if err != nil { if err != nil {
c.mu.Unlock()
return nil, err return nil, err
} }
c.mu.Unlock()
return op, c.notifyUpdated() 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() 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 return err
} }
} }
@ -222,7 +222,7 @@ func (c *RepoCache) buildCache() error {
} }
// repoIsAvailable check is the given repository is locked by a Cache. // 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. // corresponding process is not there anymore.
// If no error is returned, the repo is free to edit. // If no error is returned, the repo is free to edit.
func repoIsAvailable(repo repository.RepoStorage) error { 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. // well as metadata for the Create operation.
// The new bug is written in the repository (commit) // 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) { 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 { if err != nil {
return nil, nil, err return nil, nil, err
} }
for key, value := range metadata {
op.SetMetadata(key, value)
}
err = b.Commit(c.repo) err = b.Commit(c.repo)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -36,7 +36,7 @@ func (c *RepoCache) Keyring() repository.Keyring {
return c.repo.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) { func (c *RepoCache) GetUserName() (string, error) {
return c.repo.GetUserName() return c.repo.GetUserName()
} }
@ -131,7 +131,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult {
b := result.Entity.(*bug.Bug) b := result.Entity.(*bug.Bug)
snap := b.Compile() snap := b.Compile()
c.muBug.Lock() c.muBug.Lock()
c.bugExcerpts[result.Id] = NewBugExcerpt(b, &snap) c.bugExcerpts[result.Id] = NewBugExcerpt(b, snap)
c.muBug.Unlock() c.muBug.Unlock()
} }
} }

View File

@ -18,78 +18,73 @@ import (
Operations Operations
*/ */
type op1 struct { const (
author identity.Interface _ OperationType = iota
Op1
Op2
)
OperationType int `json:"type"` type op1 struct {
Field1 string `json:"field_1"` OpBase
Files []repository.Hash `json:"files"` Field1 string `json:"field_1"`
Files []repository.Hash `json:"files"`
} }
func newOp1(author identity.Interface, field1 string, files ...repository.Hash) *op1 { 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 { func (op *op1) Id() entity.Id {
data, _ := json.Marshal(o) return IdOperation(op, &op.OpBase)
return entity.DeriveId(data)
} }
func (o *op1) Validate() error { return nil } func (op *op1) Validate() error { return nil }
func (o *op1) Author() identity.Interface { func (op *op1) GetFiles() []repository.Hash {
return o.author return op.Files
}
func (o *op1) GetFiles() []repository.Hash {
return o.Files
} }
type op2 struct { type op2 struct {
author identity.Interface OpBase
Field2 string `json:"field_2"`
OperationType int `json:"type"`
Field2 string `json:"field_2"`
} }
func newOp2(author identity.Interface, field2 string) *op2 { 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 { func (op *op2) Id() entity.Id {
data, _ := json.Marshal(o) return IdOperation(op, &op.OpBase)
return entity.DeriveId(data)
} }
func (o *op2) Validate() error { return nil } func (op *op2) Validate() error { return nil }
func (o *op2) Author() identity.Interface { func unmarshaler(raw json.RawMessage, resolver identity.Resolver) (Operation, error) {
return o.author
}
func unmarshaler(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (Operation, error) {
var t struct { var t struct {
OperationType int `json:"type"` OperationType OperationType `json:"type"`
} }
if err := json.Unmarshal(raw, &t); err != nil { if err := json.Unmarshal(raw, &t); err != nil {
return nil, err return nil, err
} }
var op Operation
switch t.OperationType { switch t.OperationType {
case 1: case Op1:
op := &op1{} op = &op1{}
err := json.Unmarshal(raw, &op) case Op2:
op.author = author op = &op2{}
return op, err
case 2:
op := &op2{}
err := json.Unmarshal(raw, &op)
op.author = author
return op, err
default: default:
return nil, fmt.Errorf("unknown operation type %v", t.OperationType) 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, ...) // the Namespace in git references (bugs, prs, ...)
Namespace string Namespace string
// a function decoding a JSON message into an Operation // 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 // the expected format version number, that can be used for data migration/upgrade
FormatVersion uint FormatVersion uint
} }

View File

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

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"time"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/entity/dag"
@ -64,10 +65,8 @@ type Operation interface {
Apply(snapshot *Snapshot) Apply(snapshot *Snapshot)
} }
type OperationType int
const ( const (
_ OperationType = iota _ dag.OperationType = iota
SetSignatureRequiredOp SetSignatureRequiredOp
AddAdministratorOp AddAdministratorOp
RemoveAdministratorOp RemoveAdministratorOp
@ -75,37 +74,30 @@ const (
// SetSignatureRequired is an operation to set/unset if git signature are required. // SetSignatureRequired is an operation to set/unset if git signature are required.
type SetSignatureRequired struct { type SetSignatureRequired struct {
author identity.Interface dag.OpBase
OperationType OperationType `json:"type"` Value bool `json:"value"`
Value bool `json:"value"`
} }
func NewSetSignatureRequired(author identity.Interface, value bool) *SetSignatureRequired { 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 { func (ssr *SetSignatureRequired) Id() entity.Id {
// the Id of the operation is the hash of the serialized data. // the Id of the operation is the hash of the serialized data.
// we could memorize the Id when deserializing, but that will do return dag.IdOperation(ssr, &ssr.OpBase)
data, _ := json.Marshal(ssr)
return entity.DeriveId(data)
} }
func (ssr *SetSignatureRequired) Validate() error { func (ssr *SetSignatureRequired) Validate() error {
if ssr.author == nil { return ssr.OpBase.Validate(ssr, SetSignatureRequiredOp)
return fmt.Errorf("author not set")
}
return ssr.author.Validate()
}
func (ssr *SetSignatureRequired) Author() identity.Interface {
return ssr.author
} }
// Apply is the function that makes changes on the snapshot // Apply is the function that makes changes on the snapshot
func (ssr *SetSignatureRequired) Apply(snapshot *Snapshot) { func (ssr *SetSignatureRequired) Apply(snapshot *Snapshot) {
// check that we are allowed to change the config // check that we are allowed to change the config
if _, ok := snapshot.Administrator[ssr.author]; !ok { if _, ok := snapshot.Administrator[ssr.Author()]; !ok {
return return
} }
snapshot.SignatureRequired = ssr.Value 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 // AddAdministrator is an operation to add a new administrator in the set
type AddAdministrator struct { type AddAdministrator struct {
author identity.Interface dag.OpBase
OperationType OperationType `json:"type"` ToAdd []identity.Interface `json:"to_add"`
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"`
} }
func NewAddAdministratorOp(author identity.Interface, toAdd ...identity.Interface) *AddAdministrator { 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 { func (aa *AddAdministrator) Id() entity.Id {
// we could memorize the Id when deserializing, but that will do // the Id of the operation is the hash of the serialized data.
data, _ := json.Marshal(aa) return dag.IdOperation(aa, &aa.OpBase)
return entity.DeriveId(data)
} }
func (aa *AddAdministrator) Validate() error { func (aa *AddAdministrator) Validate() error {
@ -138,20 +126,13 @@ func (aa *AddAdministrator) Validate() error {
if len(aa.ToAdd) == 0 { if len(aa.ToAdd) == 0 {
return fmt.Errorf("nothing to add") return fmt.Errorf("nothing to add")
} }
if aa.author == nil { return aa.OpBase.Validate(aa, AddAdministratorOp)
return fmt.Errorf("author not set")
}
return aa.author.Validate()
}
func (aa *AddAdministrator) Author() identity.Interface {
return aa.author
} }
// Apply is the function that makes changes on the snapshot // Apply is the function that makes changes on the snapshot
func (aa *AddAdministrator) Apply(snapshot *Snapshot) { func (aa *AddAdministrator) Apply(snapshot *Snapshot) {
// check that we are allowed to change the config ... or if there is no admin yet // 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 return
} }
for _, toAdd := range aa.ToAdd { 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 // RemoveAdministrator is an operation to remove an administrator from the set
type RemoveAdministrator struct { type RemoveAdministrator struct {
author identity.Interface dag.OpBase
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 {
ToRemove []identity.Interface `json:"to_remove"` ToRemove []identity.Interface `json:"to_remove"`
} }
func NewRemoveAdministratorOp(author identity.Interface, toRemove ...identity.Interface) *RemoveAdministrator { 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 { func (ra *RemoveAdministrator) Id() entity.Id {
// the Id of the operation is the hash of the serialized data. // the Id of the operation is the hash of the serialized data.
// we could memorize the Id when deserializing, but that will do return dag.IdOperation(ra, &ra.OpBase)
data, _ := json.Marshal(ra)
return entity.DeriveId(data)
} }
func (ra *RemoveAdministrator) Validate() error { func (ra *RemoveAdministrator) Validate() error {
@ -188,26 +164,19 @@ func (ra *RemoveAdministrator) Validate() error {
if len(ra.ToRemove) == 0 { if len(ra.ToRemove) == 0 {
return fmt.Errorf("nothing to remove") return fmt.Errorf("nothing to remove")
} }
if ra.author == nil { return ra.OpBase.Validate(ra, RemoveAdministratorOp)
return fmt.Errorf("author not set")
}
return ra.author.Validate()
}
func (ra *RemoveAdministrator) Author() identity.Interface {
return ra.author
} }
// Apply is the function that makes changes on the snapshot // Apply is the function that makes changes on the snapshot
func (ra *RemoveAdministrator) Apply(snapshot *Snapshot) { func (ra *RemoveAdministrator) Apply(snapshot *Snapshot) {
// check if we are allowed to make changes // check if we are allowed to make changes
if !snapshot.HasAdministrator(ra.author) { if !snapshot.HasAdministrator(ra.Author()) {
return return
} }
// special rule: we can't end up with no administrator // special rule: we can't end up with no administrator
stillSome := false stillSome := false
for admin, _ := range snapshot.Administrator { for admin, _ := range snapshot.Administrator {
if admin != ra.author { if admin != ra.Author() {
stillSome = true stillSome = true
break break
} }
@ -245,71 +214,52 @@ var def = dag.Definition{
// operationUnmarshaller is a function doing the de-serialization of the JSON data into our own // 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. // 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 { var t struct {
OperationType OperationType `json:"type"` OperationType dag.OperationType `json:"type"`
} }
if err := json.Unmarshal(raw, &t); err != nil { if err := json.Unmarshal(raw, &t); err != nil {
return nil, err return nil, err
} }
var value interface{} var op dag.Operation
switch t.OperationType { switch t.OperationType {
case AddAdministratorOp: case AddAdministratorOp:
value = &addAdministratorJson{} op = &AddAdministrator{}
case RemoveAdministratorOp: case RemoveAdministratorOp:
value = &removeAdministratorJson{} op = &RemoveAdministrator{}
case SetSignatureRequiredOp: case SetSignatureRequiredOp:
value = &SetSignatureRequired{} op = &SetSignatureRequired{}
default: default:
panic(fmt.Sprintf("unknown operation type %v", t.OperationType)) panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
} }
err := json.Unmarshal(raw, &value) err := json.Unmarshal(raw, &op)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var op Operation switch op := op.(type) {
case *AddAdministrator:
switch value := value.(type) { // We need to resolve identities
case *SetSignatureRequired: for i, stub := range op.ToAdd {
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 {
iden, err := resolver.ResolveIdentity(stub.Id()) iden, err := resolver.ResolveIdentity(stub.Id())
if err != nil { if err != nil {
return nil, err return nil, err
} }
aa.ToAdd[i] = iden op.ToAdd[i] = iden
} }
op = aa case *RemoveAdministrator:
case *removeAdministratorJson: // We need to resolve identities
// We need something less straightforward to deserialize and resolve identities for i, stub := range op.ToRemove {
ra := &RemoveAdministrator{
author: author,
OperationType: RemoveAdministratorOp,
ToRemove: make([]identity.Interface, len(value.ToRemove)),
}
for i, stub := range value.ToRemove {
iden, err := resolver.ResolveIdentity(stub.Id()) iden, err := resolver.ResolveIdentity(stub.Id())
if err != nil { if err != nil {
return nil, err 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 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 package dag
import ( import (
"crypto/rand"
"encoding/json"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository" "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. // 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 // 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. // data structure and storage.
@ -22,23 +32,39 @@ type Operation interface {
// a minimal amount of entropy and avoid collision. // 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 // 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 // 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 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. // 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. // A common way to derive an Id will be to use the entity.DeriveId() function on the serialized operation data.
Id() entity.Id Id() entity.Id
// Type return the type of the operation
Type() OperationType
// Validate check if the Operation data is valid // Validate check if the Operation data is valid
Validate() error Validate() error
// Author returns the author of this operation // Author returns the author of this operation
Author() identity.Interface 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 { type OperationWithFiles interface {
Operation
// GetFiles return the files needed by this 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 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. // this information is read later, when loading from storage.
@ -46,3 +72,201 @@ type OperationWithFiles interface {
// hash). // hash).
GetFiles() []repository.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 { for _, raw := range aux.Operations {
// delegate to specialized unmarshal function // delegate to specialized unmarshal function
op, err := def.OperationUnmarshaler(author, raw, resolver) op, err := def.OperationUnmarshaler(raw, resolver)
if err != nil { if err != nil {
return nil, nil, err 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) ops = append(ops, op)
} }

View File

@ -11,13 +11,13 @@ import (
) )
func TestOperationPackReadWrite(t *testing.T) { func TestOperationPackReadWrite(t *testing.T) {
repo, id1, _, resolver, def := makeTestContext() repo, author, _, resolver, def := makeTestContext()
opp := &operationPack{ opp := &operationPack{
Author: id1, Author: author,
Operations: []Operation{ Operations: []Operation{
newOp1(id1, "foo"), newOp1(author, "foo"),
newOp2(id1, "bar"), newOp2(author, "bar"),
}, },
CreateTime: 123, CreateTime: 123,
EditTime: 456, EditTime: 456,
@ -32,34 +32,26 @@ func TestOperationPackReadWrite(t *testing.T) {
opp2, err := readOperationPack(def, repo, resolver, commit) opp2, err := readOperationPack(def, repo, resolver, commit)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, opp, opp2) for _, op := range opp.Operations {
// force the creation of the id
// make sure we get the same Id with the same data op.Id()
opp3 := &operationPack{
Author: id1,
Operations: []Operation{
newOp1(id1, "foo"),
newOp2(id1, "bar"),
},
CreateTime: 123,
EditTime: 456,
} }
require.Equal(t, opp.Id(), opp3.Id()) require.Equal(t, opp, opp2)
} }
func TestOperationPackSignedReadWrite(t *testing.T) { 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()) orig.Keys = append(orig.Keys, identity.GenerateKey())
}) })
require.NoError(t, err) require.NoError(t, err)
opp := &operationPack{ opp := &operationPack{
Author: id1, Author: author,
Operations: []Operation{ Operations: []Operation{
newOp1(id1, "foo"), newOp1(author, "foo"),
newOp2(id1, "bar"), newOp2(author, "bar"),
}, },
CreateTime: 123, CreateTime: 123,
EditTime: 456, EditTime: 456,
@ -74,23 +66,15 @@ func TestOperationPackSignedReadWrite(t *testing.T) {
opp2, err := readOperationPack(def, repo, resolver, commit) opp2, err := readOperationPack(def, repo, resolver, commit)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, opp, opp2) for _, op := range opp.Operations {
// force the creation of the id
// make sure we get the same Id with the same data op.Id()
opp3 := &operationPack{
Author: id1,
Operations: []Operation{
newOp1(id1, "foo"),
newOp2(id1, "bar"),
},
CreateTime: 123,
EditTime: 456,
} }
require.Equal(t, opp.Id(), opp3.Id()) require.Equal(t, opp, opp2)
} }
func TestOperationPackFiles(t *testing.T) { func TestOperationPackFiles(t *testing.T) {
repo, id1, _, resolver, def := makeTestContext() repo, author, _, resolver, def := makeTestContext()
blobHash1, err := repo.StoreData(randomData()) blobHash1, err := repo.StoreData(randomData())
require.NoError(t, err) require.NoError(t, err)
@ -99,10 +83,10 @@ func TestOperationPackFiles(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
opp := &operationPack{ opp := &operationPack{
Author: id1, Author: author,
Operations: []Operation{ Operations: []Operation{
newOp1(id1, "foo", blobHash1, blobHash2), newOp1(author, "foo", blobHash1, blobHash2),
newOp1(id1, "foo", blobHash2), newOp1(author, "foo", blobHash2),
}, },
CreateTime: 123, CreateTime: 123,
EditTime: 456, EditTime: 456,
@ -117,6 +101,10 @@ func TestOperationPackFiles(t *testing.T) {
opp2, err := readOperationPack(def, repo, resolver, commit) opp2, err := readOperationPack(def, repo, resolver, commit)
require.NoError(t, err) require.NoError(t, err)
for _, op := range opp.Operations {
// force the creation of the id
op.Id()
}
require.Equal(t, opp, opp2) require.Equal(t, opp, opp2)
require.ElementsMatch(t, opp2.Operations[0].(OperationWithFiles).GetFiles(), []repository.Hash{ 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 module github.com/MichaelMure/git-bug
go 1.16 go 1.18
require ( require (
github.com/99designs/gqlgen v0.17.1 github.com/99designs/gqlgen v0.17.13
github.com/99designs/keyring v1.2.1 github.com/99designs/keyring v1.2.1
github.com/MichaelMure/go-term-text v0.3.1 github.com/MichaelMure/go-term-text v0.3.1
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7
github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195
github.com/awesome-gocui/gocui v1.1.0 github.com/awesome-gocui/gocui v1.1.0
github.com/blevesearch/bleve v1.0.14 github.com/blevesearch/bleve v1.0.14
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 github.com/cheekybits/genny v1.0.0
github.com/corpix/uarand v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.0
github.com/go-git/go-billy/v5 v5.3.1 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/phayes/freeport v0.0.0-20171002181615-b8543db493a5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7 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/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/stretchr/testify v1.7.2 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 github.com/xanzy/go-gitlab v0.68.0
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 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/sys v0.0.0-20220204135822-1c1b9b1eba6a
golang.org/x/text v0.3.7 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= 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 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= 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.13 h1:ETUEqvRg5Zvr1lXtpoRdj026fzVay0ZlJPwI33qXLIw=
github.com/99designs/gqlgen v0.17.1/go.mod h1:K5fzLKwtph+FFgh9j7nFbRUdBKvTcGnsta51fsMTn3o= 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 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o=
github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= 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 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/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/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 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 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 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.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 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 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 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= 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 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY=
github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU= 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/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 v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 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/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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 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/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw= github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw=
github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4= 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 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 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= 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/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 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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 h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= 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= 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/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 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE= github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 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.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 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.2.3 h1:f/MjBEBDLttYCGfRaKBbKSRVF5aV2O6fnBpzknuE3jU= github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA=
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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.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 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= 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 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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/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 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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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= 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/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 h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= 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/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 h1:VAzdS5Nw68fbf5RZ8RDVlUvPXNU6Z3jtPCK/qvm4FoQ=
github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.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 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= 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 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 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/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/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= github.com/vektah/gqlparser/v2 v2.4.6 h1:Yjzp66g6oVq93Jihbi0qhGnf/6zIWjcm8H6gA27zstE=
github.com/vektah/gqlparser/v2 v2.4.1 h1:QOyEn8DAPMUMARGMeshKDkDgNmVoEaEGiDB0uWxcSlQ= github.com/vektah/gqlparser/v2 v2.4.6/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc= github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 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= 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 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= 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/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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/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= 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-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-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-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 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-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-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-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-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 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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-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-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-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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/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-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-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-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-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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE= 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-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-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-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.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.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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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/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.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.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.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.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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= 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(), time.Now().Unix(),
fake.Sentence(), fake.Sentence(),
paragraphs(), paragraphs(),
nil, nil,
) )
if err != nil { if err != nil {
@ -143,19 +144,19 @@ func paragraphs() string {
} }
func comment(b bug.Interface, p identity.Interface, timestamp int64) { 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) { 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) { 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) { func close(b bug.Interface, p identity.Interface, timestamp int64) {
_, _ = bug.Close(b, p, timestamp) _, _ = bug.Close(b, p, timestamp, nil)
} }
var addedLabels []string var addedLabels []string
@ -182,5 +183,5 @@ func labels(b bug.Interface, p identity.Interface, timestamp int64) {
// ignore error // ignore error
// if the randomisation produce no changes, no op // if the randomisation produce no changes, no op
// is added to the bug // 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 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) { func (repo *GoGitRepo) GetUserName() (string, error) {
return repo.AnyConfig().ReadString("user.name") return repo.AnyConfig().ReadString("user.name")
} }

View File

@ -60,9 +60,9 @@ type RepoKeyring interface {
Keyring() Keyring 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 { 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) GetUserName() (string, error)
// GetUserEmail returns the email address that the user has used to configure git. // 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 package webui

View File

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

View File

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