mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-14 08:45:30 +03:00
Merge pull request #835 from MichaelMure/op-base
entity/dag: proper base operation for simplified implementation
This commit is contained in:
commit
cd52872475
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
go-version: [1.18.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
go-version: 1.18.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
@ -1,7 +1,7 @@
|
||||
//go:generate genny -in=connection_template.go -out=gen_lazy_bug.go gen "Name=LazyBug NodeType=entity.Id EdgeType=LazyBugEdge ConnectionType=models.BugConnection"
|
||||
//go:generate genny -in=connection_template.go -out=gen_lazy_identity.go gen "Name=LazyIdentity NodeType=entity.Id EdgeType=LazyIdentityEdge ConnectionType=models.IdentityConnection"
|
||||
//go:generate genny -in=connection_template.go -out=gen_identity.go gen "Name=Identity NodeType=models.IdentityWrapper EdgeType=models.IdentityEdge ConnectionType=models.IdentityConnection"
|
||||
//go:generate genny -in=connection_template.go -out=gen_operation.go gen "Name=Operation NodeType=bug.Operation EdgeType=models.OperationEdge ConnectionType=models.OperationConnection"
|
||||
//go:generate genny -in=connection_template.go -out=gen_operation.go gen "Name=Operation NodeType=dag.Operation EdgeType=models.OperationEdge ConnectionType=models.OperationConnection"
|
||||
//go:generate genny -in=connection_template.go -out=gen_comment.go gen "Name=Comment NodeType=bug.Comment EdgeType=models.CommentEdge ConnectionType=models.CommentConnection"
|
||||
//go:generate genny -in=connection_template.go -out=gen_timeline.go gen "Name=TimelineItem NodeType=bug.TimelineItem EdgeType=models.TimelineItemEdge ConnectionType=models.TimelineItemConnection"
|
||||
//go:generate genny -in=connection_template.go -out=gen_label.go gen "Name=Label NodeType=bug.Label EdgeType=models.LabelEdge ConnectionType=models.LabelConnection"
|
||||
|
@ -8,23 +8,23 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/api/graphql/models"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
)
|
||||
|
||||
// BugOperationEdgeMaker define a function that take a bug.Operation and an offset and
|
||||
// DagOperationEdgeMaker define a function that take a dag.Operation and an offset and
|
||||
// create an Edge.
|
||||
type OperationEdgeMaker func(value bug.Operation, offset int) Edge
|
||||
type OperationEdgeMaker func(value dag.Operation, offset int) Edge
|
||||
|
||||
// OperationConMaker define a function that create a models.OperationConnection
|
||||
type OperationConMaker func(
|
||||
edges []*models.OperationEdge,
|
||||
nodes []bug.Operation,
|
||||
nodes []dag.Operation,
|
||||
info *models.PageInfo,
|
||||
totalCount int) (*models.OperationConnection, error)
|
||||
|
||||
// OperationCon will paginate a source according to the input of a relay connection
|
||||
func OperationCon(source []bug.Operation, edgeMaker OperationEdgeMaker, conMaker OperationConMaker, input models.ConnectionInput) (*models.OperationConnection, error) {
|
||||
var nodes []bug.Operation
|
||||
func OperationCon(source []dag.Operation, edgeMaker OperationEdgeMaker, conMaker OperationConMaker, input models.ConnectionInput) (*models.OperationConnection, error) {
|
||||
var nodes []dag.Operation
|
||||
var edges []*models.OperationEdge
|
||||
var cursors []string
|
||||
var pageInfo = &models.PageInfo{}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// +build ignore
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
|
@ -33,7 +33,7 @@ models:
|
||||
Hash:
|
||||
model: github.com/MichaelMure/git-bug/repository.Hash
|
||||
Operation:
|
||||
model: github.com/MichaelMure/git-bug/bug.Operation
|
||||
model: github.com/MichaelMure/git-bug/entity/dag.Operation
|
||||
CreateOperation:
|
||||
model: github.com/MichaelMure/git-bug/bug.CreateOperation
|
||||
SetTitleOperation:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
@ -250,7 +251,7 @@ type OpenBugPayload struct {
|
||||
// The connection type for an Operation
|
||||
type OperationConnection struct {
|
||||
Edges []*OperationEdge `json:"edges"`
|
||||
Nodes []bug.Operation `json:"nodes"`
|
||||
Nodes []dag.Operation `json:"nodes"`
|
||||
PageInfo *PageInfo `json:"pageInfo"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
}
|
||||
@ -258,7 +259,7 @@ type OperationConnection struct {
|
||||
// Represent an Operation
|
||||
type OperationEdge struct {
|
||||
Cursor string `json:"cursor"`
|
||||
Node bug.Operation `json:"node"`
|
||||
Node dag.Operation `json:"node"`
|
||||
}
|
||||
|
||||
// Information about pagination in a connection.
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
)
|
||||
|
||||
// BugWrapper is an interface used by the GraphQL resolvers to handle a bug.
|
||||
@ -24,7 +25,7 @@ type BugWrapper interface {
|
||||
Participants() ([]IdentityWrapper, error)
|
||||
CreatedAt() time.Time
|
||||
Timeline() ([]bug.TimelineItem, error)
|
||||
Operations() ([]bug.Operation, error)
|
||||
Operations() ([]dag.Operation, error)
|
||||
|
||||
IsAuthored()
|
||||
}
|
||||
@ -144,7 +145,7 @@ func (lb *lazyBug) Timeline() ([]bug.TimelineItem, error) {
|
||||
return lb.snap.Timeline, nil
|
||||
}
|
||||
|
||||
func (lb *lazyBug) Operations() ([]bug.Operation, error) {
|
||||
func (lb *lazyBug) Operations() ([]dag.Operation, error) {
|
||||
err := lb.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -210,6 +211,6 @@ func (l *loadedBug) Timeline() ([]bug.TimelineItem, error) {
|
||||
return l.Snapshot.Timeline, nil
|
||||
}
|
||||
|
||||
func (l *loadedBug) Operations() ([]bug.Operation, error) {
|
||||
func (l *loadedBug) Operations() ([]dag.Operation, error) {
|
||||
return l.Snapshot.Operations, nil
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/MichaelMure/git-bug/api/graphql/graph"
|
||||
"github.com/MichaelMure/git-bug/api/graphql/models"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
)
|
||||
|
||||
var _ graph.BugResolver = &bugResolver{}
|
||||
@ -69,14 +70,14 @@ func (bugResolver) Operations(_ context.Context, obj models.BugWrapper, after *s
|
||||
Last: last,
|
||||
}
|
||||
|
||||
edger := func(op bug.Operation, offset int) connections.Edge {
|
||||
edger := func(op dag.Operation, offset int) connections.Edge {
|
||||
return models.OperationEdge{
|
||||
Node: op,
|
||||
Cursor: connections.OffsetToCursor(offset),
|
||||
}
|
||||
}
|
||||
|
||||
conMaker := func(edges []*models.OperationEdge, nodes []bug.Operation, info *models.PageInfo, totalCount int) (*models.OperationConnection, error) {
|
||||
conMaker := func(edges []*models.OperationEdge, nodes []dag.Operation, info *models.PageInfo, totalCount int) (*models.OperationConnection, error) {
|
||||
return &models.OperationConnection{
|
||||
Edges: edges,
|
||||
Nodes: nodes,
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
@ -288,7 +289,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out
|
||||
|
||||
for _, op := range snapshot.Operations[1:] {
|
||||
// ignore SetMetadata operations
|
||||
if _, ok := op.(*bug.SetMetadataOperation); ok {
|
||||
if _, ok := op.(dag.OperationDoesntChangeSnapshot); ok {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -15,8 +15,8 @@ import (
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
)
|
||||
@ -245,7 +245,7 @@ func TestGithubPushPull(t *testing.T) {
|
||||
// verify operation have correct metadata
|
||||
for _, op := range tt.bug.Snapshot().Operations {
|
||||
// Check if the originals operations (*not* SetMetadata) are tagged properly
|
||||
if _, ok := op.(*bug.SetMetadataOperation); !ok {
|
||||
if _, ok := op.(dag.OperationDoesntChangeSnapshot); !ok {
|
||||
_, haveIDMetadata := op.GetMetadata(metaKeyGithubId)
|
||||
require.True(t, haveIDMetadata)
|
||||
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
@ -44,7 +45,7 @@ func TestGithubImporter(t *testing.T) {
|
||||
name: "simple issue",
|
||||
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/1",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "simple issue", "initial comment", nil),
|
||||
bug.NewAddCommentOp(author, 0, "first comment", nil),
|
||||
bug.NewAddCommentOp(author, 0, "second comment", nil),
|
||||
@ -55,7 +56,7 @@ func TestGithubImporter(t *testing.T) {
|
||||
name: "empty issue",
|
||||
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/2",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "empty issue", "", nil),
|
||||
},
|
||||
},
|
||||
@ -64,7 +65,7 @@ func TestGithubImporter(t *testing.T) {
|
||||
name: "complex issue",
|
||||
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/3",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "complex issue", "initial comment", nil),
|
||||
bug.NewLabelChangeOperation(author, 0, []bug.Label{"bug"}, []bug.Label{}),
|
||||
bug.NewLabelChangeOperation(author, 0, []bug.Label{"duplicate"}, []bug.Label{}),
|
||||
@ -81,7 +82,7 @@ func TestGithubImporter(t *testing.T) {
|
||||
name: "editions",
|
||||
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/4",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "editions", "initial comment edited", nil),
|
||||
bug.NewEditCommentOp(author, 0, "", "erased then edited again", nil),
|
||||
bug.NewAddCommentOp(author, 0, "first comment", nil),
|
||||
@ -93,7 +94,7 @@ func TestGithubImporter(t *testing.T) {
|
||||
name: "comment deletion",
|
||||
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/5",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "comment deletion", "", nil),
|
||||
},
|
||||
},
|
||||
@ -102,7 +103,7 @@ func TestGithubImporter(t *testing.T) {
|
||||
name: "edition deletion",
|
||||
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/6",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "edition deletion", "initial comment", nil),
|
||||
bug.NewEditCommentOp(author, 0, "", "initial comment edited again", nil),
|
||||
bug.NewAddCommentOp(author, 0, "first comment", nil),
|
||||
@ -114,7 +115,7 @@ func TestGithubImporter(t *testing.T) {
|
||||
name: "hidden comment",
|
||||
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/7",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "hidden comment", "initial comment", nil),
|
||||
bug.NewAddCommentOp(author, 0, "first comment", nil),
|
||||
},
|
||||
@ -124,7 +125,7 @@ func TestGithubImporter(t *testing.T) {
|
||||
name: "transfered issue",
|
||||
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/8",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "transfered issue", "", nil),
|
||||
},
|
||||
},
|
||||
@ -133,7 +134,7 @@ func TestGithubImporter(t *testing.T) {
|
||||
name: "unicode control characters",
|
||||
url: "https://github.com/MichaelMure/git-bug-test-github-bridge/issues/10",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "unicode control characters", "u0000: \nu0001: \nu0002: \nu0003: \nu0004: \nu0005: \nu0006: \nu0007: \nu0008: \nu0009: \t\nu0010: \nu0011: \nu0012: \nu0013: \nu0014: \nu0015: \nu0016: \nu0017: \nu0018: \nu0019:", nil),
|
||||
},
|
||||
},
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
@ -256,7 +257,7 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, out
|
||||
labelSet := make(map[string]struct{})
|
||||
for _, op := range snapshot.Operations[1:] {
|
||||
// ignore SetMetadata operations
|
||||
if _, ok := op.(*bug.SetMetadataOperation); ok {
|
||||
if _, ok := op.(dag.OperationDoesntChangeSnapshot); ok {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -11,11 +11,12 @@ import (
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
@ -247,7 +248,7 @@ func TestGitlabPushPull(t *testing.T) {
|
||||
// verify operation have correct metadata
|
||||
for _, op := range tt.bug.Snapshot().Operations {
|
||||
// Check if the originals operations (*not* SetMetadata) are tagged properly
|
||||
if _, ok := op.(*bug.SetMetadataOperation); !ok {
|
||||
if _, ok := op.(dag.OperationDoesntChangeSnapshot); !ok {
|
||||
_, haveIDMetadata := op.GetMetadata(metaKeyGitlabId)
|
||||
require.True(t, haveIDMetadata)
|
||||
|
||||
@ -272,7 +273,7 @@ func TestGitlabPushPull(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
require.Equal(t, issueOrigin, target)
|
||||
|
||||
//TODO: maybe more tests to ensure bug final state
|
||||
// TODO: maybe more tests to ensure bug final state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/MichaelMure/git-bug/bridge/core/auth"
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/interrupt"
|
||||
@ -49,7 +50,7 @@ func TestGitlabImport(t *testing.T) {
|
||||
name: "simple issue",
|
||||
url: "https://gitlab.com/git-bug/test/-/issues/1",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "simple issue", "initial comment", nil),
|
||||
bug.NewAddCommentOp(author, 0, "first comment", nil),
|
||||
bug.NewAddCommentOp(author, 0, "second comment", nil),
|
||||
@ -60,7 +61,7 @@ func TestGitlabImport(t *testing.T) {
|
||||
name: "empty issue",
|
||||
url: "https://gitlab.com/git-bug/test/-/issues/2",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "empty issue", "", nil),
|
||||
},
|
||||
},
|
||||
@ -69,7 +70,7 @@ func TestGitlabImport(t *testing.T) {
|
||||
name: "complex issue",
|
||||
url: "https://gitlab.com/git-bug/test/-/issues/3",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "complex issue", "initial comment", nil),
|
||||
bug.NewAddCommentOp(author, 0, "### header\n\n**bold**\n\n_italic_\n\n> with quote\n\n`inline code`\n\n```\nmultiline code\n```\n\n- bulleted\n- list\n\n1. numbered\n1. list\n\n- [ ] task\n- [x] list\n\n@MichaelMure mention\n\n#2 reference issue\n#3 auto-reference issue", nil),
|
||||
bug.NewSetTitleOp(author, 0, "complex issue edited", "complex issue"),
|
||||
@ -86,7 +87,7 @@ func TestGitlabImport(t *testing.T) {
|
||||
name: "editions",
|
||||
url: "https://gitlab.com/git-bug/test/-/issues/4",
|
||||
bug: &bug.Snapshot{
|
||||
Operations: []bug.Operation{
|
||||
Operations: []dag.Operation{
|
||||
bug.NewCreateOp(author, 0, "editions", "initial comment edited", nil),
|
||||
bug.NewAddCommentOp(author, 0, "first comment edited", nil),
|
||||
},
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
@ -297,7 +298,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, out ch
|
||||
|
||||
for _, op := range snapshot.Operations[1:] {
|
||||
// ignore SetMetadata operations
|
||||
if _, ok := op.(*bug.SetMetadataOperation); ok {
|
||||
if _, ok := op.(dag.OperationDoesntChangeSnapshot); ok {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/cache"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/util/text"
|
||||
)
|
||||
|
||||
@ -377,7 +378,7 @@ func labelSetsMatch(jiraSet []string, gitbugSet []bug.Label) bool {
|
||||
|
||||
// Create a bug.Operation (or a series of operations) from a JIRA changelog
|
||||
// entry
|
||||
func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, entry ChangeLogEntry, potentialOp bug.Operation) error {
|
||||
func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, entry ChangeLogEntry, potentialOp dag.Operation) error {
|
||||
|
||||
// If we have an operation which is already mapped to the entire changelog
|
||||
// entry then that means this changelog entry was induced by an export
|
||||
|
16
bug/bug.go
16
bug/bug.go
@ -44,11 +44,7 @@ func NewBug() *Bug {
|
||||
|
||||
// Read will read a bug from a repository
|
||||
func Read(repo repository.ClockedRepo, id entity.Id) (*Bug, error) {
|
||||
e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Bug{Entity: e}, nil
|
||||
return ReadWithResolver(repo, identity.NewSimpleResolver(repo), id)
|
||||
}
|
||||
|
||||
// ReadWithResolver will read a bug from its Id, with a custom identity.Resolver
|
||||
@ -144,21 +140,21 @@ func (bug *Bug) Operations() []Operation {
|
||||
}
|
||||
|
||||
// Compile a bug in a easily usable snapshot
|
||||
func (bug *Bug) Compile() Snapshot {
|
||||
snap := Snapshot{
|
||||
func (bug *Bug) Compile() *Snapshot {
|
||||
snap := &Snapshot{
|
||||
id: bug.Id(),
|
||||
Status: OpenStatus,
|
||||
}
|
||||
|
||||
for _, op := range bug.Operations() {
|
||||
op.Apply(&snap)
|
||||
op.Apply(snap)
|
||||
snap.Operations = append(snap.Operations, op)
|
||||
}
|
||||
|
||||
return snap
|
||||
}
|
||||
|
||||
// Lookup for the very first operation of the bug.
|
||||
// FirstOp lookup for the very first operation of the bug.
|
||||
// For a valid Bug, this operation should be a CreateOp
|
||||
func (bug *Bug) FirstOp() Operation {
|
||||
if fo := bug.Entity.FirstOp(); fo != nil {
|
||||
@ -167,7 +163,7 @@ func (bug *Bug) FirstOp() Operation {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup for the very last operation of the bug.
|
||||
// LastOp lookup for the very last operation of the bug.
|
||||
// For a valid Bug, should never be nil
|
||||
func (bug *Bug) LastOp() Operation {
|
||||
if lo := bug.Entity.LastOp(); lo != nil {
|
||||
|
@ -41,5 +41,5 @@ func (c Comment) FormatTime() string {
|
||||
return c.UnixTime.Time().Format("Mon Jan 2 15:04:05 2006 +0200")
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
// IsAuthored is a sign post method for gqlgen
|
||||
func (c Comment) IsAuthored() {}
|
||||
|
@ -7,34 +7,34 @@ import (
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
// Id return the Bug identifier
|
||||
// Id returns the Bug identifier
|
||||
Id() entity.Id
|
||||
|
||||
// Validate check if the Bug data is valid
|
||||
// Validate checks if the Bug data is valid
|
||||
Validate() error
|
||||
|
||||
// Append an operation into the staging area, to be committed later
|
||||
Append(op Operation)
|
||||
|
||||
// Operations return the ordered operations
|
||||
// Operations returns the ordered operations
|
||||
Operations() []Operation
|
||||
|
||||
// Indicate that the in-memory state changed and need to be commit in the repository
|
||||
// NeedCommit indicates that the in-memory state changed and need to be commit in the repository
|
||||
NeedCommit() bool
|
||||
|
||||
// Commit write the staging area in Git and move the operations to the packs
|
||||
// Commit writes the staging area in Git and move the operations to the packs
|
||||
Commit(repo repository.ClockedRepo) error
|
||||
|
||||
// Lookup for the very first operation of the bug.
|
||||
// FirstOp lookup for the very first operation of the bug.
|
||||
// For a valid Bug, this operation should be a CreateOp
|
||||
FirstOp() Operation
|
||||
|
||||
// Lookup for the very last operation of the bug.
|
||||
// LastOp lookup for the very last operation of the bug.
|
||||
// For a valid Bug, should never be nil
|
||||
LastOp() Operation
|
||||
|
||||
// Compile a bug in a easily usable snapshot
|
||||
Compile() Snapshot
|
||||
// Compile a bug in an easily usable snapshot
|
||||
Compile() *Snapshot
|
||||
|
||||
// CreateLamportTime return the Lamport time of creation
|
||||
CreateLamportTime() lamport.Time
|
||||
@ -42,14 +42,3 @@ type Interface interface {
|
||||
// EditLamportTime return the Lamport time of the last edit
|
||||
EditLamportTime() lamport.Time
|
||||
}
|
||||
|
||||
func bugFromInterface(bug Interface) *Bug {
|
||||
switch bug := bug.(type) {
|
||||
case *Bug:
|
||||
return bug
|
||||
case *WithSnapshot:
|
||||
return bug.Bug
|
||||
default:
|
||||
panic("missing type case")
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
@ -17,24 +16,24 @@ var _ dag.OperationWithFiles = &AddCommentOperation{}
|
||||
|
||||
// AddCommentOperation will add a new comment in the bug
|
||||
type AddCommentOperation struct {
|
||||
OpBase
|
||||
dag.OpBase
|
||||
Message string `json:"message"`
|
||||
// TODO: change for a map[string]util.hash to store the filename ?
|
||||
Files []repository.Hash `json:"files"`
|
||||
}
|
||||
|
||||
func (op *AddCommentOperation) Id() entity.Id {
|
||||
return idOperation(op, &op.OpBase)
|
||||
return dag.IdOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
|
||||
snapshot.addActor(op.Author_)
|
||||
snapshot.addParticipant(op.Author_)
|
||||
snapshot.addActor(op.Author())
|
||||
snapshot.addParticipant(op.Author())
|
||||
|
||||
comment := Comment{
|
||||
id: entity.CombineIds(snapshot.Id(), op.Id()),
|
||||
Message: op.Message,
|
||||
Author: op.Author_,
|
||||
Author: op.Author(),
|
||||
Files: op.Files,
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
}
|
||||
@ -64,64 +63,31 @@ func (op *AddCommentOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a two-steps JSON unmarshalling
|
||||
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
|
||||
// overriding the outer op's MarshalJSON
|
||||
func (op *AddCommentOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Message string `json:"message"`
|
||||
Files []repository.Hash `json:"files"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Message = aux.Message
|
||||
op.Files = aux.Files
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *AddCommentOperation) IsAuthored() {}
|
||||
|
||||
func NewAddCommentOp(author identity.Interface, unixTime int64, message string, files []repository.Hash) *AddCommentOperation {
|
||||
return &AddCommentOperation{
|
||||
OpBase: newOpBase(AddCommentOp, author, unixTime),
|
||||
OpBase: dag.NewOpBase(AddCommentOp, author, unixTime),
|
||||
Message: message,
|
||||
Files: files,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTimelineItem replace a AddComment operation in the Timeline and hold its edition history
|
||||
// AddCommentTimelineItem hold a comment in the timeline
|
||||
type AddCommentTimelineItem struct {
|
||||
CommentTimelineItem
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
// IsAuthored is a sign post method for gqlgen
|
||||
func (a *AddCommentTimelineItem) IsAuthored() {}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func AddComment(b Interface, author identity.Interface, unixTime int64, message string) (*AddCommentOperation, error) {
|
||||
return AddCommentWithFiles(b, author, unixTime, message, nil)
|
||||
}
|
||||
|
||||
func AddCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash) (*AddCommentOperation, error) {
|
||||
addCommentOp := NewAddCommentOp(author, unixTime, message, files)
|
||||
if err := addCommentOp.Validate(); err != nil {
|
||||
// AddComment is a convenience function to add a comment to a bug
|
||||
func AddComment(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*AddCommentOperation, error) {
|
||||
op := NewAddCommentOp(author, unixTime, message, files)
|
||||
for key, val := range metadata {
|
||||
op.SetMetadata(key, val)
|
||||
}
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Append(addCommentOp)
|
||||
return addCommentOp, nil
|
||||
b.Append(op)
|
||||
return op, nil
|
||||
}
|
||||
|
@ -1,37 +1,18 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
func TestAddCommentSerialize(t *testing.T) {
|
||||
repo := repository.NewMockRepo()
|
||||
|
||||
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
|
||||
require.NoError(t, err)
|
||||
|
||||
unix := time.Now().Unix()
|
||||
before := NewAddCommentOp(rene, unix, "message", nil)
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
require.NoError(t, err)
|
||||
|
||||
var after AddCommentOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
require.NoError(t, err)
|
||||
|
||||
// enforce creating the ID
|
||||
before.Id()
|
||||
|
||||
// Replace the identity as it's not serialized
|
||||
after.Author_ = rene
|
||||
|
||||
require.Equal(t, before, &after)
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
|
||||
return NewAddCommentOp(author, unixTime, "message", nil)
|
||||
})
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
|
||||
return NewAddCommentOp(author, unixTime, "message", []repository.Hash{"hash1", "hash2"})
|
||||
})
|
||||
}
|
||||
|
103
bug/op_create.go
103
bug/op_create.go
@ -1,7 +1,6 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
@ -17,53 +16,38 @@ var _ dag.OperationWithFiles = &CreateOperation{}
|
||||
|
||||
// CreateOperation define the initial creation of a bug
|
||||
type CreateOperation struct {
|
||||
OpBase
|
||||
dag.OpBase
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Files []repository.Hash `json:"files"`
|
||||
}
|
||||
|
||||
func (op *CreateOperation) Id() entity.Id {
|
||||
return idOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
// OVERRIDE
|
||||
func (op *CreateOperation) SetMetadata(key string, value string) {
|
||||
// sanity check: we make sure we are not in the following scenario:
|
||||
// - the bug is created with a first operation
|
||||
// - Id() is used
|
||||
// - metadata are added, which will change the Id
|
||||
// - Id() is used again
|
||||
|
||||
if op.id != entity.UnsetId {
|
||||
panic("usage of Id() after changing the first operation")
|
||||
}
|
||||
|
||||
op.OpBase.SetMetadata(key, value)
|
||||
return dag.IdOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
func (op *CreateOperation) Apply(snapshot *Snapshot) {
|
||||
// sanity check: will fail when adding a second Create
|
||||
if snapshot.id != "" && snapshot.id != entity.UnsetId && snapshot.id != op.Id() {
|
||||
panic("adding a second Create operation")
|
||||
return
|
||||
}
|
||||
|
||||
snapshot.id = op.Id()
|
||||
|
||||
snapshot.addActor(op.Author_)
|
||||
snapshot.addParticipant(op.Author_)
|
||||
snapshot.addActor(op.Author())
|
||||
snapshot.addParticipant(op.Author())
|
||||
|
||||
snapshot.Title = op.Title
|
||||
|
||||
comment := Comment{
|
||||
id: entity.CombineIds(snapshot.Id(), op.Id()),
|
||||
Message: op.Message,
|
||||
Author: op.Author_,
|
||||
Author: op.Author(),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
}
|
||||
|
||||
snapshot.Comments = []Comment{comment}
|
||||
snapshot.Author = op.Author_
|
||||
snapshot.Author = op.Author()
|
||||
snapshot.CreateTime = op.Time()
|
||||
|
||||
snapshot.Timeline = []TimelineItem{
|
||||
@ -82,13 +66,6 @@ func (op *CreateOperation) Validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(op.Nonce) > 64 {
|
||||
return fmt.Errorf("create nonce is too big")
|
||||
}
|
||||
if len(op.Nonce) < 20 {
|
||||
return fmt.Errorf("create nonce is too small")
|
||||
}
|
||||
|
||||
if text.Empty(op.Title) {
|
||||
return fmt.Errorf("title is empty")
|
||||
}
|
||||
@ -103,45 +80,9 @@ func (op *CreateOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a two step JSON unmarshalling
|
||||
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
|
||||
// overriding the outer op's MarshalJSON
|
||||
func (op *CreateOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Nonce []byte `json:"nonce"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Files []repository.Hash `json:"files"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Nonce = aux.Nonce
|
||||
op.Title = aux.Title
|
||||
op.Message = aux.Message
|
||||
op.Files = aux.Files
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *CreateOperation) IsAuthored() {}
|
||||
|
||||
func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation {
|
||||
return &CreateOperation{
|
||||
OpBase: newOpBase(CreateOp, author, unixTime),
|
||||
OpBase: dag.NewOpBase(CreateOp, author, unixTime),
|
||||
Title: title,
|
||||
Message: message,
|
||||
Files: files,
|
||||
@ -153,23 +94,19 @@ type CreateTimelineItem struct {
|
||||
CommentTimelineItem
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
// IsAuthored is a sign post method for gqlgen
|
||||
func (c *CreateTimelineItem) IsAuthored() {}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func Create(author identity.Interface, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
|
||||
return CreateWithFiles(author, unixTime, title, message, nil)
|
||||
}
|
||||
|
||||
func CreateWithFiles(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) (*Bug, *CreateOperation, error) {
|
||||
newBug := NewBug()
|
||||
createOp := NewCreateOp(author, unixTime, title, message, files)
|
||||
|
||||
if err := createOp.Validate(); err != nil {
|
||||
return nil, createOp, err
|
||||
// Create is a convenience function to create a bug
|
||||
func Create(author identity.Interface, unixTime int64, title, message string, files []repository.Hash, metadata map[string]string) (*Bug, *CreateOperation, error) {
|
||||
b := NewBug()
|
||||
op := NewCreateOp(author, unixTime, title, message, files)
|
||||
for key, val := range metadata {
|
||||
op.SetMetadata(key, val)
|
||||
}
|
||||
|
||||
newBug.Append(createOp)
|
||||
|
||||
return newBug, createOp, nil
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, op, err
|
||||
}
|
||||
b.Append(op)
|
||||
return b, op, nil
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
@ -58,26 +58,10 @@ func TestCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateSerialize(t *testing.T) {
|
||||
repo := repository.NewMockRepo()
|
||||
|
||||
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
|
||||
require.NoError(t, err)
|
||||
|
||||
unix := time.Now().Unix()
|
||||
before := NewCreateOp(rene, unix, "title", "message", nil)
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
require.NoError(t, err)
|
||||
|
||||
var after CreateOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
require.NoError(t, err)
|
||||
|
||||
// enforce creating the ID
|
||||
before.Id()
|
||||
|
||||
// Replace the identity as it's not serialized
|
||||
after.Author_ = rene
|
||||
|
||||
require.Equal(t, before, &after)
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
|
||||
return NewCreateOp(author, unixTime, "title", "message", nil)
|
||||
})
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
|
||||
return NewCreateOp(author, unixTime, "title", "message", []repository.Hash{"hash1", "hash2"})
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@ -20,14 +19,14 @@ var _ dag.OperationWithFiles = &EditCommentOperation{}
|
||||
|
||||
// EditCommentOperation will change a comment in the bug
|
||||
type EditCommentOperation struct {
|
||||
OpBase
|
||||
dag.OpBase
|
||||
Target entity.Id `json:"target"`
|
||||
Message string `json:"message"`
|
||||
Files []repository.Hash `json:"files"`
|
||||
}
|
||||
|
||||
func (op *EditCommentOperation) Id() entity.Id {
|
||||
return idOperation(op, &op.OpBase)
|
||||
return dag.IdOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
|
||||
@ -68,7 +67,7 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
snapshot.addActor(op.Author_)
|
||||
snapshot.addActor(op.Author())
|
||||
|
||||
// Updating the corresponding comment
|
||||
|
||||
@ -101,43 +100,9 @@ func (op *EditCommentOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON is two steps JSON unmarshalling
|
||||
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
|
||||
// overriding the outer op's MarshalJSON
|
||||
func (op *EditCommentOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Target entity.Id `json:"target"`
|
||||
Message string `json:"message"`
|
||||
Files []repository.Hash `json:"files"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Target = aux.Target
|
||||
op.Message = aux.Message
|
||||
op.Files = aux.Files
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *EditCommentOperation) IsAuthored() {}
|
||||
|
||||
func NewEditCommentOp(author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash) *EditCommentOperation {
|
||||
return &EditCommentOperation{
|
||||
OpBase: newOpBase(EditCommentOp, author, unixTime),
|
||||
OpBase: dag.NewOpBase(EditCommentOp, author, unixTime),
|
||||
Target: target,
|
||||
Message: message,
|
||||
Files: files,
|
||||
@ -145,27 +110,20 @@ func NewEditCommentOp(author identity.Interface, unixTime int64, target entity.I
|
||||
}
|
||||
|
||||
// EditComment is a convenience function to apply the operation
|
||||
func EditComment(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string) (*EditCommentOperation, error) {
|
||||
return EditCommentWithFiles(b, author, unixTime, target, message, nil)
|
||||
}
|
||||
|
||||
func EditCommentWithFiles(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash) (*EditCommentOperation, error) {
|
||||
editCommentOp := NewEditCommentOp(author, unixTime, target, message, files)
|
||||
if err := editCommentOp.Validate(); err != nil {
|
||||
func EditComment(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash, metadata map[string]string) (*EditCommentOperation, error) {
|
||||
op := NewEditCommentOp(author, unixTime, target, message, files)
|
||||
for key, val := range metadata {
|
||||
op.SetMetadata(key, val)
|
||||
}
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Append(editCommentOp)
|
||||
return editCommentOp, nil
|
||||
b.Append(op)
|
||||
return op, nil
|
||||
}
|
||||
|
||||
// EditCreateComment is a convenience function to edit the body of a bug (the first comment)
|
||||
func EditCreateComment(b Interface, author identity.Interface, unixTime int64, message string) (*EditCommentOperation, error) {
|
||||
func EditCreateComment(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*EditCommentOperation, error) {
|
||||
createOp := b.FirstOp().(*CreateOperation)
|
||||
return EditComment(b, author, unixTime, createOp.Id(), message)
|
||||
}
|
||||
|
||||
// EditCreateCommentWithFiles is a convenience function to edit the body of a bug (the first comment)
|
||||
func EditCreateCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash) (*EditCommentOperation, error) {
|
||||
createOp := b.FirstOp().(*CreateOperation)
|
||||
return EditCommentWithFiles(b, author, unixTime, createOp.Id(), message, files)
|
||||
return EditComment(b, author, unixTime, createOp.Id(), message, files, metadata)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
@ -75,26 +75,10 @@ func TestEdit(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEditCommentSerialize(t *testing.T) {
|
||||
repo := repository.NewMockRepo()
|
||||
|
||||
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
|
||||
require.NoError(t, err)
|
||||
|
||||
unix := time.Now().Unix()
|
||||
before := NewEditCommentOp(rene, unix, "target", "message", nil)
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
require.NoError(t, err)
|
||||
|
||||
var after EditCommentOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
require.NoError(t, err)
|
||||
|
||||
// enforce creating the ID
|
||||
before.Id()
|
||||
|
||||
// Replace the identity as it's not serialized
|
||||
after.Author_ = rene
|
||||
|
||||
require.Equal(t, before, &after)
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
|
||||
return NewEditCommentOp(author, unixTime, "target", "message", nil)
|
||||
})
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
|
||||
return NewEditCommentOp(author, unixTime, "target", "message", []repository.Hash{"hash1", "hash2"})
|
||||
})
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
)
|
||||
@ -16,18 +16,18 @@ var _ Operation = &LabelChangeOperation{}
|
||||
|
||||
// LabelChangeOperation define a Bug operation to add or remove labels
|
||||
type LabelChangeOperation struct {
|
||||
OpBase
|
||||
dag.OpBase
|
||||
Added []Label `json:"added"`
|
||||
Removed []Label `json:"removed"`
|
||||
}
|
||||
|
||||
func (op *LabelChangeOperation) Id() entity.Id {
|
||||
return idOperation(op, &op.OpBase)
|
||||
return dag.IdOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
// Apply apply the operation
|
||||
// Apply applies the operation
|
||||
func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
|
||||
snapshot.addActor(op.Author_)
|
||||
snapshot.addActor(op.Author())
|
||||
|
||||
// Add in the set
|
||||
AddLoop:
|
||||
@ -59,7 +59,7 @@ AddLoop:
|
||||
|
||||
item := &LabelChangeTimelineItem{
|
||||
id: op.Id(),
|
||||
Author: op.Author_,
|
||||
Author: op.Author(),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
Added: op.Added,
|
||||
Removed: op.Removed,
|
||||
@ -92,41 +92,9 @@ func (op *LabelChangeOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a two step JSON unmarshalling
|
||||
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
|
||||
// overriding the outer op's MarshalJSON
|
||||
func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Added []Label `json:"added"`
|
||||
Removed []Label `json:"removed"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Added = aux.Added
|
||||
op.Removed = aux.Removed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *LabelChangeOperation) IsAuthored() {}
|
||||
|
||||
func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
|
||||
return &LabelChangeOperation{
|
||||
OpBase: newOpBase(LabelChangeOp, author, unixTime),
|
||||
OpBase: dag.NewOpBase(LabelChangeOp, author, unixTime),
|
||||
Added: added,
|
||||
Removed: removed,
|
||||
}
|
||||
@ -144,11 +112,11 @@ func (l LabelChangeTimelineItem) Id() entity.Id {
|
||||
return l.id
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (l *LabelChangeTimelineItem) IsAuthored() {}
|
||||
// IsAuthored is a sign post method for gqlgen
|
||||
func (l LabelChangeTimelineItem) IsAuthored() {}
|
||||
|
||||
// ChangeLabels is a convenience function to apply the operation
|
||||
func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
|
||||
// ChangeLabels is a convenience function to change labels on a bug
|
||||
func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) ([]LabelChangeResult, *LabelChangeOperation, error) {
|
||||
var added, removed []Label
|
||||
var results []LabelChangeResult
|
||||
|
||||
@ -196,23 +164,25 @@ func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, r
|
||||
return results, nil, fmt.Errorf("no label added or removed")
|
||||
}
|
||||
|
||||
labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
|
||||
|
||||
if err := labelOp.Validate(); err != nil {
|
||||
op := NewLabelChangeOperation(author, unixTime, added, removed)
|
||||
for key, val := range metadata {
|
||||
op.SetMetadata(key, val)
|
||||
}
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
b.Append(labelOp)
|
||||
b.Append(op)
|
||||
|
||||
return results, labelOp, nil
|
||||
return results, op, nil
|
||||
}
|
||||
|
||||
// ForceChangeLabels is a convenience function to apply the operation
|
||||
// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely
|
||||
// responsible of what you are doing. In the general case, you want to use ChangeLabels instead.
|
||||
// responsible for what you are doing. In the general case, you want to use ChangeLabels instead.
|
||||
// The intended use of this function is to allow importers to create legal but unexpected label changes,
|
||||
// like removing a label with no information of when it was added before.
|
||||
func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) (*LabelChangeOperation, error) {
|
||||
func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) (*LabelChangeOperation, error) {
|
||||
added := make([]Label, len(add))
|
||||
for i, str := range add {
|
||||
added[i] = Label(str)
|
||||
@ -223,15 +193,18 @@ func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, a
|
||||
removed[i] = Label(str)
|
||||
}
|
||||
|
||||
labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
|
||||
op := NewLabelChangeOperation(author, unixTime, added, removed)
|
||||
|
||||
if err := labelOp.Validate(); err != nil {
|
||||
for key, val := range metadata {
|
||||
op.SetMetadata(key, val)
|
||||
}
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.Append(labelOp)
|
||||
b.Append(op)
|
||||
|
||||
return labelOp, nil
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func labelExist(labels []Label, label Label) bool {
|
||||
|
@ -1,37 +1,20 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
func TestLabelChangeSerialize(t *testing.T) {
|
||||
repo := repository.NewMockRepo()
|
||||
|
||||
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
|
||||
require.NoError(t, err)
|
||||
|
||||
unix := time.Now().Unix()
|
||||
before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
require.NoError(t, err)
|
||||
|
||||
var after LabelChangeOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
require.NoError(t, err)
|
||||
|
||||
// enforce creating the ID
|
||||
before.Id()
|
||||
|
||||
// Replace the identity as it's not serialized
|
||||
after.Author_ = rene
|
||||
|
||||
require.Equal(t, before, &after)
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
|
||||
return NewLabelChangeOperation(author, unixTime, []Label{"added"}, []Label{"removed"})
|
||||
})
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
|
||||
return NewLabelChangeOperation(author, unixTime, []Label{"added"}, nil)
|
||||
})
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
|
||||
return NewLabelChangeOperation(author, unixTime, nil, []Label{"removed"})
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -1,108 +1,21 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/text"
|
||||
)
|
||||
|
||||
var _ Operation = &SetMetadataOperation{}
|
||||
|
||||
type SetMetadataOperation struct {
|
||||
OpBase
|
||||
Target entity.Id `json:"target"`
|
||||
NewMetadata map[string]string `json:"new_metadata"`
|
||||
func NewSetMetadataOp(author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *dag.SetMetadataOperation[*Snapshot] {
|
||||
return dag.NewSetMetadataOp[*Snapshot](SetMetadataOp, author, unixTime, target, newMetadata)
|
||||
}
|
||||
|
||||
func (op *SetMetadataOperation) Id() entity.Id {
|
||||
return idOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
func (op *SetMetadataOperation) Apply(snapshot *Snapshot) {
|
||||
for _, target := range snapshot.Operations {
|
||||
if target.Id() == op.Target {
|
||||
// Apply the metadata in an immutable way: if a metadata already
|
||||
// exist, it's not possible to override it.
|
||||
for key, value := range op.NewMetadata {
|
||||
target.setExtraMetadataImmutable(key, value)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (op *SetMetadataOperation) Validate() error {
|
||||
if err := op.OpBase.Validate(op, SetMetadataOp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := op.Target.Validate(); err != nil {
|
||||
return errors.Wrap(err, "target invalid")
|
||||
}
|
||||
|
||||
for key, val := range op.NewMetadata {
|
||||
if !text.SafeOneLine(key) {
|
||||
return fmt.Errorf("metadata key is unsafe")
|
||||
}
|
||||
if !text.Safe(val) {
|
||||
return fmt.Errorf("metadata value is not fully printable")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a two step JSON unmarshalling
|
||||
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
|
||||
// overriding the outer op's MarshalJSON
|
||||
func (op *SetMetadataOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Target entity.Id `json:"target"`
|
||||
NewMetadata map[string]string `json:"new_metadata"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Target = aux.Target
|
||||
op.NewMetadata = aux.NewMetadata
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *SetMetadataOperation) IsAuthored() {}
|
||||
|
||||
func NewSetMetadataOp(author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *SetMetadataOperation {
|
||||
return &SetMetadataOperation{
|
||||
OpBase: newOpBase(SetMetadataOp, author, unixTime),
|
||||
Target: target,
|
||||
NewMetadata: newMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func SetMetadata(b Interface, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*SetMetadataOperation, error) {
|
||||
SetMetadataOp := NewSetMetadataOp(author, unixTime, target, newMetadata)
|
||||
if err := SetMetadataOp.Validate(); err != nil {
|
||||
// SetMetadata is a convenience function to add metadata on another operation
|
||||
func SetMetadata(b Interface, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*Snapshot], error) {
|
||||
op := NewSetMetadataOp(author, unixTime, target, newMetadata)
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Append(SetMetadataOp)
|
||||
return SetMetadataOp, nil
|
||||
b.Append(op)
|
||||
return op, nil
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
)
|
||||
@ -14,21 +13,21 @@ var _ Operation = &SetStatusOperation{}
|
||||
|
||||
// SetStatusOperation will change the status of a bug
|
||||
type SetStatusOperation struct {
|
||||
OpBase
|
||||
dag.OpBase
|
||||
Status Status `json:"status"`
|
||||
}
|
||||
|
||||
func (op *SetStatusOperation) Id() entity.Id {
|
||||
return idOperation(op, &op.OpBase)
|
||||
return dag.IdOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
|
||||
snapshot.Status = op.Status
|
||||
snapshot.addActor(op.Author_)
|
||||
snapshot.addActor(op.Author())
|
||||
|
||||
item := &SetStatusTimelineItem{
|
||||
id: op.Id(),
|
||||
Author: op.Author_,
|
||||
Author: op.Author(),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
Status: op.Status,
|
||||
}
|
||||
@ -48,39 +47,9 @@ func (op *SetStatusOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a two step JSON unmarshalling
|
||||
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
|
||||
// overriding the outer op's MarshalJSON
|
||||
func (op *SetStatusOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Status Status `json:"status"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Status = aux.Status
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *SetStatusOperation) IsAuthored() {}
|
||||
|
||||
func NewSetStatusOp(author identity.Interface, unixTime int64, status Status) *SetStatusOperation {
|
||||
return &SetStatusOperation{
|
||||
OpBase: newOpBase(SetStatusOp, author, unixTime),
|
||||
OpBase: dag.NewOpBase(SetStatusOp, author, unixTime),
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
@ -96,12 +65,15 @@ func (s SetStatusTimelineItem) Id() entity.Id {
|
||||
return s.id
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (s *SetStatusTimelineItem) IsAuthored() {}
|
||||
// IsAuthored is a sign post method for gqlgen
|
||||
func (s SetStatusTimelineItem) IsAuthored() {}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
|
||||
// Open is a convenience function to change a bugs state to Open
|
||||
func Open(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) {
|
||||
op := NewSetStatusOp(author, unixTime, OpenStatus)
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -109,9 +81,12 @@ func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOpe
|
||||
return op, nil
|
||||
}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func Close(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
|
||||
// Close is a convenience function to change a bugs state to Close
|
||||
func Close(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) {
|
||||
op := NewSetStatusOp(author, unixTime, ClosedStatus)
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,37 +1,14 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
func TestSetStatusSerialize(t *testing.T) {
|
||||
repo := repository.NewMockRepo()
|
||||
|
||||
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
|
||||
require.NoError(t, err)
|
||||
|
||||
unix := time.Now().Unix()
|
||||
before := NewSetStatusOp(rene, unix, ClosedStatus)
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
require.NoError(t, err)
|
||||
|
||||
var after SetStatusOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
require.NoError(t, err)
|
||||
|
||||
// enforce creating the ID
|
||||
before.Id()
|
||||
|
||||
// Replace the identity as it's not serialized
|
||||
after.Author_ = rene
|
||||
|
||||
require.Equal(t, before, &after)
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetStatusOperation {
|
||||
return NewSetStatusOp(author, unixTime, ClosedStatus)
|
||||
})
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/util/timestamp"
|
||||
|
||||
@ -15,22 +15,22 @@ var _ Operation = &SetTitleOperation{}
|
||||
|
||||
// SetTitleOperation will change the title of a bug
|
||||
type SetTitleOperation struct {
|
||||
OpBase
|
||||
dag.OpBase
|
||||
Title string `json:"title"`
|
||||
Was string `json:"was"`
|
||||
}
|
||||
|
||||
func (op *SetTitleOperation) Id() entity.Id {
|
||||
return idOperation(op, &op.OpBase)
|
||||
return dag.IdOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
|
||||
snapshot.Title = op.Title
|
||||
snapshot.addActor(op.Author_)
|
||||
snapshot.addActor(op.Author())
|
||||
|
||||
item := &SetTitleTimelineItem{
|
||||
id: op.Id(),
|
||||
Author: op.Author_,
|
||||
Author: op.Author(),
|
||||
UnixTime: timestamp.Timestamp(op.UnixTime),
|
||||
Title: op.Title,
|
||||
Was: op.Was,
|
||||
@ -59,41 +59,9 @@ func (op *SetTitleOperation) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a two step JSON unmarshalling
|
||||
// This workaround is necessary to avoid the inner OpBase.MarshalJSON
|
||||
// overriding the outer op's MarshalJSON
|
||||
func (op *SetTitleOperation) UnmarshalJSON(data []byte) error {
|
||||
// Unmarshal OpBase and the op separately
|
||||
|
||||
base := OpBase{}
|
||||
err := json.Unmarshal(data, &base)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aux := struct {
|
||||
Title string `json:"title"`
|
||||
Was string `json:"was"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.OpBase = base
|
||||
op.Title = aux.Title
|
||||
op.Was = aux.Was
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (op *SetTitleOperation) IsAuthored() {}
|
||||
|
||||
func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was string) *SetTitleOperation {
|
||||
return &SetTitleOperation{
|
||||
OpBase: newOpBase(SetTitleOp, author, unixTime),
|
||||
OpBase: dag.NewOpBase(SetTitleOp, author, unixTime),
|
||||
Title: title,
|
||||
Was: was,
|
||||
}
|
||||
@ -111,11 +79,11 @@ func (s SetTitleTimelineItem) Id() entity.Id {
|
||||
return s.id
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
func (s *SetTitleTimelineItem) IsAuthored() {}
|
||||
// IsAuthored is a sign post method for gqlgen
|
||||
func (s SetTitleTimelineItem) IsAuthored() {}
|
||||
|
||||
// Convenience function to apply the operation
|
||||
func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) {
|
||||
// SetTitle is a convenience function to change a bugs title
|
||||
func SetTitle(b Interface, author identity.Interface, unixTime int64, title string, metadata map[string]string) (*SetTitleOperation, error) {
|
||||
var lastTitleOp *SetTitleOperation
|
||||
for _, op := range b.Operations() {
|
||||
switch op := op.(type) {
|
||||
@ -131,12 +99,14 @@ func SetTitle(b Interface, author identity.Interface, unixTime int64, title stri
|
||||
was = b.FirstOp().(*CreateOperation).Title
|
||||
}
|
||||
|
||||
setTitleOp := NewSetTitleOp(author, unixTime, title, was)
|
||||
|
||||
if err := setTitleOp.Validate(); err != nil {
|
||||
op := NewSetTitleOp(author, unixTime, title, was)
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
if err := op.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.Append(setTitleOp)
|
||||
return setTitleOp, nil
|
||||
b.Append(op)
|
||||
return op, nil
|
||||
}
|
||||
|
@ -1,37 +1,14 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
func TestSetTitleSerialize(t *testing.T) {
|
||||
repo := repository.NewMockRepo()
|
||||
|
||||
rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
|
||||
require.NoError(t, err)
|
||||
|
||||
unix := time.Now().Unix()
|
||||
before := NewSetTitleOp(rene, unix, "title", "was")
|
||||
|
||||
data, err := json.Marshal(before)
|
||||
require.NoError(t, err)
|
||||
|
||||
var after SetTitleOperation
|
||||
err = json.Unmarshal(data, &after)
|
||||
require.NoError(t, err)
|
||||
|
||||
// enforce creating the ID
|
||||
before.Id()
|
||||
|
||||
// Replace the identity as it's not serialized
|
||||
after.Author_ = rene
|
||||
|
||||
require.Equal(t, before, &after)
|
||||
dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetTitleOperation {
|
||||
return NewSetTitleOp(author, unixTime, "title", "was")
|
||||
})
|
||||
}
|
||||
|
240
bug/operation.go
240
bug/operation.go
@ -1,23 +1,15 @@
|
||||
package bug
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
// OperationType is an operation type identifier
|
||||
type OperationType int
|
||||
|
||||
const (
|
||||
_ OperationType = iota
|
||||
_ dag.OperationType = iota
|
||||
CreateOp
|
||||
SetTitleOp
|
||||
AddCommentOp
|
||||
@ -32,55 +24,24 @@ const (
|
||||
type Operation interface {
|
||||
dag.Operation
|
||||
|
||||
// Type return the type of the operation
|
||||
Type() OperationType
|
||||
|
||||
// Time return the time when the operation was added
|
||||
Time() time.Time
|
||||
// Apply the operation to a Snapshot to create the final state
|
||||
Apply(snapshot *Snapshot)
|
||||
|
||||
// SetMetadata store arbitrary metadata about the operation
|
||||
SetMetadata(key string, value string)
|
||||
// GetMetadata retrieve arbitrary metadata about the operation
|
||||
GetMetadata(key string) (string, bool)
|
||||
// AllMetadata return all metadata for this operation
|
||||
AllMetadata() map[string]string
|
||||
|
||||
setExtraMetadataImmutable(key string, value string)
|
||||
}
|
||||
|
||||
func idOperation(op Operation, base *OpBase) entity.Id {
|
||||
if base.id == "" {
|
||||
// something went really wrong
|
||||
panic("op's id not set")
|
||||
}
|
||||
if base.id == entity.UnsetId {
|
||||
// This means we are trying to get the op's Id *before* it has been stored, for instance when
|
||||
// adding multiple ops in one go in an OperationPack.
|
||||
// As the Id is computed based on the actual bytes written on the disk, we are going to predict
|
||||
// those and then get the Id. This is safe as it will be the exact same code writing on disk later.
|
||||
// make sure that package external operations do conform to our interface
|
||||
var _ Operation = &dag.NoOpOperation[*Snapshot]{}
|
||||
var _ Operation = &dag.SetMetadataOperation[*Snapshot]{}
|
||||
|
||||
data, err := json.Marshal(op)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
base.id = entity.DeriveId(data)
|
||||
}
|
||||
return base.id
|
||||
}
|
||||
|
||||
func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
|
||||
func operationUnmarshaller(raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
|
||||
var t struct {
|
||||
OperationType OperationType `json:"type"`
|
||||
OperationType dag.OperationType `json:"type"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(raw, &t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var op Operation
|
||||
var op dag.Operation
|
||||
|
||||
switch t.OperationType {
|
||||
case AddCommentOp:
|
||||
@ -92,9 +53,9 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resol
|
||||
case LabelChangeOp:
|
||||
op = &LabelChangeOperation{}
|
||||
case NoOpOp:
|
||||
op = &NoOpOperation{}
|
||||
op = &dag.NoOpOperation[*Snapshot]{}
|
||||
case SetMetadataOp:
|
||||
op = &SetMetadataOperation{}
|
||||
op = &dag.SetMetadataOperation[*Snapshot]{}
|
||||
case SetStatusOp:
|
||||
op = &SetStatusOperation{}
|
||||
case SetTitleOp:
|
||||
@ -108,188 +69,5 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resol
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch op := op.(type) {
|
||||
case *AddCommentOperation:
|
||||
op.Author_ = author
|
||||
case *CreateOperation:
|
||||
op.Author_ = author
|
||||
case *EditCommentOperation:
|
||||
op.Author_ = author
|
||||
case *LabelChangeOperation:
|
||||
op.Author_ = author
|
||||
case *NoOpOperation:
|
||||
op.Author_ = author
|
||||
case *SetMetadataOperation:
|
||||
op.Author_ = author
|
||||
case *SetStatusOperation:
|
||||
op.Author_ = author
|
||||
case *SetTitleOperation:
|
||||
op.Author_ = author
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown operation type %T", op))
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
// OpBase implement the common code for all operations
|
||||
type OpBase struct {
|
||||
OperationType OperationType `json:"type"`
|
||||
Author_ identity.Interface `json:"-"` // not serialized
|
||||
// TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
|
||||
UnixTime int64 `json:"timestamp"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
|
||||
// mandatory random bytes to ensure a better randomness of the data used to later generate the ID
|
||||
// len(Nonce) should be > 20 and < 64 bytes
|
||||
// It has no functional purpose and should be ignored.
|
||||
Nonce []byte `json:"nonce"`
|
||||
|
||||
// Not serialized. Store the op's id in memory.
|
||||
id entity.Id
|
||||
// Not serialized. Store the extra metadata in memory,
|
||||
// compiled from SetMetadataOperation.
|
||||
extraMetadata map[string]string
|
||||
}
|
||||
|
||||
// newOpBase is the constructor for an OpBase
|
||||
func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
|
||||
return OpBase{
|
||||
OperationType: opType,
|
||||
Author_: author,
|
||||
UnixTime: unixTime,
|
||||
Nonce: makeNonce(20),
|
||||
id: entity.UnsetId,
|
||||
}
|
||||
}
|
||||
|
||||
func makeNonce(len int) []byte {
|
||||
result := make([]byte, len)
|
||||
_, err := rand.Read(result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (base *OpBase) UnmarshalJSON(data []byte) error {
|
||||
// Compute the Id when loading the op from disk.
|
||||
base.id = entity.DeriveId(data)
|
||||
|
||||
aux := struct {
|
||||
OperationType OperationType `json:"type"`
|
||||
UnixTime int64 `json:"timestamp"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Nonce []byte `json:"nonce"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base.OperationType = aux.OperationType
|
||||
base.UnixTime = aux.UnixTime
|
||||
base.Metadata = aux.Metadata
|
||||
base.Nonce = aux.Nonce
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (base *OpBase) Type() OperationType {
|
||||
return base.OperationType
|
||||
}
|
||||
|
||||
// Time return the time when the operation was added
|
||||
func (base *OpBase) Time() time.Time {
|
||||
return time.Unix(base.UnixTime, 0)
|
||||
}
|
||||
|
||||
// Validate check the OpBase for errors
|
||||
func (base *OpBase) Validate(op Operation, opType OperationType) error {
|
||||
if base.OperationType != opType {
|
||||
return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
|
||||
}
|
||||
|
||||
if op.Time().Unix() == 0 {
|
||||
return fmt.Errorf("time not set")
|
||||
}
|
||||
|
||||
if base.Author_ == nil {
|
||||
return fmt.Errorf("author not set")
|
||||
}
|
||||
|
||||
if err := op.Author().Validate(); err != nil {
|
||||
return errors.Wrap(err, "author")
|
||||
}
|
||||
|
||||
if op, ok := op.(dag.OperationWithFiles); ok {
|
||||
for _, hash := range op.GetFiles() {
|
||||
if !hash.IsValid() {
|
||||
return fmt.Errorf("file with invalid hash %v", hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(base.Nonce) > 64 {
|
||||
return fmt.Errorf("nonce is too big")
|
||||
}
|
||||
if len(base.Nonce) < 20 {
|
||||
return fmt.Errorf("nonce is too small")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMetadata store arbitrary metadata about the operation
|
||||
func (base *OpBase) SetMetadata(key string, value string) {
|
||||
if base.Metadata == nil {
|
||||
base.Metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
base.Metadata[key] = value
|
||||
base.id = entity.UnsetId
|
||||
}
|
||||
|
||||
// GetMetadata retrieve arbitrary metadata about the operation
|
||||
func (base *OpBase) GetMetadata(key string) (string, bool) {
|
||||
val, ok := base.Metadata[key]
|
||||
|
||||
if ok {
|
||||
return val, true
|
||||
}
|
||||
|
||||
// extraMetadata can't replace the original operations value if any
|
||||
val, ok = base.extraMetadata[key]
|
||||
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// AllMetadata return all metadata for this operation
|
||||
func (base *OpBase) AllMetadata() map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
for key, val := range base.extraMetadata {
|
||||
result[key] = val
|
||||
}
|
||||
|
||||
// Original metadata take precedence
|
||||
for key, val := range base.Metadata {
|
||||
result[key] = val
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
|
||||
if base.extraMetadata == nil {
|
||||
base.extraMetadata = make(map[string]string)
|
||||
}
|
||||
if _, exist := base.extraMetadata[key]; !exist {
|
||||
base.extraMetadata[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Author return author identity
|
||||
func (base *OpBase) Author() identity.Interface {
|
||||
return base.Author_
|
||||
}
|
||||
|
@ -6,10 +6,13 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
// TODO: move to entity/dag?
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
repo := repository.NewMockRepoClock()
|
||||
|
||||
@ -44,11 +47,7 @@ func TestValidate(t *testing.T) {
|
||||
NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus),
|
||||
NewSetStatusOp(makeIdentity(t, "René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus),
|
||||
NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus),
|
||||
&CreateOperation{OpBase: OpBase{
|
||||
Author_: rene,
|
||||
UnixTime: 0,
|
||||
OperationType: CreateOp,
|
||||
},
|
||||
&CreateOperation{OpBase: dag.NewOpBase(CreateOp, rene, 0),
|
||||
Title: "title",
|
||||
Message: "message",
|
||||
},
|
||||
@ -105,7 +104,7 @@ func TestID(t *testing.T) {
|
||||
err = rene.Commit(repo)
|
||||
require.NoError(t, err)
|
||||
|
||||
b, op, err := Create(rene, time.Now().Unix(), "title", "message")
|
||||
b, op, err := Create(rene, time.Now().Unix(), "title", "message", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
id1 := op.Id()
|
||||
|
@ -5,9 +5,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
)
|
||||
|
||||
var _ dag.Snapshot = &Snapshot{}
|
||||
|
||||
// Snapshot is a compiled form of the Bug data structure used for storage and merge
|
||||
type Snapshot struct {
|
||||
id entity.Id
|
||||
@ -23,7 +26,7 @@ type Snapshot struct {
|
||||
|
||||
Timeline []TimelineItem
|
||||
|
||||
Operations []Operation
|
||||
Operations []dag.Operation
|
||||
}
|
||||
|
||||
// Id returns the Bug identifier
|
||||
@ -35,6 +38,10 @@ func (snap *Snapshot) Id() entity.Id {
|
||||
return snap.id
|
||||
}
|
||||
|
||||
func (snap *Snapshot) AllOperations() []dag.Operation {
|
||||
return snap.Operations
|
||||
}
|
||||
|
||||
// EditTime returns the last time a bug was modified
|
||||
func (snap *Snapshot) EditTime() time.Time {
|
||||
if len(snap.Operations) == 0 {
|
||||
@ -133,5 +140,5 @@ func (snap *Snapshot) HasAnyActor(ids ...entity.Id) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Sign post method for gqlgen
|
||||
// IsAuthored is a sign post method for gqlgen
|
||||
func (snap *Snapshot) IsAuthored() {}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package bug
|
||||
|
||||
import "github.com/MichaelMure/git-bug/repository"
|
||||
import (
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
var _ Interface = &WithSnapshot{}
|
||||
|
||||
@ -10,11 +12,10 @@ type WithSnapshot struct {
|
||||
snap *Snapshot
|
||||
}
|
||||
|
||||
// Snapshot return the current snapshot
|
||||
func (b *WithSnapshot) Snapshot() *Snapshot {
|
||||
func (b *WithSnapshot) Compile() *Snapshot {
|
||||
if b.snap == nil {
|
||||
snap := b.Bug.Compile()
|
||||
b.snap = &snap
|
||||
b.snap = snap
|
||||
}
|
||||
return b.snap
|
||||
}
|
||||
|
105
cache/bug_cache.go
vendored
105
cache/bug_cache.go
vendored
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/MichaelMure/git-bug/bug"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
@ -33,7 +34,7 @@ func NewBugCache(repoCache *RepoCache, b *bug.Bug) *BugCache {
|
||||
func (c *BugCache) Snapshot() *bug.Snapshot {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.bug.Snapshot()
|
||||
return c.bug.Compile()
|
||||
}
|
||||
|
||||
func (c *BugCache) Id() entity.Id {
|
||||
@ -84,18 +85,11 @@ func (c *BugCache) AddCommentWithFiles(message string, files []repository.Hash)
|
||||
|
||||
func (c *BugCache) AddCommentRaw(author *IdentityCache, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*bug.AddCommentOperation, error) {
|
||||
c.mu.Lock()
|
||||
op, err := bug.AddCommentWithFiles(c.bug, author.Identity, unixTime, message, files)
|
||||
op, err := bug.AddComment(c.bug, author, unixTime, message, files, metadata)
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
@ -110,24 +104,12 @@ func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelCh
|
||||
|
||||
func (c *BugCache) ChangeLabelsRaw(author *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) {
|
||||
c.mu.Lock()
|
||||
changes, op, err := bug.ChangeLabels(c.bug, author.Identity, unixTime, added, removed)
|
||||
changes, op, err := bug.ChangeLabels(c.bug, author.Identity, unixTime, added, removed, metadata)
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return changes, nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
err = c.notifyUpdated()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return changes, op, nil
|
||||
return changes, op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
func (c *BugCache) ForceChangeLabels(added []string, removed []string) (*bug.LabelChangeOperation, error) {
|
||||
@ -141,23 +123,12 @@ func (c *BugCache) ForceChangeLabels(added []string, removed []string) (*bug.Lab
|
||||
|
||||
func (c *BugCache) ForceChangeLabelsRaw(author *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) (*bug.LabelChangeOperation, error) {
|
||||
c.mu.Lock()
|
||||
op, err := bug.ForceChangeLabels(c.bug, author.Identity, unixTime, added, removed)
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
op, err := bug.ForceChangeLabels(c.bug, author.Identity, unixTime, added, removed, metadata)
|
||||
c.mu.Unlock()
|
||||
err = c.notifyUpdated()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return op, nil
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
func (c *BugCache) Open() (*bug.SetStatusOperation, error) {
|
||||
@ -171,17 +142,11 @@ func (c *BugCache) Open() (*bug.SetStatusOperation, error) {
|
||||
|
||||
func (c *BugCache) OpenRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) {
|
||||
c.mu.Lock()
|
||||
op, err := bug.Open(c.bug, author.Identity, unixTime)
|
||||
op, err := bug.Open(c.bug, author.Identity, unixTime, metadata)
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
@ -196,17 +161,11 @@ func (c *BugCache) Close() (*bug.SetStatusOperation, error) {
|
||||
|
||||
func (c *BugCache) CloseRaw(author *IdentityCache, unixTime int64, metadata map[string]string) (*bug.SetStatusOperation, error) {
|
||||
c.mu.Lock()
|
||||
op, err := bug.Close(c.bug, author.Identity, unixTime)
|
||||
op, err := bug.Close(c.bug, author.Identity, unixTime, metadata)
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
@ -221,17 +180,11 @@ func (c *BugCache) SetTitle(title string) (*bug.SetTitleOperation, error) {
|
||||
|
||||
func (c *BugCache) SetTitleRaw(author *IdentityCache, unixTime int64, title string, metadata map[string]string) (*bug.SetTitleOperation, error) {
|
||||
c.mu.Lock()
|
||||
op, err := bug.SetTitle(c.bug, author.Identity, unixTime, title)
|
||||
op, err := bug.SetTitle(c.bug, author.Identity, unixTime, title, metadata)
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
@ -248,17 +201,11 @@ func (c *BugCache) EditCreateComment(body string) (*bug.EditCommentOperation, er
|
||||
// EditCreateCommentRaw is a convenience function to edit the body of a bug (the first comment)
|
||||
func (c *BugCache) EditCreateCommentRaw(author *IdentityCache, unixTime int64, body string, metadata map[string]string) (*bug.EditCommentOperation, error) {
|
||||
c.mu.Lock()
|
||||
op, err := bug.EditCreateComment(c.bug, author.Identity, unixTime, body)
|
||||
op, err := bug.EditCreateComment(c.bug, author.Identity, unixTime, body, nil, metadata)
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
@ -273,21 +220,15 @@ func (c *BugCache) EditComment(target entity.Id, message string) (*bug.EditComme
|
||||
|
||||
func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target entity.Id, message string, metadata map[string]string) (*bug.EditCommentOperation, error) {
|
||||
c.mu.Lock()
|
||||
op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message)
|
||||
op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message, nil, metadata)
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
func (c *BugCache) SetMetadata(target entity.Id, newMetadata map[string]string) (*bug.SetMetadataOperation, error) {
|
||||
func (c *BugCache) SetMetadata(target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*bug.Snapshot], error) {
|
||||
author, err := c.repoCache.GetUserIdentity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -296,15 +237,13 @@ func (c *BugCache) SetMetadata(target entity.Id, newMetadata map[string]string)
|
||||
return c.SetMetadataRaw(author, time.Now().Unix(), target, newMetadata)
|
||||
}
|
||||
|
||||
func (c *BugCache) SetMetadataRaw(author *IdentityCache, unixTime int64, target entity.Id, newMetadata map[string]string) (*bug.SetMetadataOperation, error) {
|
||||
func (c *BugCache) SetMetadataRaw(author *IdentityCache, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*bug.Snapshot], error) {
|
||||
c.mu.Lock()
|
||||
op, err := bug.SetMetadata(c.bug, author.Identity, unixTime, target, newMetadata)
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
return op, c.notifyUpdated()
|
||||
}
|
||||
|
||||
|
6
cache/repo_cache.go
vendored
6
cache/repo_cache.go
vendored
@ -209,9 +209,9 @@ func (c *RepoCache) buildCache() error {
|
||||
}
|
||||
|
||||
snap := b.Bug.Compile()
|
||||
c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
|
||||
c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, snap)
|
||||
|
||||
if err := c.addBugToSearchIndex(&snap); err != nil {
|
||||
if err := c.addBugToSearchIndex(snap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -222,7 +222,7 @@ func (c *RepoCache) buildCache() error {
|
||||
}
|
||||
|
||||
// repoIsAvailable check is the given repository is locked by a Cache.
|
||||
// Note: this is a smart function that will cleanup the lock file if the
|
||||
// Note: this is a smart function that will clean the lock file if the
|
||||
// corresponding process is not there anymore.
|
||||
// If no error is returned, the repo is free to edit.
|
||||
func repoIsAvailable(repo repository.RepoStorage) error {
|
||||
|
6
cache/repo_cache_bug.go
vendored
6
cache/repo_cache_bug.go
vendored
@ -461,15 +461,11 @@ func (c *RepoCache) NewBugWithFiles(title string, message string, files []reposi
|
||||
// well as metadata for the Create operation.
|
||||
// The new bug is written in the repository (commit)
|
||||
func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []repository.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) {
|
||||
b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files)
|
||||
b, op, err := bug.Create(author.Identity, unixTime, title, message, files, metadata)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for key, value := range metadata {
|
||||
op.SetMetadata(key, value)
|
||||
}
|
||||
|
||||
err = b.Commit(c.repo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
4
cache/repo_cache_common.go
vendored
4
cache/repo_cache_common.go
vendored
@ -36,7 +36,7 @@ func (c *RepoCache) Keyring() repository.Keyring {
|
||||
return c.repo.Keyring()
|
||||
}
|
||||
|
||||
// GetUserName returns the name the the user has used to configure git
|
||||
// GetUserName returns the name the user has used to configure git
|
||||
func (c *RepoCache) GetUserName() (string, error) {
|
||||
return c.repo.GetUserName()
|
||||
}
|
||||
@ -131,7 +131,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult {
|
||||
b := result.Entity.(*bug.Bug)
|
||||
snap := b.Compile()
|
||||
c.muBug.Lock()
|
||||
c.bugExcerpts[result.Id] = NewBugExcerpt(b, &snap)
|
||||
c.bugExcerpts[result.Id] = NewBugExcerpt(b, snap)
|
||||
c.muBug.Unlock()
|
||||
}
|
||||
}
|
||||
|
@ -18,78 +18,73 @@ import (
|
||||
Operations
|
||||
*/
|
||||
|
||||
type op1 struct {
|
||||
author identity.Interface
|
||||
const (
|
||||
_ OperationType = iota
|
||||
Op1
|
||||
Op2
|
||||
)
|
||||
|
||||
OperationType int `json:"type"`
|
||||
Field1 string `json:"field_1"`
|
||||
Files []repository.Hash `json:"files"`
|
||||
type op1 struct {
|
||||
OpBase
|
||||
Field1 string `json:"field_1"`
|
||||
Files []repository.Hash `json:"files"`
|
||||
}
|
||||
|
||||
func newOp1(author identity.Interface, field1 string, files ...repository.Hash) *op1 {
|
||||
return &op1{author: author, OperationType: 1, Field1: field1, Files: files}
|
||||
return &op1{OpBase: NewOpBase(Op1, author, 0), Field1: field1, Files: files}
|
||||
}
|
||||
|
||||
func (o *op1) Id() entity.Id {
|
||||
data, _ := json.Marshal(o)
|
||||
return entity.DeriveId(data)
|
||||
func (op *op1) Id() entity.Id {
|
||||
return IdOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
func (o *op1) Validate() error { return nil }
|
||||
func (op *op1) Validate() error { return nil }
|
||||
|
||||
func (o *op1) Author() identity.Interface {
|
||||
return o.author
|
||||
}
|
||||
|
||||
func (o *op1) GetFiles() []repository.Hash {
|
||||
return o.Files
|
||||
func (op *op1) GetFiles() []repository.Hash {
|
||||
return op.Files
|
||||
}
|
||||
|
||||
type op2 struct {
|
||||
author identity.Interface
|
||||
|
||||
OperationType int `json:"type"`
|
||||
Field2 string `json:"field_2"`
|
||||
OpBase
|
||||
Field2 string `json:"field_2"`
|
||||
}
|
||||
|
||||
func newOp2(author identity.Interface, field2 string) *op2 {
|
||||
return &op2{author: author, OperationType: 2, Field2: field2}
|
||||
return &op2{OpBase: NewOpBase(Op2, author, 0), Field2: field2}
|
||||
}
|
||||
|
||||
func (o *op2) Id() entity.Id {
|
||||
data, _ := json.Marshal(o)
|
||||
return entity.DeriveId(data)
|
||||
func (op *op2) Id() entity.Id {
|
||||
return IdOperation(op, &op.OpBase)
|
||||
}
|
||||
|
||||
func (o *op2) Validate() error { return nil }
|
||||
func (op *op2) Validate() error { return nil }
|
||||
|
||||
func (o *op2) Author() identity.Interface {
|
||||
return o.author
|
||||
}
|
||||
|
||||
func unmarshaler(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (Operation, error) {
|
||||
func unmarshaler(raw json.RawMessage, resolver identity.Resolver) (Operation, error) {
|
||||
var t struct {
|
||||
OperationType int `json:"type"`
|
||||
OperationType OperationType `json:"type"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(raw, &t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var op Operation
|
||||
|
||||
switch t.OperationType {
|
||||
case 1:
|
||||
op := &op1{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
op.author = author
|
||||
return op, err
|
||||
case 2:
|
||||
op := &op2{}
|
||||
err := json.Unmarshal(raw, &op)
|
||||
op.author = author
|
||||
return op, err
|
||||
case Op1:
|
||||
op = &op1{}
|
||||
case Op2:
|
||||
op = &op2{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operation type %v", t.OperationType)
|
||||
}
|
||||
|
||||
err := json.Unmarshal(raw, &op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -26,7 +26,7 @@ type Definition struct {
|
||||
// the Namespace in git references (bugs, prs, ...)
|
||||
Namespace string
|
||||
// a function decoding a JSON message into an Operation
|
||||
OperationUnmarshaler func(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (Operation, error)
|
||||
OperationUnmarshaler func(raw json.RawMessage, resolver identity.Resolver) (Operation, error)
|
||||
// the expected format version number, that can be used for data migration/upgrade
|
||||
FormatVersion uint
|
||||
}
|
||||
|
@ -50,6 +50,8 @@ func TestWriteReadMultipleAuthor(t *testing.T) {
|
||||
}
|
||||
|
||||
func assertEqualEntities(t *testing.T, a, b *Entity) {
|
||||
t.Helper()
|
||||
|
||||
// testify doesn't support comparing functions and systematically fail if they are not nil
|
||||
// so we have to set them to nil temporarily
|
||||
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/entity/dag"
|
||||
@ -64,10 +65,8 @@ type Operation interface {
|
||||
Apply(snapshot *Snapshot)
|
||||
}
|
||||
|
||||
type OperationType int
|
||||
|
||||
const (
|
||||
_ OperationType = iota
|
||||
_ dag.OperationType = iota
|
||||
SetSignatureRequiredOp
|
||||
AddAdministratorOp
|
||||
RemoveAdministratorOp
|
||||
@ -75,37 +74,30 @@ const (
|
||||
|
||||
// SetSignatureRequired is an operation to set/unset if git signature are required.
|
||||
type SetSignatureRequired struct {
|
||||
author identity.Interface
|
||||
OperationType OperationType `json:"type"`
|
||||
Value bool `json:"value"`
|
||||
dag.OpBase
|
||||
Value bool `json:"value"`
|
||||
}
|
||||
|
||||
func NewSetSignatureRequired(author identity.Interface, value bool) *SetSignatureRequired {
|
||||
return &SetSignatureRequired{author: author, OperationType: SetSignatureRequiredOp, Value: value}
|
||||
return &SetSignatureRequired{
|
||||
OpBase: dag.NewOpBase(SetSignatureRequiredOp, author, time.Now().Unix()),
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (ssr *SetSignatureRequired) Id() entity.Id {
|
||||
// the Id of the operation is the hash of the serialized data.
|
||||
// we could memorize the Id when deserializing, but that will do
|
||||
data, _ := json.Marshal(ssr)
|
||||
return entity.DeriveId(data)
|
||||
return dag.IdOperation(ssr, &ssr.OpBase)
|
||||
}
|
||||
|
||||
func (ssr *SetSignatureRequired) Validate() error {
|
||||
if ssr.author == nil {
|
||||
return fmt.Errorf("author not set")
|
||||
}
|
||||
return ssr.author.Validate()
|
||||
}
|
||||
|
||||
func (ssr *SetSignatureRequired) Author() identity.Interface {
|
||||
return ssr.author
|
||||
return ssr.OpBase.Validate(ssr, SetSignatureRequiredOp)
|
||||
}
|
||||
|
||||
// Apply is the function that makes changes on the snapshot
|
||||
func (ssr *SetSignatureRequired) Apply(snapshot *Snapshot) {
|
||||
// check that we are allowed to change the config
|
||||
if _, ok := snapshot.Administrator[ssr.author]; !ok {
|
||||
if _, ok := snapshot.Administrator[ssr.Author()]; !ok {
|
||||
return
|
||||
}
|
||||
snapshot.SignatureRequired = ssr.Value
|
||||
@ -113,24 +105,20 @@ func (ssr *SetSignatureRequired) Apply(snapshot *Snapshot) {
|
||||
|
||||
// AddAdministrator is an operation to add a new administrator in the set
|
||||
type AddAdministrator struct {
|
||||
author identity.Interface
|
||||
OperationType OperationType `json:"type"`
|
||||
ToAdd []identity.Interface `json:"to_add"`
|
||||
}
|
||||
|
||||
// addAdministratorJson is a helper struct to deserialize identities with a concrete type.
|
||||
type addAdministratorJson struct {
|
||||
ToAdd []identity.IdentityStub `json:"to_add"`
|
||||
dag.OpBase
|
||||
ToAdd []identity.Interface `json:"to_add"`
|
||||
}
|
||||
|
||||
func NewAddAdministratorOp(author identity.Interface, toAdd ...identity.Interface) *AddAdministrator {
|
||||
return &AddAdministrator{author: author, OperationType: AddAdministratorOp, ToAdd: toAdd}
|
||||
return &AddAdministrator{
|
||||
OpBase: dag.NewOpBase(AddAdministratorOp, author, time.Now().Unix()),
|
||||
ToAdd: toAdd,
|
||||
}
|
||||
}
|
||||
|
||||
func (aa *AddAdministrator) Id() entity.Id {
|
||||
// we could memorize the Id when deserializing, but that will do
|
||||
data, _ := json.Marshal(aa)
|
||||
return entity.DeriveId(data)
|
||||
// the Id of the operation is the hash of the serialized data.
|
||||
return dag.IdOperation(aa, &aa.OpBase)
|
||||
}
|
||||
|
||||
func (aa *AddAdministrator) Validate() error {
|
||||
@ -138,20 +126,13 @@ func (aa *AddAdministrator) Validate() error {
|
||||
if len(aa.ToAdd) == 0 {
|
||||
return fmt.Errorf("nothing to add")
|
||||
}
|
||||
if aa.author == nil {
|
||||
return fmt.Errorf("author not set")
|
||||
}
|
||||
return aa.author.Validate()
|
||||
}
|
||||
|
||||
func (aa *AddAdministrator) Author() identity.Interface {
|
||||
return aa.author
|
||||
return aa.OpBase.Validate(aa, AddAdministratorOp)
|
||||
}
|
||||
|
||||
// Apply is the function that makes changes on the snapshot
|
||||
func (aa *AddAdministrator) Apply(snapshot *Snapshot) {
|
||||
// check that we are allowed to change the config ... or if there is no admin yet
|
||||
if !snapshot.HasAdministrator(aa.author) && len(snapshot.Administrator) != 0 {
|
||||
if !snapshot.HasAdministrator(aa.Author()) && len(snapshot.Administrator) != 0 {
|
||||
return
|
||||
}
|
||||
for _, toAdd := range aa.ToAdd {
|
||||
@ -161,25 +142,20 @@ func (aa *AddAdministrator) Apply(snapshot *Snapshot) {
|
||||
|
||||
// RemoveAdministrator is an operation to remove an administrator from the set
|
||||
type RemoveAdministrator struct {
|
||||
author identity.Interface
|
||||
OperationType OperationType `json:"type"`
|
||||
ToRemove []identity.Interface `json:"to_remove"`
|
||||
}
|
||||
|
||||
// removeAdministratorJson is a helper struct to deserialize identities with a concrete type.
|
||||
type removeAdministratorJson struct {
|
||||
dag.OpBase
|
||||
ToRemove []identity.Interface `json:"to_remove"`
|
||||
}
|
||||
|
||||
func NewRemoveAdministratorOp(author identity.Interface, toRemove ...identity.Interface) *RemoveAdministrator {
|
||||
return &RemoveAdministrator{author: author, OperationType: RemoveAdministratorOp, ToRemove: toRemove}
|
||||
return &RemoveAdministrator{
|
||||
OpBase: dag.NewOpBase(RemoveAdministratorOp, author, time.Now().Unix()),
|
||||
ToRemove: toRemove,
|
||||
}
|
||||
}
|
||||
|
||||
func (ra *RemoveAdministrator) Id() entity.Id {
|
||||
// the Id of the operation is the hash of the serialized data.
|
||||
// we could memorize the Id when deserializing, but that will do
|
||||
data, _ := json.Marshal(ra)
|
||||
return entity.DeriveId(data)
|
||||
return dag.IdOperation(ra, &ra.OpBase)
|
||||
}
|
||||
|
||||
func (ra *RemoveAdministrator) Validate() error {
|
||||
@ -188,26 +164,19 @@ func (ra *RemoveAdministrator) Validate() error {
|
||||
if len(ra.ToRemove) == 0 {
|
||||
return fmt.Errorf("nothing to remove")
|
||||
}
|
||||
if ra.author == nil {
|
||||
return fmt.Errorf("author not set")
|
||||
}
|
||||
return ra.author.Validate()
|
||||
}
|
||||
|
||||
func (ra *RemoveAdministrator) Author() identity.Interface {
|
||||
return ra.author
|
||||
return ra.OpBase.Validate(ra, RemoveAdministratorOp)
|
||||
}
|
||||
|
||||
// Apply is the function that makes changes on the snapshot
|
||||
func (ra *RemoveAdministrator) Apply(snapshot *Snapshot) {
|
||||
// check if we are allowed to make changes
|
||||
if !snapshot.HasAdministrator(ra.author) {
|
||||
if !snapshot.HasAdministrator(ra.Author()) {
|
||||
return
|
||||
}
|
||||
// special rule: we can't end up with no administrator
|
||||
stillSome := false
|
||||
for admin, _ := range snapshot.Administrator {
|
||||
if admin != ra.author {
|
||||
if admin != ra.Author() {
|
||||
stillSome = true
|
||||
break
|
||||
}
|
||||
@ -245,71 +214,52 @@ var def = dag.Definition{
|
||||
|
||||
// operationUnmarshaller is a function doing the de-serialization of the JSON data into our own
|
||||
// concrete Operations. If needed, we can use the resolver to connect to other entities.
|
||||
func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
|
||||
func operationUnmarshaller(raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
|
||||
var t struct {
|
||||
OperationType OperationType `json:"type"`
|
||||
OperationType dag.OperationType `json:"type"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(raw, &t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var value interface{}
|
||||
var op dag.Operation
|
||||
|
||||
switch t.OperationType {
|
||||
case AddAdministratorOp:
|
||||
value = &addAdministratorJson{}
|
||||
op = &AddAdministrator{}
|
||||
case RemoveAdministratorOp:
|
||||
value = &removeAdministratorJson{}
|
||||
op = &RemoveAdministrator{}
|
||||
case SetSignatureRequiredOp:
|
||||
value = &SetSignatureRequired{}
|
||||
op = &SetSignatureRequired{}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
|
||||
}
|
||||
|
||||
err := json.Unmarshal(raw, &value)
|
||||
err := json.Unmarshal(raw, &op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var op Operation
|
||||
|
||||
switch value := value.(type) {
|
||||
case *SetSignatureRequired:
|
||||
value.author = author
|
||||
op = value
|
||||
case *addAdministratorJson:
|
||||
// We need something less straightforward to deserialize and resolve identities
|
||||
aa := &AddAdministrator{
|
||||
author: author,
|
||||
OperationType: AddAdministratorOp,
|
||||
ToAdd: make([]identity.Interface, len(value.ToAdd)),
|
||||
}
|
||||
for i, stub := range value.ToAdd {
|
||||
switch op := op.(type) {
|
||||
case *AddAdministrator:
|
||||
// We need to resolve identities
|
||||
for i, stub := range op.ToAdd {
|
||||
iden, err := resolver.ResolveIdentity(stub.Id())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aa.ToAdd[i] = iden
|
||||
op.ToAdd[i] = iden
|
||||
}
|
||||
op = aa
|
||||
case *removeAdministratorJson:
|
||||
// We need something less straightforward to deserialize and resolve identities
|
||||
ra := &RemoveAdministrator{
|
||||
author: author,
|
||||
OperationType: RemoveAdministratorOp,
|
||||
ToRemove: make([]identity.Interface, len(value.ToRemove)),
|
||||
}
|
||||
for i, stub := range value.ToRemove {
|
||||
case *RemoveAdministrator:
|
||||
// We need to resolve identities
|
||||
for i, stub := range op.ToRemove {
|
||||
iden, err := resolver.ResolveIdentity(stub.Id())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ra.ToRemove[i] = iden
|
||||
op.ToRemove[i] = iden
|
||||
}
|
||||
op = ra
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown operation type %T", value))
|
||||
}
|
||||
|
||||
return op, nil
|
||||
|
39
entity/dag/op_noop.go
Normal file
39
entity/dag/op_noop.go
Normal 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() {}
|
13
entity/dag/op_noop_test.go
Normal file
13
entity/dag/op_noop_test.go
Normal 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)
|
||||
})
|
||||
}
|
68
entity/dag/op_set_metadata.go
Normal file
68
entity/dag/op_set_metadata.go
Normal 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() {}
|
106
entity/dag/op_set_metadata_test.go
Normal file
106
entity/dag/op_set_metadata_test.go
Normal 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",
|
||||
})
|
||||
})
|
||||
}
|
@ -1,11 +1,21 @@
|
||||
package dag
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/identity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
// OperationType is an operation type identifier
|
||||
type OperationType int
|
||||
|
||||
// Operation is a piece of data defining a change to reflect on the state of an Entity.
|
||||
// What this Operation or Entity's state looks like is not of the resort of this package as it only deals with the
|
||||
// data structure and storage.
|
||||
@ -22,23 +32,39 @@ type Operation interface {
|
||||
// a minimal amount of entropy and avoid collision.
|
||||
//
|
||||
// Author's note: I tried to find a clever way around that inelegance (stuffing random useless data into the stored
|
||||
// structure is not exactly elegant) but I failed to find a proper way. Essentially, anything that would reuse some
|
||||
// structure is not exactly elegant), but I failed to find a proper way. Essentially, anything that would reuse some
|
||||
// other data (parent operation's Id, lamport clock) or the graph structure (depth) impose that the Id would only
|
||||
// make sense in the context of the graph and yield some deep coupling between Entity and Operation. This in turn
|
||||
// make the whole thing even less elegant.
|
||||
//
|
||||
// A common way to derive an Id will be to use the entity.DeriveId() function on the serialized operation data.
|
||||
Id() entity.Id
|
||||
// Type return the type of the operation
|
||||
Type() OperationType
|
||||
// Validate check if the Operation data is valid
|
||||
Validate() error
|
||||
// Author returns the author of this operation
|
||||
Author() identity.Interface
|
||||
// Time return the time when the operation was added
|
||||
Time() time.Time
|
||||
|
||||
// SetMetadata store arbitrary metadata about the operation
|
||||
SetMetadata(key string, value string)
|
||||
// GetMetadata retrieve arbitrary metadata about the operation
|
||||
GetMetadata(key string) (string, bool)
|
||||
// AllMetadata return all metadata for this operation
|
||||
AllMetadata() map[string]string
|
||||
|
||||
// setId allow to set the Id, used when unmarshalling only
|
||||
setId(id entity.Id)
|
||||
// setAuthor allow to set the author, used when unmarshalling only
|
||||
setAuthor(author identity.Interface)
|
||||
// setExtraMetadataImmutable add a metadata not carried by the operation itself on the operation
|
||||
setExtraMetadataImmutable(key string, value string)
|
||||
}
|
||||
|
||||
// OperationWithFiles is an extended Operation that has files dependency, stored in git.
|
||||
// OperationWithFiles is an optional extension for an Operation that has files dependency, stored in git.
|
||||
type OperationWithFiles interface {
|
||||
Operation
|
||||
|
||||
// GetFiles return the files needed by this operation
|
||||
// This implies that the Operation maintain and store internally the references to those files. This is how
|
||||
// this information is read later, when loading from storage.
|
||||
@ -46,3 +72,201 @@ type OperationWithFiles interface {
|
||||
// hash).
|
||||
GetFiles() []repository.Hash
|
||||
}
|
||||
|
||||
// OperationDoesntChangeSnapshot is an interface signaling that the Operation implementing it doesn't change the
|
||||
// snapshot, for example a metadata operation that act on other operations.
|
||||
type OperationDoesntChangeSnapshot interface {
|
||||
DoesntChangeSnapshot()
|
||||
}
|
||||
|
||||
// Snapshot is the minimal interface that a snapshot need to implement
|
||||
type Snapshot interface {
|
||||
// AllOperations returns all the operations that have been applied to that snapshot, in order
|
||||
AllOperations() []Operation
|
||||
}
|
||||
|
||||
// OpBase implement the common feature that every Operation should support.
|
||||
type OpBase struct {
|
||||
// Not serialized. Store the op's id in memory.
|
||||
id entity.Id
|
||||
// Not serialized
|
||||
author identity.Interface
|
||||
|
||||
OperationType OperationType `json:"type"`
|
||||
UnixTime int64 `json:"timestamp"`
|
||||
|
||||
// mandatory random bytes to ensure a better randomness of the data used to later generate the ID
|
||||
// len(Nonce) should be > 20 and < 64 bytes
|
||||
// It has no functional purpose and should be ignored.
|
||||
Nonce []byte `json:"nonce"`
|
||||
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
// Not serialized. Store the extra metadata in memory,
|
||||
// compiled from SetMetadataOperation.
|
||||
extraMetadata map[string]string
|
||||
}
|
||||
|
||||
func NewOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
|
||||
return OpBase{
|
||||
OperationType: opType,
|
||||
author: author,
|
||||
UnixTime: unixTime,
|
||||
Nonce: makeNonce(20),
|
||||
id: entity.UnsetId,
|
||||
}
|
||||
}
|
||||
|
||||
func makeNonce(len int) []byte {
|
||||
result := make([]byte, len)
|
||||
_, err := rand.Read(result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func IdOperation(op Operation, base *OpBase) entity.Id {
|
||||
if base.id == "" {
|
||||
// something went really wrong
|
||||
panic("op's id not set")
|
||||
}
|
||||
if base.id == entity.UnsetId {
|
||||
// This means we are trying to get the op's Id *before* it has been stored, for instance when
|
||||
// adding multiple ops in one go in an OperationPack.
|
||||
// As the Id is computed based on the actual bytes written on the disk, we are going to predict
|
||||
// those and then get the Id. This is safe as it will be the exact same code writing on disk later.
|
||||
|
||||
data, err := json.Marshal(op)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
base.id = entity.DeriveId(data)
|
||||
}
|
||||
return base.id
|
||||
}
|
||||
|
||||
func (base *OpBase) Type() OperationType {
|
||||
return base.OperationType
|
||||
}
|
||||
|
||||
// Time return the time when the operation was added
|
||||
func (base *OpBase) Time() time.Time {
|
||||
return time.Unix(base.UnixTime, 0)
|
||||
}
|
||||
|
||||
// Validate check the OpBase for errors
|
||||
func (base *OpBase) Validate(op Operation, opType OperationType) error {
|
||||
if base.OperationType == 0 {
|
||||
return fmt.Errorf("operation type unset")
|
||||
}
|
||||
if base.OperationType != opType {
|
||||
return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
|
||||
}
|
||||
|
||||
if op.Time().Unix() == 0 {
|
||||
return fmt.Errorf("time not set")
|
||||
}
|
||||
|
||||
if base.author == nil {
|
||||
return fmt.Errorf("author not set")
|
||||
}
|
||||
|
||||
if err := op.Author().Validate(); err != nil {
|
||||
return errors.Wrap(err, "author")
|
||||
}
|
||||
|
||||
if op, ok := op.(OperationWithFiles); ok {
|
||||
for _, hash := range op.GetFiles() {
|
||||
if !hash.IsValid() {
|
||||
return fmt.Errorf("file with invalid hash %v", hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(base.Nonce) > 64 {
|
||||
return fmt.Errorf("nonce is too big")
|
||||
}
|
||||
if len(base.Nonce) < 20 {
|
||||
return fmt.Errorf("nonce is too small")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAuthored is a sign post method for gqlgen
|
||||
func (base *OpBase) IsAuthored() {}
|
||||
|
||||
// Author return author identity
|
||||
func (base *OpBase) Author() identity.Interface {
|
||||
return base.author
|
||||
}
|
||||
|
||||
// IdIsSet returns true if the id has been set already
|
||||
func (base *OpBase) IdIsSet() bool {
|
||||
return base.id != "" && base.id != entity.UnsetId
|
||||
}
|
||||
|
||||
// SetMetadata store arbitrary metadata about the operation
|
||||
func (base *OpBase) SetMetadata(key string, value string) {
|
||||
if base.IdIsSet() {
|
||||
panic("set metadata on an operation with already an Id")
|
||||
}
|
||||
|
||||
if base.Metadata == nil {
|
||||
base.Metadata = make(map[string]string)
|
||||
}
|
||||
base.Metadata[key] = value
|
||||
}
|
||||
|
||||
// GetMetadata retrieve arbitrary metadata about the operation
|
||||
func (base *OpBase) GetMetadata(key string) (string, bool) {
|
||||
val, ok := base.Metadata[key]
|
||||
|
||||
if ok {
|
||||
return val, true
|
||||
}
|
||||
|
||||
// extraMetadata can't replace the original operations value if any
|
||||
val, ok = base.extraMetadata[key]
|
||||
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// AllMetadata return all metadata for this operation
|
||||
func (base *OpBase) AllMetadata() map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
for key, val := range base.extraMetadata {
|
||||
result[key] = val
|
||||
}
|
||||
|
||||
// Original metadata take precedence
|
||||
for key, val := range base.Metadata {
|
||||
result[key] = val
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// setId allow to set the Id, used when unmarshalling only
|
||||
func (base *OpBase) setId(id entity.Id) {
|
||||
if base.id != "" && base.id != entity.UnsetId {
|
||||
panic("trying to set id again")
|
||||
}
|
||||
base.id = id
|
||||
}
|
||||
|
||||
// setAuthor allow to set the author, used when unmarshalling only
|
||||
func (base *OpBase) setAuthor(author identity.Interface) {
|
||||
base.author = author
|
||||
}
|
||||
|
||||
func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
|
||||
if base.extraMetadata == nil {
|
||||
base.extraMetadata = make(map[string]string)
|
||||
}
|
||||
if _, exist := base.extraMetadata[key]; !exist {
|
||||
base.extraMetadata[key] = value
|
||||
}
|
||||
}
|
||||
|
@ -314,10 +314,15 @@ func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([]
|
||||
|
||||
for _, raw := range aux.Operations {
|
||||
// delegate to specialized unmarshal function
|
||||
op, err := def.OperationUnmarshaler(author, raw, resolver)
|
||||
op, err := def.OperationUnmarshaler(raw, resolver)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// Set the id from the serialized data
|
||||
op.setId(entity.DeriveId(raw))
|
||||
// Set the author, taken from the OperationPack
|
||||
op.setAuthor(author)
|
||||
|
||||
ops = append(ops, op)
|
||||
}
|
||||
|
||||
|
@ -11,13 +11,13 @@ import (
|
||||
)
|
||||
|
||||
func TestOperationPackReadWrite(t *testing.T) {
|
||||
repo, id1, _, resolver, def := makeTestContext()
|
||||
repo, author, _, resolver, def := makeTestContext()
|
||||
|
||||
opp := &operationPack{
|
||||
Author: id1,
|
||||
Author: author,
|
||||
Operations: []Operation{
|
||||
newOp1(id1, "foo"),
|
||||
newOp2(id1, "bar"),
|
||||
newOp1(author, "foo"),
|
||||
newOp2(author, "bar"),
|
||||
},
|
||||
CreateTime: 123,
|
||||
EditTime: 456,
|
||||
@ -32,34 +32,26 @@ func TestOperationPackReadWrite(t *testing.T) {
|
||||
opp2, err := readOperationPack(def, repo, resolver, commit)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, opp, opp2)
|
||||
|
||||
// make sure we get the same Id with the same data
|
||||
opp3 := &operationPack{
|
||||
Author: id1,
|
||||
Operations: []Operation{
|
||||
newOp1(id1, "foo"),
|
||||
newOp2(id1, "bar"),
|
||||
},
|
||||
CreateTime: 123,
|
||||
EditTime: 456,
|
||||
for _, op := range opp.Operations {
|
||||
// force the creation of the id
|
||||
op.Id()
|
||||
}
|
||||
require.Equal(t, opp.Id(), opp3.Id())
|
||||
require.Equal(t, opp, opp2)
|
||||
}
|
||||
|
||||
func TestOperationPackSignedReadWrite(t *testing.T) {
|
||||
repo, id1, _, resolver, def := makeTestContext()
|
||||
repo, author, _, resolver, def := makeTestContext()
|
||||
|
||||
err := id1.(*identity.Identity).Mutate(repo, func(orig *identity.Mutator) {
|
||||
err := author.(*identity.Identity).Mutate(repo, func(orig *identity.Mutator) {
|
||||
orig.Keys = append(orig.Keys, identity.GenerateKey())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
opp := &operationPack{
|
||||
Author: id1,
|
||||
Author: author,
|
||||
Operations: []Operation{
|
||||
newOp1(id1, "foo"),
|
||||
newOp2(id1, "bar"),
|
||||
newOp1(author, "foo"),
|
||||
newOp2(author, "bar"),
|
||||
},
|
||||
CreateTime: 123,
|
||||
EditTime: 456,
|
||||
@ -74,23 +66,15 @@ func TestOperationPackSignedReadWrite(t *testing.T) {
|
||||
opp2, err := readOperationPack(def, repo, resolver, commit)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, opp, opp2)
|
||||
|
||||
// make sure we get the same Id with the same data
|
||||
opp3 := &operationPack{
|
||||
Author: id1,
|
||||
Operations: []Operation{
|
||||
newOp1(id1, "foo"),
|
||||
newOp2(id1, "bar"),
|
||||
},
|
||||
CreateTime: 123,
|
||||
EditTime: 456,
|
||||
for _, op := range opp.Operations {
|
||||
// force the creation of the id
|
||||
op.Id()
|
||||
}
|
||||
require.Equal(t, opp.Id(), opp3.Id())
|
||||
require.Equal(t, opp, opp2)
|
||||
}
|
||||
|
||||
func TestOperationPackFiles(t *testing.T) {
|
||||
repo, id1, _, resolver, def := makeTestContext()
|
||||
repo, author, _, resolver, def := makeTestContext()
|
||||
|
||||
blobHash1, err := repo.StoreData(randomData())
|
||||
require.NoError(t, err)
|
||||
@ -99,10 +83,10 @@ func TestOperationPackFiles(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
opp := &operationPack{
|
||||
Author: id1,
|
||||
Author: author,
|
||||
Operations: []Operation{
|
||||
newOp1(id1, "foo", blobHash1, blobHash2),
|
||||
newOp1(id1, "foo", blobHash2),
|
||||
newOp1(author, "foo", blobHash1, blobHash2),
|
||||
newOp1(author, "foo", blobHash2),
|
||||
},
|
||||
CreateTime: 123,
|
||||
EditTime: 456,
|
||||
@ -117,6 +101,10 @@ func TestOperationPackFiles(t *testing.T) {
|
||||
opp2, err := readOperationPack(def, repo, resolver, commit)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, op := range opp.Operations {
|
||||
// force the creation of the id
|
||||
op.Id()
|
||||
}
|
||||
require.Equal(t, opp, opp2)
|
||||
|
||||
require.ElementsMatch(t, opp2.Operations[0].(OperationWithFiles).GetFiles(), []repository.Hash{
|
||||
|
57
entity/dag/operation_testing.go
Normal file
57
entity/dag/operation_testing.go
Normal 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
78
go.mod
@ -1,17 +1,16 @@
|
||||
module github.com/MichaelMure/git-bug
|
||||
|
||||
go 1.16
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.1
|
||||
github.com/99designs/gqlgen v0.17.13
|
||||
github.com/99designs/keyring v1.2.1
|
||||
github.com/MichaelMure/go-term-text v0.3.1
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7
|
||||
github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195
|
||||
github.com/awesome-gocui/gocui v1.1.0
|
||||
github.com/blevesearch/bleve v1.0.14
|
||||
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9
|
||||
github.com/corpix/uarand v0.1.1 // indirect
|
||||
github.com/cheekybits/genny v1.0.0
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/go-git/go-billy/v5 v5.3.1
|
||||
@ -23,11 +22,10 @@ require (
|
||||
github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/stretchr/testify v1.7.2
|
||||
github.com/vektah/gqlparser/v2 v2.4.1
|
||||
github.com/vektah/gqlparser/v2 v2.4.6
|
||||
github.com/xanzy/go-gitlab v0.68.0
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||
@ -35,3 +33,71 @@ require (
|
||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a
|
||||
golang.org/x/text v0.3.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.16 // indirect
|
||||
github.com/RoaringBitmap/roaring v0.4.23 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.2 // indirect
|
||||
github.com/blevesearch/segment v0.9.0 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/zap/v11 v11.0.14 // indirect
|
||||
github.com/blevesearch/zap/v12 v12.0.14 // indirect
|
||||
github.com/blevesearch/zap/v13 v13.0.6 // indirect
|
||||
github.com/blevesearch/zap/v14 v14.0.5 // indirect
|
||||
github.com/blevesearch/zap/v15 v15.0.3 // indirect
|
||||
github.com/corpix/uarand v0.1.1 // indirect
|
||||
github.com/couchbase/vellum v1.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.4.0 // indirect
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.12 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.3.1 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
github.com/philhofer/fwd v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.1.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/steveyen/gtreap v0.1.0 // indirect
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/tinylib/msgp v1.1.0 // indirect
|
||||
github.com/willf/bitset v1.1.10 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.5 // indirect
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
51
go.sum
51
go.sum
@ -33,11 +33,12 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
|
||||
github.com/99designs/gqlgen v0.17.1 h1:i2qQMPKHQjHgBWYIpO4TsaQpPqMHCPK1+h95ipvH8VU=
|
||||
github.com/99designs/gqlgen v0.17.1/go.mod h1:K5fzLKwtph+FFgh9j7nFbRUdBKvTcGnsta51fsMTn3o=
|
||||
github.com/99designs/gqlgen v0.17.13 h1:ETUEqvRg5Zvr1lXtpoRdj026fzVay0ZlJPwI33qXLIw=
|
||||
github.com/99designs/gqlgen v0.17.13/go.mod h1:w1brbeOdqVyNJI553BGwtwdVcYu1LKeYE1opLWN9RgQ=
|
||||
github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o=
|
||||
github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic=
|
||||
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
@ -54,8 +55,8 @@ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06
|
||||
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
|
||||
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
|
||||
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
@ -101,8 +102,8 @@ github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9L
|
||||
github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY=
|
||||
github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 h1:a1zrFsLFac2xoM6zG1u72DWJwZG3ayttYLfmLbxVETk=
|
||||
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
@ -118,9 +119,7 @@ github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiG
|
||||
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
|
||||
github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw=
|
||||
github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@ -236,8 +235,8 @@ github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKp
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
@ -289,11 +288,10 @@ github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
||||
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
||||
github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
@ -303,8 +301,8 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.2.3 h1:f/MjBEBDLttYCGfRaKBbKSRVF5aV2O6fnBpzknuE3jU=
|
||||
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA=
|
||||
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
@ -331,9 +329,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
@ -342,7 +338,6 @@ github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7 h1:Vk3RiBQpF0Ja+
|
||||
github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e h1:VAzdS5Nw68fbf5RZ8RDVlUvPXNU6Z3jtPCK/qvm4FoQ=
|
||||
github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
@ -366,6 +361,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
@ -376,10 +372,9 @@ github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7
|
||||
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vektah/gqlparser/v2 v2.4.1 h1:QOyEn8DAPMUMARGMeshKDkDgNmVoEaEGiDB0uWxcSlQ=
|
||||
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
|
||||
github.com/vektah/gqlparser/v2 v2.4.6 h1:Yjzp66g6oVq93Jihbi0qhGnf/6zIWjcm8H6gA27zstE=
|
||||
github.com/vektah/gqlparser/v2 v2.4.6/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/xanzy/go-gitlab v0.68.0 h1:b2iMQHgZ1V+NyRqLRJVv6RFfr4xnd/AASeS/PETYL0Y=
|
||||
@ -387,6 +382,7 @@ github.com/xanzy/go-gitlab v0.68.0/go.mod h1:o4yExCtdaqlM8YGdDJWuZoBmfxBsmA9TPEj
|
||||
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@ -407,6 +403,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -440,6 +437,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -470,7 +468,6 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
@ -497,7 +494,6 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -532,6 +528,7 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE=
|
||||
@ -592,9 +589,9 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -693,14 +690,12 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -87,6 +87,7 @@ func generateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug {
|
||||
time.Now().Unix(),
|
||||
fake.Sentence(),
|
||||
paragraphs(),
|
||||
nil, nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@ -143,19 +144,19 @@ func paragraphs() string {
|
||||
}
|
||||
|
||||
func comment(b bug.Interface, p identity.Interface, timestamp int64) {
|
||||
_, _ = bug.AddComment(b, p, timestamp, paragraphs())
|
||||
_, _ = bug.AddComment(b, p, timestamp, paragraphs(), nil, nil)
|
||||
}
|
||||
|
||||
func title(b bug.Interface, p identity.Interface, timestamp int64) {
|
||||
_, _ = bug.SetTitle(b, p, timestamp, fake.Sentence())
|
||||
_, _ = bug.SetTitle(b, p, timestamp, fake.Sentence(), nil)
|
||||
}
|
||||
|
||||
func open(b bug.Interface, p identity.Interface, timestamp int64) {
|
||||
_, _ = bug.Open(b, p, timestamp)
|
||||
_, _ = bug.Open(b, p, timestamp, nil)
|
||||
}
|
||||
|
||||
func close(b bug.Interface, p identity.Interface, timestamp int64) {
|
||||
_, _ = bug.Close(b, p, timestamp)
|
||||
_, _ = bug.Close(b, p, timestamp, nil)
|
||||
}
|
||||
|
||||
var addedLabels []string
|
||||
@ -182,5 +183,5 @@ func labels(b bug.Interface, p identity.Interface, timestamp int64) {
|
||||
// ignore error
|
||||
// if the randomisation produce no changes, no op
|
||||
// is added to the bug
|
||||
_, _, _ = bug.ChangeLabels(b, p, timestamp, added, removed)
|
||||
_, _, _ = bug.ChangeLabels(b, p, timestamp, added, removed, nil)
|
||||
}
|
||||
|
@ -235,7 +235,7 @@ func (repo *GoGitRepo) Keyring() Keyring {
|
||||
return repo.keyring
|
||||
}
|
||||
|
||||
// GetUserName returns the name the the user has used to configure git
|
||||
// GetUserName returns the name the user has used to configure git
|
||||
func (repo *GoGitRepo) GetUserName() (string, error) {
|
||||
return repo.AnyConfig().ReadString("user.name")
|
||||
}
|
||||
|
@ -60,9 +60,9 @@ type RepoKeyring interface {
|
||||
Keyring() Keyring
|
||||
}
|
||||
|
||||
// RepoCommon represent the common function the we want all the repo to implement
|
||||
// RepoCommon represent the common function we want all repos to implement
|
||||
type RepoCommon interface {
|
||||
// GetUserName returns the name the the user has used to configure git
|
||||
// GetUserName returns the name the user has used to configure git
|
||||
GetUserName() (string, error)
|
||||
|
||||
// GetUserEmail returns the email address that the user has used to configure git.
|
||||
|
@ -1,4 +1,4 @@
|
||||
// +build debugwebui
|
||||
//go:build debugwebui
|
||||
|
||||
package webui
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
// +build ignore
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by vfsgen; DO NOT EDIT.
|
||||
|
||||
// +build !debugwebui
|
||||
//go:build !debugwebui
|
||||
|
||||
package webui
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user